Thresholds (p75 field, p98 for INP)
| Metric | Good | NI | Poor |
| TTFB — Time to First Byte | ≤ 800ms | ≤ 1800ms | > 1800ms |
| FCP — First Contentful Paint | ≤ 1800ms | ≤ 3000ms | > 3000ms |
| LCP — Largest Contentful Paint | ≤ 2500ms | ≤ 4000ms | > 4000ms |
| INP — Interaction to Next Paint | ≤ 200ms | ≤ 500ms | > 500ms |
| CLS — Cumulative Layout Shift | ≤ 0.1 | ≤ 0.25 | > 0.25 |
| TBT — Total Blocking Time | ≤ 200ms | ≤ 600ms | > 600ms |
Fix quick-map
| Problem | First checks | Common fixes |
| High TTFB | Server timing, CDN cache, redirects | Edge cache, query optimization, Early Hints |
| High LCP | LCP element in trace, priority, lazy? | preload + fetchpriority, dimensions, AVIF, no client-only hero |
| High INP | Long tasks, interaction in Performance | yield, workers, defer hydration, cut third-party JS |
| High CLS | Layout shift regions in DevTools | width/height, aspect-ratio, font fallbacks, ad slots |
| High TBT (lab) | Main thread flame chart | Same as INP — split tasks, less sync JS on load |
API snippets
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
// Long tasks
new PerformanceObserver((l) => {
l.getEntries().forEach((e) => console.log('long', e.duration));
}).observe({ type: 'longtask', buffered: true });
// Yield
await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
Resource hints
<link rel="preconnect" href="https://cdn.example" crossorigin>
<link rel="preload" href="/hero.avif" as="image" fetchpriority="high">
<link rel="modulepreload" href="/app.js">
<link rel="prefetch" href="/next-page.js">
CSS performance
.item {
content-visibility: auto;
contain-intrinsic-size: 0 120px;
contain: layout style paint;
}
/* Animate only: */
.panel { transform: translateY(0); opacity: 1; }