# 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.