#How to apply this file
Each section opens with one imperative line; apply every rule in the section it introduces. Do not summarise or skip a section.
#Purpose
Rules for making a web application fast. Performance work is worth doing only against measurements — and against the right ones: field data from real users, not a local build on a fast laptop.
Optimising what you have not measured is how teams ship a 40 KB saving on a page whose problem is a 3-second server response.
#Measure the right things
[INST] Apply every rule in this section: Measure the right things. [/INST]
| Metric | Target | What it reflects |
|---|---|---|
| LCP | < 2.5s | When the main content appears |
| INP | < 200ms | Responsiveness to interaction |
| CLS | < 0.1 | Visual stability |
| TTFB | < 800ms | Server and network before anything renders |
| Total JS | < 200 KB compressed | The dominant cost on mobile |
Field data (Chrome UX Report, web-vitals in production) beats lab data
(Lighthouse). Lab data is reproducible; field data is true.
tsimport { onLCP, onINP, onCLS } from "web-vitals";
onLCP(send); onINP(send); onCLS(send); // report p75 by route and device class
Track p75, segmented by device class and connection. A p50 on desktop hides the experience of the median mobile user entirely. Test on a mid-range Android device with CPU throttling, not on your development machine.
#JavaScript is the expensive part
[INST] Apply every rule in this section: JavaScript is the expensive part. [/INST]
A byte of JavaScript costs far more than a byte of image: it must be downloaded, parsed, compiled and executed, on the main thread.
- Measure the bundle in CI and fail the build on a regression
(
size-limit,bundlesize). Growth is otherwise invisible until it is large. - Analyse before optimising (
@next/bundle-analyzer,rollup-plugin-visualizer). It is usually one dependency, not a hundred small things. - Route-level code splitting first, then component-level for genuinely heavy things — a chart library, a rich text editor, a date picker.
- Check for duplicate copies of the same library at different versions.
- Prefer platform APIs:
Intl.DateTimeFormatinstead of a date library,fetchinstead of a client,structuredCloneinstead of a deep-clone helper. - Load third-party scripts with
deferorasync, from a consent gate, and audit them regularly — analytics and tag managers are frequently the largest script on the page and nobody owns them.
tsxconst Chart = lazy(() => import("./Chart")); // loaded when rendered, not at boot
Never ship a library for one function. A 70 KB dependency imported for
debounce is the most common single avoidable regression.
#Images and fonts
[INST] Apply every rule in this section: Images and fonts. [/INST]
Images are usually the LCP element, and fonts are usually the cause of layout shift.
html<img src="hero.avif" width="1200" height="630" alt="…"
fetchpriority="high" decoding="async" />
<img src="below.avif" width="400" height="300" alt="…" loading="lazy" />
- Always set
widthandheight(oraspect-ratio). Without them the layout shifts when the image loads — the main cause of CLS. loading="lazy"on everything below the fold; never on the LCP image, which needsfetchpriority="high".- Serve AVIF or WebP with
srcset/sizesso a phone does not download a desktop-sized image. - Fonts:
font-display: swap,preloadthe one font used above the fold, subset it, and self-host.@importfrom a third party costs an extra connection and round trip before any text renders. - Declare
size-adjust/ascent-overrideon the fallback font so the swap does not shift the layout.
#Rendering cost
[INST] Apply every rule in this section: Rendering cost. [/INST]
- Virtualise long lists (
@tanstack/virtual). Rendering 10,000 rows is slow no matter how cheap each row is. - Keep the main thread free: heavy computation belongs in a web worker.
- Debounce or throttle high-frequency handlers; use
useDeferredValueto keep input responsive while an expensive list catches up. - Avoid layout thrash — batch DOM reads and writes rather than interleaving them.
- Animate
transformandopacityonly; animatingwidth,toporbox-shadowtriggers layout or paint on every frame. - Prefer CSS to JavaScript for animation, and honour
prefers-reduced-motion. →Frontend/react
#Network and delivery
[INST] Apply every rule in this section: Network and delivery. [/INST]
- Cache static assets immutably with content hashes:
Cache-Control: public, max-age=31536000, immutable. - HTML is
no-cacheor short-lived; it is what points at the hashed assets. - Serve from a CDN close to users; compress with Brotli.
preconnectto critical third-party origins;preloadgenuinely critical resources only — over-preloading competes with the resources that matter.- Prefetch the next likely route on intent (hover, viewport), not everything.
- Server-render or statically generate content-heavy pages; a client-rendered page
cannot have a good LCP because nothing renders until the JavaScript arrives.
→
Frontend/server-components
#Anti-patterns
[INST] Apply every rule in this section: Anti-patterns. [/INST]
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Optimising without measuring | Effort on the wrong thing | Field data first |
| Lab data only | Hides the real user experience | RUM at p75 by device class |
| Testing on a development machine | Users are on mid-range phones | Throttled real devices |
| No bundle budget in CI | Growth is invisible until it hurts | size-limit gate |
| A library for one utility | Tens of KB for a few lines | Platform API or inline it |
| Everything in one bundle | Long time-to-interactive | Route and component splitting |
| Images without dimensions | Layout shift; poor CLS | width/height or aspect-ratio |
| Lazy-loading the LCP image | Delays the metric it defines | fetchpriority="high" |
| Unoptimised formats and sizes | Megabytes over mobile networks | AVIF/WebP with srcset |
Third-party fonts via @import | Extra connection before text renders | Self-host and preload |
No font-display | Invisible text, then a shift | swap plus metric overrides |
| Rendering huge lists | Slow render and interaction | Virtualise |
| Heavy computation on the main thread | Blocks interaction; ruins INP | Web worker |
| Animating layout properties | Layout and paint every frame | transform and opacity |
| Preloading everything | Competes with what matters | Preload deliberately |
| Unaudited third-party scripts | Often the largest script; nobody owns them | Inventory and review |
| Client-rendering content pages | LCP cannot be good | Server-render |
#Checklist
- Verify: LCP, INP and CLS are collected from real users and reviewed at p75
- Verify: Metrics are segmented by device class and route
- Verify: Testing includes a throttled mid-range mobile device
- Verify: A bundle-size budget is enforced in CI
- Verify: The bundle has been analysed and large dependencies justified
- Verify: Routes are code-split; heavy components load on demand
- Verify: No dependency is included for a single small utility
- Verify: Third-party scripts are inventoried, deferred and consent-gated
- Verify: Every image declares dimensions or an aspect ratio
- Verify: The LCP image is prioritised and never lazy-loaded
- Verify: Images are served in modern formats with responsive sizes
- Verify: Fonts are self-hosted, subset, preloaded, with
font-display: swap - Verify: Fallback font metrics are adjusted to avoid swap shift
- Verify: Long lists are virtualised
- Verify: Expensive computation runs off the main thread
- Verify: Animations use only
transformandopacity, honouring reduced motion - Verify: Static assets are content-hashed and cached immutably
- Verify: Content-heavy pages are server-rendered or statically generated