
Last time we fixed the queries. This time: the browser. If your backend is fast and your page still feels slow, the problem is almost never “the internet.” It’s one of four things, in order of how often I find them:
1. Render-blocking assets you forgot existed
Every <script> without defer and every stylesheet in the head blocks first paint. The classic offender is a font request from a third-party domain, sitting at the top of the critical path:
<!-- bad: blocks rendering until the font CDN answers -->
<link rel="stylesheet" href="https://fonts.example.com/...">
<!-- better: preload the actual file, keep it in the critical path on purpose -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<style>font-display: swap;</style>
Self-host the font. One request, cached, no third-party latency, no privacy leak.
2. Layout shift from images with no dimensions
Cumulative Layout Shift is the “the button moved and I clicked the wrong thing” tax. The fix is one line most sites never do:
<img src="/photo.jpg" width="800" height="500"
loading="lazy"
style="aspect-ratio: 8/5" alt="...">
Set width/height on every image tag (or an aspect-ratio in CSS). Browsers reserve the box before the bytes arrive. In my experience this single habit eliminates most CLS complaints from users.
3. The JavaScript you never audited
Rule of thumb I actually use: if a script isn’t required for the page’s first meaningful interaction, it does not belong in the initial bundle. Analytics, chat widgets, and tag managers ship with enough JS to run a small company. Load them after interaction, or on requestidlecallback:
const start = () => {
const s = document.createElement('script');
s.src = '/chat-widget.js';
s.async = true;
document.body.appendChild(s);
};
if ('requestIdleCallback' in window) {
requestIdleCallback(start, { timeout: 3000 });
} else {
setTimeout(start, 1200);
}
4. Your cache headers are lying to the browser
A PNG with no Cache-Control gets re-validated or re-fetched on every visit. Static assets should be immutable:
Cache-Control: public, max-age=31536000, immutable
Content-Type: image/png
Version your filenames when you change them (app.3f2a.js) and the “immutable” part becomes safe. Do this and the second visit to your site gets dramatically faster — with zero code changes.
The fifteen-minute audit
- Open DevTools → Network → disable cache → load your homepage on “Slow 3G”.
- Note the first paint. Now enable cache and reload — the delta is your cache-header story.
- Sort by transfer size. Anything above 100 KB that isn’t your app is a candidate for lazy-loading or removal.
- Check every
<img>for missing dimensions. Fix them today.
That’s the whole series so far: the database, the browser, and the two habits that keep both honest. I’ll pick up from here with deployment — where “it works locally” goes to die.
— Stella