# Most JavaScript performance problems aren't JavaScript problems

> After enough profiling sessions you start to realise the language is rarely the bottleneck. Our assumptions usually are.

By Matthew D. Webb · 2021-02-01 · 5 min read
Canonical: https://mdwebb.io/articles/most-js-performance-problems-arent-js/
AI involvement: Updated with images, diagrams and the interactive demo.

---

Front-end performance discussions have a habit of collapsing into JavaScript discussions almost immediately. Bundle size, render speed, memory, framework choice, language features, the optimisation trick someone read about on a Tuesday. All of it matters, and all of it tends to crowd out a more useful observation.

Most of the performance problems I have actually chased down in production were not caused by JavaScript at all. They were caused by architecture, or data, or work that never needed doing, or an assumption nobody questioned because the thing ran perfectly well on a developer laptop with a fast connection and no real data in it.

## The first suspect is usually the wrong one

Performance investigations tend to open the same way. A page feels slow, users say so, the synthetic monitoring starts drawing the wrong shape, and someone opens DevTools and goes hunting for expensive JavaScript. Sometimes they find it. More often they find something a lot less dramatic: the browser sitting on its hands waiting for data, or pulling down a payload far larger than anyone intended, or dutifully rendering several thousand rows of a list the user can see forty of, or recomputing the same state on a loop because three components cannot agree on who owns it.

The page feels slow, and the language is barely involved. The time is being spent somewhere else entirely, and if you go in assuming the bottleneck is your code, you will find something to "fix" and walk away convinced, while the actual cost sits untouched.

<figure>
  <img src="/diagrams/where-time-goes.svg" alt="A horizontal bar breaking down a slow page load: most of the time is spent waiting on the network and data, a smaller share on rendering and layout, and only a thin sliver on JavaScript execution." />
</figure>

## The network is still undefeated

Front-end engineers can get romantic about milliseconds, and [now and then they are right to](/articles/latency-budgets-trading-uis/). But a single unnecessary network round trip will eat a week of careful micro-optimisation and come back asking for seconds.

The classic way to lose here is to build a dependency chain without noticing you have built one. Request A has to finish before B can start, B before C, and only once C lands can anything render. Each request, taken alone, is perfectly respectable. Strung end to end, they are a loading spinner with ambitions. The fix is almost never to make the requests faster; it is to stop making them wait for each other.

<div data-island="waterfall-demo"></div>

The JavaScript doing the work is identical in both arrangements. What changes is how the work is organised, and that is usually where the real seconds are hiding. Performance is decided less by how fast a line of code runs than by [how intelligently the work around it is scheduled](/articles/the-event-loop-is-not-what-you-think/).

## State management has a cost

Most teams learn the same thing about state, usually the hard way: every piece of it has to be created, updated, kept consistent, and eventually torn down, and all of that is work. Applications rarely get slow for want of state. They get slow because they hoard it.

This shows up most clearly in large React codebases. A value gets lifted somewhere global because it might be handy later. A component subscribes to it. A third derives more state from that. Nobody did anything obviously wrong at any step, and yet a single innocuous click now re-renders half the screen, because the data flow grew into a shape no one designed and no one is quite responsible for.

<figure>
  <img src="/diagrams/state-cascade.svg" alt="A global store at the top feeding a derived value and several components below it. One user interaction sends re-render signals fanning out across the whole tree." />
</figure>

## Frameworks are usually innocent

The internet loves a framework argument. Every couple of years a new one turns up promising dramatic performance, and sometimes the promise is even real - though the gains are usually less about the framework and more about it nudging you into better decisions you could have made anyway.

Most mature frameworks are far more capable than the argument gives them credit for. The big wins almost always come from the same unglamorous list:

- Doing less work.
- Loading less data.
- Rendering less content.
- Caching what you can.
- Being smarter about the network.

Swapping framework is occasionally the right call. It is very rarely the first one, and reaching for it early is usually a way of rewriting the application instead of understanding it.

## Measure before you optimise

The easiest mistake in this whole field is optimising on a hunch. The code looks inefficient, the algorithm feels expensive, the implementation seems wrong, so you fix it - and then the profiler tells you it was contributing nothing to what the user felt. You can spend a thoroughly satisfying afternoon making a cold path faster.

The best performance work I have watched always starts in the same boring place: measurement. Not intuition, not taste, not whichever architectural opinion you walked in holding. Numbers. The discipline is mostly about finding where the time is genuinely going, and the honest answer is surprising often enough that guessing is a bad habit to keep. You optimise what the system in front of you actually needs, not whichever fix would look most impressive in the retelling.

## The browser doesn't care how elegant you are

JavaScript takes an unusual amount of the blame for slow front ends. Some of it is fair. A lot of it is not. The longer I spend building these systems, the more performance looks like a systems problem rather than a language one. Applications get fast when a team genuinely understands its data flow, its rendering behaviour, the shape of its network, and the trade-offs underneath all three.

The language matters. The framework matters. The tooling matters. But the largest gains nearly always come from understanding the system as a whole rather than polishing one corner of it until it shines. The browser, in the end, is supremely indifferent to how elegant your code is. It only cares how much work you have asked it to do.

## Further reading

- [Critical rendering path](https://web.dev/articles/critical-rendering-path): explains the dependency chain between HTML, CSS and JavaScript that decides how fast anything appears.
- [Populating the page: how browsers work](https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work): MDN's tour of navigation, parsing and rendering, useful for seeing where the time really lands.
- [Analyze runtime performance](https://developer.chrome.com/docs/devtools/performance): a hands-on guide to the Chrome DevTools Performance panel, because measuring beats guessing.
- [The cost of JavaScript in 2019](https://v8.dev/blog/cost-of-javascript-2019): the V8 team on why download and CPU execution time dominate, and what to do about it.
