Developers should aim for UX rather than performance
At first, these two (2) terms mind sound irrelevant to one another. After all UX (User Experience) is a different specialty and can’t be directly related to engineering, or can it? You see, engineers tend to view UX as an external study that decides the way the elements are positioned in a screen & the flow that the users have to go through before they reaches their goal. While this is absolutely true, that’s only part of an equation that sometimes we engineers tend to forget: to provide the best experience for the users that are visiting our platform. Thus, UX doesn’t stop at the study level, but it’s ubiquitous and integrated to every aspect of the company. That means that even we — developers — should contribute & enhance the experience of our users in any way we can.
Approach & Core concepts
Although this may sound obvious, it’s something that’s frequently overlooked, since, by nature, engineers tend to focus on making a website fast by decreasing the loading time, removing complex animations to free up the main thread, code-splitting the modules, etc. What’s interesting, is that a performant & fast website doesn’t always go hand-in-hand with a good experience. We say that a certain website is fast, but we don’t ever stop to ask ourselves whether we have measured “fast” correctly. Does it matter if something is fast, if it doesn’t affect or is not noticed by the users in any way? Should speed matter if the experience is destroyed because of their device being incapable of providing the resources needed to render the app?
Let me give you an example. Suppose you have a Modal that’s code-splitted and loaded when its corresponding trigger button is clicked. Let’s say that the Modal’s code takes 100ms to be fetched, parsed and displayed to the users from the moment they click on the button. On a high-speed connection that might be fine, since the delay is so small that they’ll most likely not even notice. On a slow connection though, they might be left wondering whether they have clicked the button and they’ll most likely try clicking it again and again. Now, let’s suppose that the Modal didn’t have a trigger, but instead, showed up as soon as the app booted, prompting users to sign up to the platform in order to get a 10% discount. For most mobile users, loading a Modal on boot-time might be ok, but there might be some users browsing on a blackberry that went into overdrive in order to download the corresponding chunk, parse it & animate the appearance of this Modal.
From a developer’s standpoint, in both cases the website is performant since the code for the Modal has been successfully code-splitted, the main chunk’s footprint has been reduced and the features of the platform have been “progressively” downloaded. Unfortunately though, this had an impact on certain users’ experience. On the first scenario, some of them got frustrated by the fact that they were clicking a button and nothing was happening, while on the second, they had the app lag/freeze cause their old phone was on power saving mode on a hot summer day.
Although there are many ways to go around these issues, the core concept is that if a chunk is being loaded during a user interaction, then we should make sure to either defer it or load it earlier. Native APIs like
isInputPending or resource hints like preload and prefetch are particularly helpful in situations like these. On the other hand, if a chunk is loading while the user is not interacting with the website, then loading & parsing timings are are less important, since they don’t block or affect the user in any way. In general, it would be better to have a big chunk of code load slowly in the background when no action is happening, rather than load a small chunk of code during a user interaction (Interestingly enough, React’s concurrent mode will be utilizing a mechanism to correctly de-prioritize work when a user is interacting with any element on the screen). In the same way, if the user is browsing from a slow environment, we should review the features we expose in order to guarantee smoothness. Think about it for a second, would you rather have a powerful app that the users can’t browse or a more minimal one where they can at least see the core offering?
The way the chunks are split matters as well. If a script starts downloading, then there is not much you can do to stop it from being downloaded & parsed. Parsing a JS file results into main thread stress which might be noticed by the user. That means that you should carefully choose what you download, when you download it and how big its content will be. BestPrice handles that by splitting its code into small prefetchable chunks that are sequentially orchestrated. That means that chunk B will only be prefetched when chunk A has finished parsing and the user’s intent remains the same. Subsequently, chunk C will preload when chunk B has finished and so on. If at any point the user’s intent changes (i.e. he presses the back button or leaves the page), then the chunk pipeline stops (cancels early) and no more scripts are prefetched. Although this comes with a certain engineering overhead, it ensures that there will never be excess downloading & parsing on the main thread.
Closing Notes & Takeaways
Actions that directly impact the experience of our users, such as choosing when & where to code-split or when to fully omit a feature in certain environments, can’t be measured using the generic performance metrics. This is because they depend on dynamic variables, such as the the nature of the app itself and the context of the environment (battery, brightness, device, etc.). What might be slow in terms of metrics, might feel faster in terms of experience, simply because the user perceives it like so. Thus, although performance profiling can give us a ton of information about a problem, it should not necessarily be the solution to it. What we essentially want to measure is the gracefulness and smoothness of the users’ navigation and interactions, which is something directly associated with the business of each company. We strive for a better Lighthouse score, we add
PureComponent to our React app, we reduce the duplication of our critical CSS, but at the end of the day the real question is:
Did we make any significant enhancement to the experience of our users? If not, then our focus should be elsewhere…