# Patterns — Optimizing Web Performance ## Contents - LCP: hero image and critical path - Images: responsive, modern, shift-free - Fonts - JavaScript: splitting and on-interaction loading - Third-party scripts: facades and idle loading - INP: breaking long tasks - Measuring - Gotchas ## LCP: hero image and critical path ```html … ``` ## Images: responsive, modern, shift-free ```html … ``` - `width`/`height` (or CSS `aspect-ratio`) on every `img`/`video`/`iframe` — CLS zero-cost insurance. - `loading="lazy"` only below the fold; the browser handles the rest. ## Fonts ```html ``` ```css @font-face { font-family: 'Inter'; src: url('/fonts/inter-var.woff2') format('woff2'); font-weight: 100 900; /* one variable file replaces 5 static weights */ font-display: swap; /* text visible immediately with fallback */ } /* Reduce swap-induced shift: size-adjusted fallback */ @font-face { font-family: 'Inter-fallback'; src: local('Arial'); size-adjust: 107%; /* match Inter's metrics; tune per family */ } ``` Subset with `pyftsubset` (fonttools) to the scripts actually used — typically 30–100 KB → under 15 KB. ## JavaScript: splitting and on-interaction loading ```tsx // Route-level splitting (React Router / Next.js does this per page by default) const Settings = lazy(() => import('./Settings')); // Widget on interaction — nothing loads until the user needs it button.addEventListener('click', async () => { const { openChart } = await import('./chart.js'); openChart(data); }, { once: true }); ``` ```bash # Find what's actually in the bundle before removing anything npx source-map-explorer dist/assets/*.js ``` ## Third-party scripts: facades and idle loading ```html ``` ## INP: breaking long tasks ```js // Yield to the main thread between chunks of work (>50 ms tasks block input) async function processRows(rows) { for (const chunk of chunks(rows, 200)) { // 200 rows ≈ stays under 50 ms renderChunk(chunk); await (scheduler.yield?.() ?? new Promise(r => setTimeout(r))); } } ``` Pure computation (parsing, diffing, search indexing) → Web Worker; the main thread only renders. ## Measuring ```bash npx lighthouse https://example.com --preset=perf --form-factor=mobile --view ``` ```js // Field data from real users import { onLCP, onINP, onCLS } from 'web-vitals'; [onLCP, onINP, onCLS].forEach(fn => fn(m => navigator.sendBeacon('/vitals', JSON.stringify(m)))); ``` ## Gotchas - **Lazy-loading the LCP image** is the single most common self-inflicted LCP regression — audit every `loading="lazy"` above the fold. - **`preload` overuse** starves the network of bandwidth for actual critical resources — preload at most the LCP image and one font. - **`display: none` fonts still download** if declared in CSS — subset instead. - **CLS from late-loading banners/toolbars**: reserve the slot with `min-height` even when content is conditional. - **Debounce vs INP**: debouncing helps continuous input, but a *slow handler* needs chunking (see INP section) — debounce doesn't shorten the task. - **Bundle analyzer lies about tree-shaking** until you build in production mode — always analyze the production build. - **CDN cache misses** dominate TTFB for global users — check `cf-cache-status` / `x-cache` headers before optimizing the payload.