Back to writing

Small changes that make an SPA feel faster

Performance work can sound larger than it needs to be. I have sometimes been tempted to jump straight to a new caching layer, a complicated rendering strategy, or a long list of metrics.

In many SPA projects, I have found the first useful improvements are smaller. They come from noticing where people wait, then removing a little of that waiting without making the code harder to understand.

Start with the slow moment

Before changing anything, I find it helpful to look for the moment that actually feels slow. Is the first screen carrying too much JavaScript? Is an image much larger than it needs to be? Does a search wait for every keystroke? Is the page requesting the same data again when the user comes back to it?

Looking at the browser’s network and performance tools, then trying the flow on a slower connection or a smaller device, has often told me more than guessing from the code. The aim is not to optimise every number. It is to find one delay that people are likely to notice.

Send less at the beginning

One of the simplest improvements is sending less work on the first load. Images can be compressed, sized for the space they actually occupy, and loaded later when they sit below the fold. Routes or heavier features can be loaded when a user needs them instead of being part of the first bundle.

This does not mean splitting every component into its own file. I usually find it more useful to start with an obvious boundary: a route, a large editor, a chart, or a feature that many visitors never open. The goal is simply to let the first useful screen arrive sooner.

Avoid waiting when the product does not need to

Some waits come from the way the UI asks for data. If independent requests can start together, they may not need to wait for one another. If a user returns to a page and the data is still useful, keeping it for a short time can avoid another full loading state.

One after anotheruserordersstatsreadyStarted togetheruserordersstatsready
The same three requests. Started together, the screen is ready when the slowest one finishes, not when all of them have queued up.

Interactions deserve the same care. A search request may not need to run on every keystroke. A small debounce can reduce unnecessary work while still feeling responsive.

let timer;

input.addEventListener("input", (event) => {
  clearTimeout(timer);
  // wait for a short pause in typing before searching
  timer = setTimeout(() => search(event.target.value), 250);
});

Very long lists may need pagination or virtualisation, but only once the list is large enough for users to feel the cost.

Make the wait understandable

Some waiting will remain, and that is fine. What matters is whether the interface acknowledges it. I have found that a clear loading state, a button that responds right after an action, and content that does not jump around can make the same amount of time feel much less uncertain.

For me, this is the useful mindset for SPA performance: start with the real experience, make one clear improvement, then check whether it helped. Small changes made consistently can be more valuable than a large optimisation the product did not need.

Working on something similar? Let's talk