Threat modelling before the first line of code
Most breaches trace back to a design decision, not a coding mistake. Here is the two-hour session we run at the start of every build — and the template we use.
Web
We rebuilt six sites this year. The same four fixes accounted for most of the gain every time — and image budgets were not the biggest one.
We rebuilt six production sites this year with an explicit performance mandate. Going in, we expected image optimisation to dominate the wins. It did not. The same four fixes accounted for most of the improvement on every project, and only one of them involved images.
The single largest LCP improvement, on five of six sites, came from how fonts were loaded. A stylesheet request to a font CDN in the document head blocks rendering until it resolves — and on a cold 4G connection that is frequently 400–900ms before the browser has even discovered the font files it then has to fetch.
The fix is layered. Preconnect to the font origin so the TLS handshake happens in parallel. Load the stylesheet asynchronously so it never blocks the first paint. Set font-display: swap so text is visible immediately in a fallback. And — this is the part teams skip — pick a fallback stack with similar metrics so the swap does not shift your layout and wreck CLS.
Median LCP improvement across the six sites: 620ms.
Cumulative Layout Shift is almost never caused by something subtle. It is caused by an element that has no reserved space until its content arrives. Images without width and height attributes. Ad slots. Embedded videos. Banner components that render conditionally after a client-side check.
Setting width and height attributes on every image — even when CSS overrides the actual display size — lets the browser reserve the correct aspect ratio before the bytes arrive. Combined with aspect-ratio on containers for embeds, this took four of the six sites from a failing CLS to under 0.02.
On the two React-heavy sites, the dominant INP problem was hydration of components that had no interactivity at all — marketing copy, footers, static tables. The main thread was busy attaching event handlers to text.
Moving those to server components (or simply to plain markup) cut total blocking time by more than half. The general principle is worth stating plainly: JavaScript should be shipped for behaviour, not for templating. If a component never responds to input, it does not need to exist on the client.
Image work did matter — it was just not the top of the list. What produced measurable gains, in order:
Two popular optimisations produced no measurable benefit on any of the six projects. Critical CSS inlining was a wash once the stylesheet was small and cached properly, and it added real build complexity. Aggressive code splitting below about 30kB per chunk cost more in request overhead than it saved in parse time.
Measure on the device your users actually hold. A mid-range Android on a throttled connection tells you more than any lab score.
If you are starting today: fix fonts, reserve space for media, remove hydration from static components, then optimise images. Measure after each step with real-user data rather than a synthetic run. Four changes, in that order, got every one of our six rebuilds into the green.
Keep reading
Most breaches trace back to a design decision, not a coding mistake. Here is the two-hour session we run at the start of every build — and the template we use.
Demos are easy. Evaluation harnesses, grounding, injection defence and cost ceilings are what separate a prototype from a product.
What we learned building a patient app for clinics where the connection drops every few minutes — and why sync conflicts are a product decision.
Need this done, not just read?
If any of the above sounds like a problem you have, a thirty-minute call with the engineer who wrote it is free.