Most "frontend performance" checklists still read like it's 2021: split your bundles, compress your images, use a CDN.
All still true.
None of it explains why a site can pass Lighthouse and still feel janky the moment someone clicks something.
That gap exists because the metric that actually measures "does this feel responsive" changed.
Interaction to Next Paint (INP) replaced First Input Delay as an official Core Web Vital back in March 2024, and as of early 2026 it's still the metric most sites fail — reporting puts INP as the most commonly-missed Core Web Vital, with a large share of sites still over the 200ms threshold.
FID only measured the delay before the browser started processing your first click.
INP measures every interaction on the page — clicks, taps, key presses — and reports the worst one.
You can't game a single data point anymore.
That shift changes what "performance work" should mean in practice.
This isn't a rehash of the code-splitting/image-optimization basics — it's what's actually new or newly relevant on top of that foundation.
The old checklist still matters, briefly
Skip this section if you've already got these covered.
If not, do them first — nothing below replaces them:
-
Code splitting —
React.lazy()+Suspense, or Angular's lazy-loaded feature modules. Don't ship the whole app on first load. -
Image optimization — WebP/AVIF,
srcsetfor responsive sizing, lazy-load offscreen images. -
Render-blocking resources — inline critical CSS,
defer/asyncnon-critical JS,font-display: swap. - Caching + CDN — hashed filenames so aggressive caching is safe, serve static assets from an edge, not your origin.
These are still correct.
They're also table stakes — most competent frontend teams already do them.
What follows is where the actual gains are hiding in 2026.
1. Diagnose INP with the Long Animation Frames API, not guesswork
The old Long Tasks API told you that something blocked the main thread for 50ms+.
It didn't tell you what — was it a script, a style recalculation, layout?
The Long Animation Frames (LoAF) API exposes that breakdown: script attribution, style/layout duration, and which specific function was responsible.
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Blocking duration:', entry.blockingDuration);
for (const script of entry.scripts) {
console.log(script.name, script.duration, script.invoker);
}
}
});
observer.observe({
type: 'long-animation-frame',
buffered: true
});
If you've been trying to fix INP by guessing which component is slow, this replaces the guessing with an actual attribution trail. Chromium supports it; check current browser coverage before you build tooling around it exclusively.
2. Cut main-thread work with selective hydration
A big source of poor INP in component-heavy apps isn't one slow interaction — it's the sheer amount of JavaScript competing for the main thread before the page ever becomes interactive. Islands architecture (Astro, Qwik, and increasingly meta-frameworks built on React/Vue) addresses this directly: most of the page ships as static HTML, and only the components that actually need interactivity hydrate — and only when they're needed, not all at once on load.
The practical version of this in a React app you're not rewriting: audit what's actually interactive above the fold. A hero section with a static image and heading doesn't need to hydrate before a click handler three screens down does. If your framework supports partial or deferred hydration, that's real INP budget back.
3. Tell the browser what actually matters with fetchpriority
Browsers guess resource priority based on tag type and position. Sometimes the guess is wrong — a below-the-fold image loads before your actual LCP candidate. The fetchpriority attribute overrides that guess:
<img src="/hero.avif" alt="Product dashboard" fetchpriority="high" />
<img src="/footer-logo.svg" alt="Partner logo" fetchpriority="low" loading="lazy" />
It's a one-line change with a direct effect on LCP when your hero image was competing with less important requests. Check current support on Can I Use before relying on it as your only LCP fix — treat it as a targeted correction, not a substitute for actually optimizing the image itself.
4. Stop rendering what nobody can see
content-visibility: auto tells the browser to skip layout and paint work for off-screen content until it's about to become visible, while still reserving space so layout doesn't shift:
.long-list-item {
content-visibility: auto;
contain-intrinsic-size: 0 200px; /* estimated size, prevents CLS */
}
For long lists, comment sections, or any page with a lot of below-the-fold DOM, this cuts initial rendering cost without touching your component logic. It's a CSS-only performance win, which is rare enough to be worth calling out. Browser support has broadened over the last couple of years — verify current coverage for the browsers your users actually run.
5. Speculative navigation: prerendering before the click
The Speculation Rules API lets the browser prefetch or fully prerender a page before the user clicks the link, based on rules you define:
<script type="speculationrules">
{
"prerender": [{
"where": { "href_matches": "/blog/*" },
"eagerness": "moderate"
}]
}
</script>
On a prerendered navigation, the next page can feel instant — the browser already rendered it in a hidden tab.
Two honest caveats: it's explicitly a multi-page-application technique (it targets full document navigations, not SPA route changes), and it's still limited availability — Chromium-based browsers only, not Baseline yet.
If you run a content site, blog, or traditional MPA, it's worth testing. If you're a SPA shipping client-side routing, this doesn't apply to you the way code-splitting does.
6. Audit your bundle instead of guessing what's heavy
Tree-shaking and dependency audits aren't new, but the habit of actually looking still gets skipped under deadline pressure. Run a visualizer before you assume you know what's bloating the bundle:
npx vite-bundle-visualizer
# or for webpack
npx webpack-bundle-analyzer stats.json
The recurring surprise is always the same shape: one dependency pulled in for a single utility function, sitting at 30–40kb gzipped. You don't know that until you look at the treemap.
Trade-offs worth being upfront about
| Technique | Where it helps | Where it doesn't |
|---|---|---|
| LoAF API | Diagnosing why INP is bad | Doesn't fix anything by itself — it's a diagnostic tool |
| Selective hydration | Component-heavy apps with a lot of non-interactive content | Requires framework support; not a drop-in for existing React SPAs |
fetchpriority |
Correcting a wrong LCP-candidate guess | Doesn't help if the image itself is unoptimized |
content-visibility |
Long pages with lots of off-screen DOM | Needs a reasonable contain-intrinsic-size estimate or you'll get CLS instead |
| Speculation Rules | Multi-page apps, content sites | Doesn't apply to SPA client-side routing; Chromium-only today |
None of these replace the fundamentals. They're what's left to optimize once code splitting, image formats, and caching are already handled — which is exactly the point where a lot of teams stop, because the easy wins are gone and the next layer requires knowing these exist.
A quick checklist
- Have you measured INP with real user data (CrUX, RUM), not just lab Lighthouse scores?
- Do you know which interactions are slow, or are you guessing? (LoAF API answers this)
- Is everything above the fold actually necessary to hydrate immediately?
- Does your LCP image have
fetchpriority="high"and skiploading="lazy"? - Have you looked at a bundle visualizer in the last quarter, or are you assuming you know what's in there?
Worth discussing
If you're on a multi-page site, have you tried the Speculation Rules API yet — and did it actually feel instant, or did prerendering cost outweigh the win?
And for anyone dealing with INP specifically: what turned out to be your actual bottleneck once you looked, versus what you assumed it would be?
Top comments (1)
The LoAF API point is a good catch, actual attribution beats guessing every time. Bookmarking this for the content-visibility trick alone.