Skip to content
Back to the journal
Engineering7 min read

Shaving 400ms Off Every Page We Ship

The median looked fine, which is exactly why we missed it. Where the time actually went, in the order we found it.

Theo Marchetti

Engineering Lead ·

Cover image for Shaving 400ms Off Every Page We Ship

We did not have a performance problem. We had a performance problem on the pages that mattered, which is worse, because the average looked fine.

Our dashboard median was respectable. Our marketing pages, the ones a stranger meets first, were carrying nearly a second of avoidable delay. Here is where it went, in the order we found it.

Measure the ninety-fifth, not the median

The median describes the visit you would have had anyway. The ninety-fifth percentile describes the visitor on a train, on a four-year-old phone, deciding whether your product is worth the wait.

We moved every dashboard and every alert to the ninety-fifth percentile on the first day, and half the arguments about whether a change mattered simply stopped.

Fonts were the single biggest win

Two hundred and forty milliseconds, from one change.

We were loading four weights of a display face and three of a body face, all of them blocking, all of them fetched from a third-party origin that required its own connection handshake.

  • Self-host the files, so there is no second origin to connect to
  • Ship two weights, not seven, and let the browser synthesise nothing
  • Subset to the characters actually used
  • Preload only the one face that appears above the fold

The design did not change. Nobody noticed anything except that the text stopped arriving late.

Images were the second

Ninety milliseconds, and a lot of transferred bytes.

The rules we settled on are boring and they hold. Every image gets explicit dimensions so nothing shifts as it loads. Everything below the fold loads lazily. The hero, and only the hero, is marked high priority.

A layout shift is not a performance metric that happens to annoy people. It is the interface moving the button as they reach for it.

Then the unglamorous forty milliseconds

The rest came in small pieces, none of them worth a blog post on its own.

  1. 01A date formatting library, imported whole, used for one function. Replaced with nine lines.
  2. 02An analytics script loading before the page rendered instead of after.
  3. 03Three components that could render on the server and were not.
  4. 04A carousel on the homepage that shipped its entire library for a component ninety percent of visitors never scrolled to.

Put a ceiling on it, in the pipeline

None of this survives without enforcement, because performance regresses the way a room gets untidy: one reasonable decision at a time.

Our build now fails if the main bundle grows more than five percent in a single change. It has failed eleven times this year. Nine were legitimate and the budget was raised deliberately, in a conversation. Two were a library that should never have been added.

That ratio is the whole argument for having the check.

One considered issue, every other Tuesday

No roundups, no reposted threads. Just the notes we wish someone had written down before we learned it the expensive way.

Unsubscribe in one click. We never share the list.