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.

SymptomSignalMeasurementHypothesisEvidenceRoot CauseChangeValidationRegression 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.