Performance
How to make a page load about as fast as the network allows. The speed rules for any site, what moved the numbers in practice, and what to skip.
- Required 5
- Recommended 12
- Optional 5
- Avoid 1
Required
Keep load, response and stability fast
RequiredKeep the three Core Web Vitals in the good range for at least 75% of visits. LCP (main content shown) stays under 2.5 s, INP (reaction to a tap) under 200 ms and CLS (layout jumps) under 0.1.
Send small, modern images
RequiredSend AVIF or WebP images, each one sized for the screen it lands on with
srcsetandsizes, and always givewidthandheight.Cache files for a year and pages briefly
RequiredCache files with a content hash in their name for a year as
immutable. Give HTML amax-ageof at most 600 seconds, orno-cache.Compress every text response
RequiredCompress every text response with Brotli (or zstd), with gzip as a fallback, ideally ahead of time at the highest level.
Fail the build when pages get heavier
RequiredSet a budget for page weight, request count and lab metrics, and make the build fail when a change goes over it.
Recommended
Measure speed in visitors' browsers
RecommendedCollect LCP, INP and CLS from visitors' browsers on every page type, and report the 75th percentile for phones and desktops apart.
Load far-down images later
RecommendedLazy-load images and iframes below the fold (load them only when someone scrolls near), and never the main image at the top.
Hint only what speeds up the first paint
RecommendedPreload only what the first paint needs, prefetch likely next pages, and preconnect only to other origins you are certain to use.
Say which request headers change a response
RecommendedSend
Varywith every request header that changes a response, such asAccept-EncodingandAccept.Answer repeat requests with 304
RecommendedSend an
ETagorLast-Modifiedon responses, and answer matching requests with 304 Not Modified.Ignore tracking parameters in caches
RecommendedTell caches to ignore tracking parameters with
No-Vary-Search, so one page isn't stored many times.Load fonts without moving the page
RecommendedSelf-host fonts as WOFF2, subset them, and add fallback fonts adjusted to the same size. Never let a late font move the page. Use
font-display: optionalon long text, andswaponly where you measured no shift.Put the styles in the page
RecommendedPut nothing in the head that blocks the first paint. Inline the key CSS and load the rest without blocking.
Never let a script block the page
RecommendedLoad scripts as
type="module"or withdefer, keep blocking scripts out of the head, and send as little JavaScript as possible.Break up long tasks
RecommendedNever run JavaScript for more than 50 ms at a time. Split long work into chunks, and let the browser handle input between them.
Fetch the next page on hover
RecommendedFetch or fully load likely next pages ahead of time with Speculation Rules, but skip addresses that change something.
Keep the back button instant
RecommendedKeep every page able to use the back/forward cache, which shows a page instantly on Back. Use no
unloadhandlers, and noCache-Control: no-storeon ordinary pages.
Optional
Send early hints only when they help
OptionalSend 103 Early Hints (a note sent before the answer) only for critical, safe files. Check that first paint gets faster.
Skip work for what is off screen
OptionalSkip drawing content that is off screen with
content-visibility: auto, and start heavy widgets only when they come into view.Fence off self-contained parts
OptionalUse
containon self-contained parts of the page, so the browser can limit layout and paint work.Use shared dictionaries for big bundles
OptionalUse shared compression dictionaries only for bundles over 100 KB compressed that change at least monthly. Skip them otherwise.
Report how long the server took
OptionalReport how long the server took in a
Server-Timingheader, without internal names.
Avoid
Drop old HTTP/1 tricks
AvoidDon't use old HTTP/1 tricks such as extra asset domains, image sprites or merging everything into one file. With HTTP/2 they do harm.
Reaching the network floor
A new visitor's page can't arrive sooner than three round trips. They are the TCP handshake, the TLS 1.3 handshake and the request itself. That is the network floor. The main image needs at least one more. A fast page gets as close to that floor as it can:
- Fit the first send. Keep the compressed HTML small (10 to 15 KB) and raise the initial congestion window, so the page arrives in one go.
- Make first paint depend on nothing else. Inline the CSS, keep scripts deferred, and don't preload fonts. First paint then follows the HTML by the time it takes to read and draw it. On a reference site that was about 40 to 55 ms.
- Start the main image at once. Put it early in the HTML with
fetchpriority="high"and a size that fits the screen, so it arrives one round trip after the HTML. - Make clicks free. Prefetch on hover or touch, fill a service worker cache with the page's links after load, and skip page-transition animations.
After that, only protocol features that skip handshakes can help. TLS 1.3 resumption with 0-RTT needs an earlier visit. HTTP/3 saves the TCP handshake, but on a first visit only with an HTTPS DNS record.
Distance from the floor
An earlier lab server with HTTPS, HTTP/2 and initcwnd 32, at three round-trip times. Floor = TCP + TLS 1.3 + HTML = 3 round trips. A separate main image adds one. Times in ms, phone 3× / desktop 2×.
| Round trip | HTML, separate image (inlined image) | Image shown, separate | Image shown, inlined | 4-round-trip floor |
|---|---|---|---|---|
| 12 | 39 (51) | 90 / 86 | 78 / 86 | 48 |
| 50 | 153 (203) | 222 / 222 | 230 / 238 | 200 |
| 100 | 303 (403) | 418 / 422 | 430 / 438 | 400 |
- The HTML arrives at exactly 3 round trips plus about 3 ms at every distance.
- The image arrives at 4 round trips plus about 20 ms to decode and paint it.
- Putting the image inside the HTML as base64 wins only on very short links. From a 50 ms round trip up, the page grows past the first send and costs a round trip.
Tried and rejected
| Option | Why not |
|---|---|
| Inlining the main image | Slower from a 50 ms round trip up (table above). |
| HTTP/3 for first visits | Browsers switch after seeing Alt-Svc, so a first visit gains nothing without an HTTPS DNS record. Older servers, such as nginx before 1.25, have no HTTP/3. |
tcp_slow_start_after_idle off, BBR | Help repeat or long connections, not a first visit. |
| Autoplaying animations | Cost work on phones and break reduced-motion expectations. Start them on a click. |
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers website playbook: https://agentwranglers.se/playbook/website/performance.