6 lessons

Frontend Performance

The user's half of the latency budget: the browser waterfall, Core Web Vitals as user-experience signals, JavaScript cost beyond bytes, images, and layout work that blocks the first paint.

Symptom→Signal→Measurement→Hypothesis→Evidence→Root Cause→Change→Validation→Regression Check

Every lesson below starts from an observable symptom and ends with the measurement that proves the fix worked. Numbers carry a label saying whether they were measured, estimated, simulated or invented to show a shape.

The Half of the Budget You Cannot See From the Server

Your p99 is 80ms and users still call the app slow. Server time is one line item in a budget that also contains DNS, TLS, render-blocking CSS, JavaScript parse and execute, and an image decode on a phone three years older than your laptop.

Symptom · Support tickets and survey answers say "slow" while every backend dashboard is green. The complaint clusters on mobile, on first visits, and on the marketing-heavy routes.
Core Web Vitals as Signals, Not Scores

LCP asks "did anything useful appear?", INP asks "did the page answer when I touched it?", CLS asks "did it move while I was reading?". They are proxies for three different user frustrations — and they are web-platform definitions that have already changed once.

Symptom · A dashboard shows a vitals score that is either reassuring or alarming, and nobody can say which specific user experience it corresponds to or what would move it.
Reading the Browser Waterfall
▶ lab

HTML discovers CSS, CSS blocks the paint, script blocks the parser, and the image nobody prioritised is fetched last. The waterfall shows a dependency chain — and the difference between a resource being downloaded and the page being usable.

Symptom · The page shows blank or unstyled content for seconds. Individual resources all download quickly when inspected on their own, so nothing looks obviously wrong.
JavaScript Costs Four Times, Not Once

A 400KB bundle is not one cost. It is downloaded, parsed, compiled and executed — and gzip only helps with the first of those. The last three are CPU on a device you did not choose and cannot upgrade.

Symptom · Transferred bytes drop after a compression or CDN change, and time-to-interactive on real devices barely moves. The gap between "loaded" and "responds to taps" stays stubbornly wide.
Images: The Largest Bytes, Rarely the Largest Block

Images are usually most of a page's weight and rarely the thing holding up interaction. They are also where the cheapest wins live — serving a 3000px photo into a 400px slot is a mistake that costs nothing to fix and shows up immediately in LCP.

Symptom · Page weight is dominated by images. LCP is poor on mobile. Compressing everything produces a smaller page and a barely-changed LCP.
Layout, Paint and the Main Thread

One thread runs your JavaScript, computes layout, paints, and handles the user's tap. A 300ms task anywhere in that list means a 300ms wait everywhere else in it — which is why "the page freezes when I scroll" and "my handler is slow" are the same bug.

Symptom · Taps and scrolls feel sticky or delayed. Animations stutter. The page is fully loaded, so no loading metric shows anything wrong.