Web Performance Checklist 2026: Core Web Vitals and Beyond
Page speed affects SEO, conversion, and user experience. A practical checklist covering LCP, CLS, INP, and the optimizations that actually move metrics.
TL;DR
- Core Web Vitals: LCP ≤2.5s, CLS ≤0.1, INP ≤200ms. These affect SEO ranking and user experience.
- Field data (real users) matters more than lab data (Lighthouse)—Google ranks based on the 75th percentile of actual visits.
- LCP: Optimize images (WebP/AVIF, proper sizing), preload above-fold images, reduce server response time to <800ms.
- CLS: Set explicit dimensions on images and ads, use font-display: swap with size-adjusted fallbacks.
- INP: Break long tasks (yield every 8–16ms), defer non-critical JavaScript, offload heavy work to Web Workers.
- Add Lighthouse CI budgets to deployment pipelines to prevent performance regression.
- Performance is an ongoing practice, not a one-time fix.
Core Web Vitals Overview
Google’s Core Web Vitals are the metrics that matter for SEO and user experience:
| Metric | Full Name | Measures | Good | Needs Improvement | Poor |
|---|---|---|---|---|---|
| LCP | Largest Contentful Paint | Load speed | ≤2.5s | 2.5–4.0s | >4.0s |
| CLS | Cumulative Layout Shift | Visual stability | ≤0.1 | 0.1–0.25 | >0.25 |
| INP | Interaction to Next Paint | Interactivity | ≤200ms | 200–500ms | >500ms |
These are measured at the 75th percentile of real user experiences. If 75% of your users have LCP under 2.5s, you pass.
Lab vs. Field Data
| Data Type | Source | Use Case |
|---|---|---|
| Lab data | Lighthouse, WebPageTest | Development, debugging |
| Field data | Chrome UX Report (CrUX), RUM | Actual ranking signal |
Important: Lighthouse scores don’t directly affect rankings. Field data does. A site can score 100 in Lighthouse but fail Core Web Vitals if real users experience poor performance.
Use lab data to diagnose issues. Use field data to measure success.
LCP Optimization
Largest Contentful Paint measures when the main content becomes visible.
What Affects LCP
| Factor | Impact | Priority |
|---|---|---|
| Server response time | High | Fix first |
| Render-blocking resources | High | Fix early |
| Resource load time | High | Optimize images |
| Client-side rendering | Medium | Consider SSR |
Server Response Time (<800ms)
# Enable compression
gzip on;
gzip_types text/plain text/css application/json application/javascript;
# Caching headers
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Optimize database queries, use CDN, upgrade hosting if TTFB exceeds 800ms.
Image Optimization
Images are typically the LCP element. Optimize them:
<!-- Responsive images with modern formats -->
<picture>
<source
srcset="image.avif"
type="image/avif"
>
<source
srcset="image.webp"
type="image/webp"
>
<img
src="image.jpg"
alt="Description"
width="1200"
height="630"
loading="eager"
fetchpriority="high"
>
</picture>
For LCP images specifically:
- Use
loading="eager"(not lazy) - Add
fetchpriority="high" - Preload in
<head>
<link
rel="preload"
as="image"
href="hero.webp"
fetchpriority="high"
>
Eliminate Render-Blocking Resources
<!-- Defer non-critical JavaScript -->
<script src="analytics.js" defer></script>
<!-- Async load non-critical CSS -->
<link
rel="preload"
href="below-fold.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
>
Inline critical CSS for above-fold content:
<style>
/* Critical CSS inlined */
.hero { ... }
.nav { ... }
</style>
CLS Optimization
Cumulative Layout Shift measures unexpected layout movement.
What Causes Layout Shifts
| Cause | Fix |
|---|---|
| Images without dimensions | Set width/height |
| Ads/embeds without space | Reserve placeholder |
| Web fonts causing FOIT/FOUT | font-display: swap + fallback |
| Dynamic content insertion | Reserve space or animate |
Always Set Image Dimensions
<!-- Always include width and height -->
<img
src="image.jpg"
alt="Description"
width="800"
height="600"
>
Or use CSS aspect ratio:
.image-container {
aspect-ratio: 16 / 9;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
Web Font Optimization
/* Size-adjusted fallback prevents layout shift */
@font-face {
font-family: 'Custom Font';
src: url('font.woff2') format('woff2');
font-display: swap;
size-adjust: 105%;
ascent-override: 95%;
descent-override: 22%;
}
/* Fallback stack with similar metrics */
body {
font-family: 'Custom Font', 'Helvetica Neue', sans-serif;
}
Reserve Space for Dynamic Content
/* Reserve ad space */
.ad-slot {
min-height: 250px;
background: #f0f0f0;
}
/* Skeleton for async content */
.content-skeleton {
min-height: 200px;
animation: shimmer 1.5s infinite;
}
INP Optimization
Interaction to Next Paint measures responsiveness to user input.
What Causes Poor INP
| Cause | Fix |
|---|---|
| Long JavaScript tasks | Break into smaller chunks |
| Heavy event handlers | Debounce/throttle |
| Main thread blocking | Use Web Workers |
| Large DOM | Virtualize lists |
Break Long Tasks
// BAD: Long blocking task
function processAllItems(items) {
items.forEach(item => heavyOperation(item));
}
// GOOD: Yield to main thread
async function processAllItems(items) {
for (const item of items) {
heavyOperation(item);
// Yield every 8-16ms
if (shouldYield()) {
await scheduler.yield();
}
}
}
function shouldYield() {
// Use scheduler.yield() or requestIdleCallback
return performance.now() % 16 < 1;
}
Use requestIdleCallback
// Process work during idle time
function processInIdle(items, callback) {
const queue = [...items];
function processChunk(deadline) {
while (queue.length > 0 && deadline.timeRemaining() > 5) {
const item = queue.shift();
processItem(item);
}
if (queue.length > 0) {
requestIdleCallback(processChunk);
} else {
callback();
}
}
requestIdleCallback(processChunk);
}
Offload to Web Workers
// Main thread
const worker = new Worker('worker.js');
worker.postMessage({ data: heavyData });
worker.onmessage = (event) => {
// Result from worker
updateUI(event.data);
};
// worker.js
self.onmessage = (event) => {
const result = heavyComputation(event.data);
self.postMessage(result);
};
Performance Budget
Setting Budgets
// lighthouserc.json
{
"ci": {
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"first-contentful-paint": ["warn", { "maxNumericValue": 2000 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"interactive": ["warn", { "maxNumericValue": 3500 }],
"total-byte-weight": ["warn", { "maxNumericValue": 500000 }]
}
}
}
}
CI Integration
# GitHub Actions
name: Performance Check
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: npm run build
- name: Lighthouse CI
uses: treosh/lighthouse-ci-action@v11
with:
configPath: './lighthouserc.json'
uploadArtifacts: true
Monitoring and Measurement
Tools
| Tool | Purpose | Data Type |
|---|---|---|
| PageSpeed Insights | Quick check | Lab + Field |
| Chrome UX Report | Real user data | Field |
| Lighthouse | Deep analysis | Lab |
| WebPageTest | Detailed waterfall | Lab |
| RUM (Vercel, Cloudflare) | Continuous monitoring | Field |
Key Dashboard Metrics
Track these for your site:
## Weekly Performance Report
| Metric | This Week | Last Week | Target |
|--------|-----------|-----------|--------|
| LCP (p75) | 2.1s | 2.3s | ≤2.5s ✅ |
| CLS (p75) | 0.08 | 0.12 | ≤0.1 ✅ |
| INP (p75) | 180ms | 220ms | ≤200ms ✅ |
| FCP (p75) | 1.4s | 1.5s | ≤1.8s ✅ |
| TTI (lab) | 3.2s | 3.5s | ≤3.8s ✅ |
Complete Checklist
Images
- Use modern formats (WebP, AVIF)
- Resize to display size
- Set width and height attributes
- Lazy load below-fold images
- Preload LCP image with fetchpriority=“high”
- Use srcset for responsive images
Fonts
- Use font-display: swap
- Preload critical fonts
- Use size-adjusted fallbacks
- Limit font variations
- Self-host fonts (avoid external requests)
JavaScript
- Defer non-critical scripts
- Code-split large bundles
- Break long tasks (yield every 8–16ms)
- Use Web Workers for heavy computation
- Remove unused JavaScript
CSS
- Inline critical CSS
- Defer non-critical CSS
- Remove unused CSS
- Avoid layout-triggering properties in animations
Server
- TTFB < 800ms
- Enable compression (gzip/Brotli)
- Set cache headers
- Use CDN
- HTTP/2 or HTTP/3
Layout Stability
- Dimensions on all images
- Reserved space for ads/embeds
- No content insertion above viewport
- Smooth transitions for dynamic content
Monitoring
- Lighthouse CI in deployment pipeline
- Real user monitoring (RUM)
- Weekly performance review
- Alerts for regression
FAQ
Should I optimize for Lighthouse score or Core Web Vitals?
Core Web Vitals (field data). Lighthouse is a diagnostic tool, not the ranking signal. A perfect Lighthouse score with poor field data won’t help SEO.
How do I know if my changes helped?
Compare field data week-over-week. CrUX updates monthly; RUM tools update continuously. Allow 1–2 weeks after changes for meaningful comparison.
What’s the biggest quick win?
Image optimization. Most sites can improve LCP by 30–50% just by properly sizing and formatting images.
How often should I check performance?
Monitor field data weekly. Run Lighthouse on every deploy (via CI). Do deep analysis monthly.
Does performance really affect SEO?
Yes. Core Web Vitals are a ranking factor. Poor performance also increases bounce rate, which indirectly affects rankings. Fast sites convert better too.
What about single-page apps (SPAs)?
SPAs often struggle with INP. Use route-level code splitting, minimize initial bundle, and ensure transitions don’t block main thread.
Sources & Further Reading
- Top Core Web Vitals Improvements — Google’s priority list
- Ultimate CWV Checklist — Comprehensive checklist
- 30 Core Web Vitals Tips — Detailed tips
- Web Vitals Learning Pathway — Google training
- Advanced Core Web Vitals — Diagnosis and prioritization
- Image SEO Optimization — Related: image optimization
- Firefox Performance Debugging — Related: browser profiling
Interested in our research?
We share our work openly. If you'd like to collaborate or discuss ideas — we'd love to hear from you.
Get in Touch