Web Performance
Core Web Vitals, bundle size and code splitting, and the image and font decisions that move the numbers most.
Measure, Then Fix
Two kinds of data. Lab — Lighthouse on your machine — is repeatable and good for comparing before and after. Field — real users on real devices and networks — is the truth, and it is always worse than the lab.
Test on a mid-range phone over a throttled connection. A desktop on office wifi will tell you everything is fine, right up until it is not for the people actually using it.
Core Web Vitals
Three metrics, each standing for a different question a user is asking without thinking about it.
| Metric | Asks | Good | Usually caused by |
|---|---|---|---|
| LCP Largest Contentful Paint | Has the main thing appeared? | Under 2.5s | A slow server, or a huge hero image |
| INP Interaction to Next Paint | Does it respond when I touch it? | Under 200ms | Long JavaScript tasks blocking the main thread |
| CLS Cumulative Layout Shift | Does it stay still? | Under 0.1 | Images and ads with no reserved space |
CLS is the cheapest to fix and the most infuriating to experience — the tap that lands on the wrong thing because content moved. Set width and height on every image, reserve space for anything injected, and never insert content above what someone is already reading.
INP is about the main thread. JavaScript is single-threaded, so a long task means nothing else can happen — not a click, not a keystroke, not a repaint. Break long work up, or move it off the main thread.
Bundle Size & Code Splitting
JavaScript is the most expensive thing you ship, byte for byte. An image of the same size decodes off the main thread; JavaScript must be downloaded, parsed, compiled and executed before it does anything.
| Technique | Effect |
|---|---|
| Route-based splitting | Load only the current page's code |
| Lazy-load heavy components | Charts and editors arrive when opened |
| Audit before adding a dependency | A date library can outweigh your app |
| Prefer platform APIs | Intl and fetch cost nothing to ship |
| Defer third-party scripts | Analytics should never block first paint |
Run a bundle analyser before optimising anything. It routinely shows one dependency accounting for a third of the bundle, and often one nobody remembers adding.
Images & Fonts
Images are usually most of a page's bytes, so the decisions here move LCP more than almost anything else in your control.
| Do | Why |
|---|---|
| Serve modern formats | Far smaller than JPEG at the same quality |
Offer sizes with srcset | A phone should not download a desktop image |
| Set width and height | Reserves space, so no layout shift |
loading="lazy" below the fold | Do not spend bandwidth on unseen images |
| Never lazy-load the hero | It is your LCP element — prioritise it |
Fonts cause the other visible problem. A custom font that has not arrived yet leaves text either invisible or in a fallback that reflows when it swaps.
| Technique | Effect |
|---|---|
font-display: swap | Show fallback text immediately |
| Preload the critical font | Starts the download sooner |
| Subset the character set | Drops glyphs you never use |
| Match fallback metrics | Reduces the reflow when it swaps |
| Self-host | Removes a third-party connection |
Interview Questions
What are the Core Web Vitals?
LCP, how long until the main content appears, good under 2.5s. INP, how quickly the page responds to interaction, good under 200ms. CLS, how much the layout moves, good under 0.1.
How do you fix layout shift?
Reserve space before content arrives — width and height on images, fixed dimensions for embeds and ads, and never inserting content above something the user is already reading.
Why is JavaScript more expensive than an image of the same size?
An image is decoded off the main thread. JavaScript must be downloaded, parsed, compiled and executed on the main thread, blocking everything else while it runs.
Lab data looks great, field data is poor. Why?
Lab is a fast machine on a fast network with no extensions and a warm cache. Field is real devices, real networks and real third-party scripts. Field is the truth; lab is for comparing changes.
Which image should never be lazy-loaded?
The one above the fold, usually the hero — it is typically the LCP element, and deferring it directly delays the metric you are trying to improve.
What does font-display: swap do?Renders text in a fallback font immediately rather than leaving it invisible while the custom font loads. Text is readable sooner, at the cost of a reflow when the real font arrives — which matching fallback metrics reduces.