ISSUE 2026-09-27SERIES BREC 51TYPE BRIEFgarnetgrid.com
The question you are actually asking
Insights
Most performance work fails because it starts with a score instead of a cause. This is a practitioner's account of where the time actually goes: the four parts of Largest Contentful Paint, why responsiveness is a scheduling problem rather than a bundle-size problem, why layout shift is really a space-reservation problem, and the caching mistakes that are worse than simply being slow.
Most requests to improve performance arrive as a request to raise a score, and that is not the question. The real question is which two or three specific things sit between a user's tap and the first useful pixel on your slowest page, and whether you can name them. Performance work goes wrong when it becomes a general tidying programme rather than a hunt for one particular blocking resource, one unsized image, one handler doing synchronous work on every keystroke. Sites that score well and still feel slow are common, and the reason is almost always that they were measured on the wrong hardware, over the wrong network, on the wrong page. What follows is the order in which I would look.
Measure what your users get, not what your laptop gets
Running Lighthouse on a development machine over wired ethernet is a debugging tool. It is reproducible, it is useful for isolating a change, and it is not evidence about your users. Treat lab numbers as a hypothesis generator and field numbers as the result.
Field data means real devices, real networks, real cache states, real pages. Google's Core Web Vitals are assessed at the 75th percentile of page views, which is the part people skip over: the number you are looking at already means that roughly a quarter of visits are worse than that. A median that looks fine tells you very little.
Segment before you conclude anything. By page template, by device class, by country, by connection type. The useful finding is nearly always "the product listing template on mid-range Android handsets", not "the site". Aggregate over the whole site and the fault averages out into invisibility.
Two honest limitations worth stating internally. Your own repeat-visit experience, with a warm cache and a warm connection, resembles nobody arriving cold from search. And Chrome's field dataset only covers Chrome, so if a meaningful share of your traffic is iOS Safari you have no field data for it and will need your own real-user monitoring to see it at all.
Largest Contentful Paint is usually a delivery bug, not a size bug
LCP is widely reported as a single number, and that is what makes it hard to act on. It decomposes into four parts, and naming which part dominates on your page is most of the work:
In my experience the dominant part is usually load delay, and load delay is a discovery problem. The browser cannot fetch what it cannot see in the initial HTML. If the hero image is set by JavaScript, or comes from a CSS background, or sits behind a client-side route, the fetch cannot start until much later than it needed to, and no amount of recompressing the file will recover that time.
The most common self-inflicted version of this is `loading="lazy"` applied to every image by a component or a CMS, including the one above the fold. Lazy loading is for things below the fold; on the LCP element it actively delays the metric it appears to help. Mark the hero with high fetch priority, and reach for preload only when the resource genuinely is not discoverable from the markup. Preloads compete with each other, so preloading everything is the same as preloading nothing.
Fonts deserve their own look. If your LCP element is text in a web font, render delay waits on the font file, and that file often sits at the end of a dependency chain: HTML, then CSS, then the font. Self-hosting removes a connection setup to another origin. Whether you let the fallback font paint first is a design decision about which flash your brand can tolerate, not a performance decision, and it should be made by someone who owns the look.
Format and responsive sizing do matter. They are simply the second-order win once discovery is fixed.
Responsiveness is a scheduling problem
Interaction to Next Paint replaced First Input Delay as the responsiveness Core Web Vital, and it is a much harder metric to pass because it covers the whole interaction: the delay before the handler runs, the handler itself, and the time to present the resulting frame. It also reports close to the worst interaction on a page visit rather than the first one. The target is 200 milliseconds or less at the 75th percentile.
Bundle size is a proxy for this, and often a misleading one. What hurts is long tasks holding the main thread at the moment the user acts. A large script parsed once while the user is still reading can be less damaging than a small handler that runs layout-reading work on every keystroke.
The failure modes I see repeatedly are concrete. Hydrating a whole page when only one widget is interactive. A controlled input whose state update re-renders a large subtree on each character. An effect that writes to the very value it depends on, so it re-enters itself. A loop that reads `getBoundingClientRect` and then writes a style, forcing the engine to recompute layout synchronously on every iteration.
The fixes are scheduling fixes rather than size fixes. Paint the optimistic result first and do the expensive work after yielding. Break long tasks and hand control back to the browser between chunks. Move genuinely heavy computation off the main thread. Defer anything not required for the first interaction, and be honest about which interaction that actually is.
Profile this with CPU throttling on, and ideally on a real mid-range phone. On a developer workstation almost everything passes.
Layout shift is a reservation problem
Cumulative Layout Shift, with a target of 0.1 or less, is not really about things moving. It is about space that was never reserved. Anything that arrives after first paint and occupies room will push content around unless you booked the room in advance.
The usual culprits: images and iframes with no dimensions or aspect ratio; ads and embeds of unknown height; a consent banner injected into the top of the document flow; a font swap where the fallback and the web font have different metrics; and content inserted above the fold by a personalisation or experimentation script, which is the worst of the set because the team that owns it often has no idea it is a rendering change.
The remedies are boring and that is a good sign. Reserve a box for every late arrival. Make overlays genuinely overlay, positioned out of flow, rather than shoving the page down. Tune the fallback font's metrics so the swap is dimensionally neutral.
One trap: the score accumulates across the page's whole lifetime, not just load. Infinite scroll and lazily hydrated sections can fail a page that looks perfectly stable in the first second.
The origin, the edge, and the cache
Time to first byte is the floor under everything else. No front-end work recovers an origin that takes most of a second to respond, and it is worth knowing whether that time is queueing, database work, or template rendering before optimising anything downstream of it.
For static assets the configuration is uncontroversial: content-hashed filenames, a long max-age, marked immutable. Get it right once and stop thinking about it.
HTML is where real damage happens. A document that varies per visitor, by session, by cookie, or by entitlement must not be stored in a shared cache, and it is surprisingly easy to make it cacheable by accident. A proxy rule, an edge function that rewrites the HTML on the way through, a CDN default that quietly ignores your variance headers. The symptom is not slowness, it is one user seeing another user's page, or a deploy that appears not to have shipped for hours. This class of bug is worth a deliberate check after any change to the edge layer, because the performance work that caused it will look like a win right up until it does not. Where you can tolerate slightly stale HTML, serving stale while revalidating in the background is the honest middle ground.
Check that compression is actually enabled for every text content type, not just documents and scripts. JSON API responses and SVG are the ones I most often find uncompressed.
On high-latency mobile links the number of dependent round trips still matters more than raw bytes. A chain of document, then stylesheet, then a font stylesheet, then the font file, is four sequential waits before a single word of styled text appears.
Third-party code is a budget you do not control
Every third-party script is a standing bet that someone else's deployment will not damage your page. Tag managers are the sharpest version, because their contents change without a deploy on your side, which means your performance budget is enforced by people who have never read your code and are not measured on your metrics.
The recurring failures: a synchronous script from a slow origin blocking parsing; a consent platform that by design must run before everything else and therefore sits squarely on the critical path; an analytics library that patches history or installs a global error handler and changes how your own errors surface; a chat or map or video embed pulling a large payload for a control most visitors never touch.
For heavy embeds, a facade works well: render a static placeholder that looks right, and load the real component on first interaction. It is one of the few interventions that reliably removes a large cost without a product argument.
Then ask a different question of each vendor: what breaks if this is unreachable? If the answer is "the page", you have an availability defect wearing a performance costume.
There is no honest general figure for how much third-party JavaScript is acceptable. What you can do is enforce a budget in CI on transferred bytes and on the number of distinct origins, and make each addition a decision with a named owner and a review date.
Do it in this order. Get field data and segment it until you can name one template on one device class as the worst offender. Decompose that page's LCP into its four parts and fix the part that dominates, which is usually discovery of the hero image rather than its size. Profile the one interaction your users actually perform, on a throttled CPU, and look for the long task rather than the large file. Reserve space for everything that arrives late. Check your HTML caching rules specifically, separately from your asset rules. Then put a budget in CI so the next regression is caught by a machine instead of a complaint. And on the question everyone asks last, how fast is fast enough for your business: there is no honest general answer. The relationship between speed and abandonment is real but its shape is specific to your audience, your device mix and what you are asking them to do, and the only way to know your own numbers is to measure your own funnel.