We set a hard budget for this site: a 99 Lighthouse score on a throttled mobile connection, with no exceptions granted for marketing requests. Hitting it took a handful of specific decisions, most of them about what not to ship.
Measure the bundle before you optimise it
Guessing at bundle bloat wastes days. Run a bundle analysis first and sort by parsed size. Nearly every time we have done this, one or two dependencies accounted for the majority of the JavaScript, and neither was the one the team suspected.
Date formatting libraries, icon packs imported wholesale and animation runtimes are the usual offenders. Icon packs in particular are dangerous because the import looks tiny at the call site while pulling in a large module graph.
Server components are a size lever, not just an architecture choice
The App Router's default of server components means component code stays on the server unless you explicitly opt into the client. Every 'use client' directive is a decision to ship code to the browser.
We treat that directive as a reviewable event. If a component only needs interactivity in a small subtree, we push the boundary down to that subtree instead of marking the whole page. On this site that alone moved several hundred kilobytes off the critical path.
Fonts and images are where LCP is won
Largest Contentful Paint is usually an image or a heading. For headings, self-host fonts through next/font so the file is served from your own origin with a stable cache key, and always set a swap display so text renders immediately.
For images, the two rules that matter most are explicit dimensions and honest priority. Dimensions prevent layout shift. Priority should be set on exactly one above-the-fold image; marking several priority is the same as marking none.
- Self-host fonts and preconnect to any third-party image host.
- Give every image intrinsic width and height, or fill within a sized container.
- Set priority on the single LCP image only.
- Serve modern formats and let the framework generate the srcset.
Caching is the last 10 points
Once the payload is small, latency dominates. Static generation moves the work to build time, and a CDN puts the result near the visitor. Pages that genuinely need fresh data can revalidate on an interval rather than rendering on every request.
The pattern we settled on: statically generate everything that can be, revalidate what changes on a schedule, and reserve request-time rendering for genuinely personalised responses. Very little qualifies for that last bucket.
Key takeaways
- Analyse the bundle before optimising; the culprit is rarely the suspect.
- Treat every 'use client' as a decision to ship code to browsers.
- Explicit image dimensions and a single priority image fix most LCP and CLS issues.
- Static generation plus scheduled revalidation covers almost every page.
Ready to make the switch?
Try Zero Delay Analytics free. No credit card required, and no cookie banner needed.
Get Started