Tools
Tools: Core Web Vitals Optimization: A Practical Guide
2026-02-17
0 views
admin
The Three Metrics ## Measuring Before Optimizing ## Optimizing LCP ## 1. Preload the LCP Image ## 2. Use Responsive Images ## 3. Optimize Server Response Time ## 4. Inline Critical CSS ## Optimizing INP ## 1. Break Up Long Tasks ## 2. Use startTransition for Non-Urgent Updates (React) ## 3. Debounce Event Handlers ## Optimizing CLS ## 1. Always Set Image Dimensions ## 2. Use CSS aspect-ratio for Dynamic Content ## 3. Reserve Space for Async Content ## 4. Avoid Inserting Content Above Existing Content ## Real Results ## Key Takeaways Core Web Vitals directly impact search ranking and user experience. After optimizing several production applications, here's my practical playbook for hitting good scores on all three metrics. Always measure in the field, not just in lab conditions. LCP measures when the largest content element becomes visible. It's usually a hero image, heading, or text block. For SvelteKit, CSS is automatically inlined during SSR. For other frameworks, use tools like critters: INP (Interaction to Next Paint) replaced FID in 2024. It measures the responsiveness of all interactions, not just the first one. CLS measures unexpected layout shifts. It's the most frustrating metric for users. This is the most common CLS offender. Cookie banners, notification bars, and lazy-loaded headers all push content down. On this portfolio site, after applying these optimizations: The biggest wins came from image optimization (LCP), removing synchronous third-party scripts (INP), and setting explicit dimensions on all media (CLS). Originally published at umesh-malik.com Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
// web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals'; function sendToAnalytics(metric) { const body = JSON.stringify({ name: metric.name, value: metric.value, delta: metric.delta, id: metric.id, navigationType: metric.navigationType, }); navigator.sendBeacon('/api/analytics', body);
} onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals'; function sendToAnalytics(metric) { const body = JSON.stringify({ name: metric.name, value: metric.value, delta: metric.delta, id: metric.id, navigationType: metric.navigationType, }); navigator.sendBeacon('/api/analytics', body);
} onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics); CODE_BLOCK:
// web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals'; function sendToAnalytics(metric) { const body = JSON.stringify({ name: metric.name, value: metric.value, delta: metric.delta, id: metric.id, navigationType: metric.navigationType, }); navigator.sendBeacon('/api/analytics', body);
} onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics); COMMAND_BLOCK:
<!-- In <head> — tell the browser about the hero image early -->
<link rel="preload" as="image" href="/hero-image.webp" fetchpriority="high" /> Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
<!-- In <head> — tell the browser about the hero image early -->
<link rel="preload" as="image" href="/hero-image.webp" fetchpriority="high" /> COMMAND_BLOCK:
<!-- In <head> — tell the browser about the hero image early -->
<link rel="preload" as="image" href="/hero-image.webp" fetchpriority="high" /> CODE_BLOCK:
<img src="/hero-800.webp" srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w" sizes="(max-width: 768px) 100vw, 800px" alt="Hero image" width="800" height="400" fetchpriority="high" decoding="async"
/> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
<img src="/hero-800.webp" srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w" sizes="(max-width: 768px) 100vw, 800px" alt="Hero image" width="800" height="400" fetchpriority="high" decoding="async"
/> CODE_BLOCK:
<img src="/hero-800.webp" srcset="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1200.webp 1200w" sizes="(max-width: 768px) 100vw, 800px" alt="Hero image" width="800" height="400" fetchpriority="high" decoding="async"
/> COMMAND_BLOCK:
// SvelteKit example: cache expensive data
export const load: PageServerLoad = async ({ setHeaders }) => { setHeaders({ 'Cache-Control': 'public, max-age=3600, s-maxage=86400', }); const data = await fetchExpensiveData(); return { data };
}; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
// SvelteKit example: cache expensive data
export const load: PageServerLoad = async ({ setHeaders }) => { setHeaders({ 'Cache-Control': 'public, max-age=3600, s-maxage=86400', }); const data = await fetchExpensiveData(); return { data };
}; COMMAND_BLOCK:
// SvelteKit example: cache expensive data
export const load: PageServerLoad = async ({ setHeaders }) => { setHeaders({ 'Cache-Control': 'public, max-age=3600, s-maxage=86400', }); const data = await fetchExpensiveData(); return { data };
}; CODE_BLOCK:
// vite.config.ts
import critters from 'critters-webpack-plugin'; // This inlines above-the-fold CSS and defers the rest Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// vite.config.ts
import critters from 'critters-webpack-plugin'; // This inlines above-the-fold CSS and defers the rest CODE_BLOCK:
// vite.config.ts
import critters from 'critters-webpack-plugin'; // This inlines above-the-fold CSS and defers the rest COMMAND_BLOCK:
// Before: one long synchronous operation
function processLargeDataset(items) { items.forEach(item => heavyTransform(item)); // Blocks for 300ms
} // After: yield to the main thread
async function processLargeDataset(items) { const chunks = chunkArray(items, 50); for (const chunk of chunks) { chunk.forEach(item => heavyTransform(item)); await scheduler.yield(); // Let the browser handle pending interactions }
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
// Before: one long synchronous operation
function processLargeDataset(items) { items.forEach(item => heavyTransform(item)); // Blocks for 300ms
} // After: yield to the main thread
async function processLargeDataset(items) { const chunks = chunkArray(items, 50); for (const chunk of chunks) { chunk.forEach(item => heavyTransform(item)); await scheduler.yield(); // Let the browser handle pending interactions }
} COMMAND_BLOCK:
// Before: one long synchronous operation
function processLargeDataset(items) { items.forEach(item => heavyTransform(item)); // Blocks for 300ms
} // After: yield to the main thread
async function processLargeDataset(items) { const chunks = chunkArray(items, 50); for (const chunk of chunks) { chunk.forEach(item => heavyTransform(item)); await scheduler.yield(); // Let the browser handle pending interactions }
} COMMAND_BLOCK:
import { startTransition } from 'react'; function SearchComponent() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); function handleChange(e) { setQuery(e.target.value); // Urgent: update input immediately startTransition(() => { setResults(filterResults(e.target.value)); // Non-urgent: can be deferred }); }
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
import { startTransition } from 'react'; function SearchComponent() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); function handleChange(e) { setQuery(e.target.value); // Urgent: update input immediately startTransition(() => { setResults(filterResults(e.target.value)); // Non-urgent: can be deferred }); }
} COMMAND_BLOCK:
import { startTransition } from 'react'; function SearchComponent() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); function handleChange(e) { setQuery(e.target.value); // Urgent: update input immediately startTransition(() => { setResults(filterResults(e.target.value)); // Non-urgent: can be deferred }); }
} COMMAND_BLOCK:
function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): T { let timer: ReturnType<typeof setTimeout>; return ((...args: Parameters<T>) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }) as T;
} // Usage
input.addEventListener('input', debounce(handleSearch, 200)); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): T { let timer: ReturnType<typeof setTimeout>; return ((...args: Parameters<T>) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }) as T;
} // Usage
input.addEventListener('input', debounce(handleSearch, 200)); COMMAND_BLOCK:
function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): T { let timer: ReturnType<typeof setTimeout>; return ((...args: Parameters<T>) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }) as T;
} // Usage
input.addEventListener('input', debounce(handleSearch, 200)); CODE_BLOCK:
<!-- Bad: causes layout shift when image loads -->
<img src="/photo.webp" alt="Photo" /> <!-- Good: browser reserves space -->
<img src="/photo.webp" alt="Photo" width="800" height="600" /> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
<!-- Bad: causes layout shift when image loads -->
<img src="/photo.webp" alt="Photo" /> <!-- Good: browser reserves space -->
<img src="/photo.webp" alt="Photo" width="800" height="600" /> CODE_BLOCK:
<!-- Bad: causes layout shift when image loads -->
<img src="/photo.webp" alt="Photo" /> <!-- Good: browser reserves space -->
<img src="/photo.webp" alt="Photo" width="800" height="600" /> CODE_BLOCK:
.video-container { aspect-ratio: 16 / 9; width: 100%; background: #1a1a1a;
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
.video-container { aspect-ratio: 16 / 9; width: 100%; background: #1a1a1a;
} CODE_BLOCK:
.video-container { aspect-ratio: 16 / 9; width: 100%; background: #1a1a1a;
} CODE_BLOCK:
/* Reserve space for an ad slot or dynamic banner */
.ad-slot { min-height: 250px; contain: layout;
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
/* Reserve space for an ad slot or dynamic banner */
.ad-slot { min-height: 250px; contain: layout;
} CODE_BLOCK:
/* Reserve space for an ad slot or dynamic banner */
.ad-slot { min-height: 250px; contain: layout;
} CODE_BLOCK:
/* Pin dynamic banners to the top of the viewport */
.notification-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 50;
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
/* Pin dynamic banners to the top of the viewport */
.notification-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 50;
} CODE_BLOCK:
/* Pin dynamic banners to the top of the viewport */
.notification-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 50;
} - Measure in the field using the web-vitals library, not just Lighthouse
- LCP: preload hero images and optimize server response time
- INP: break long tasks, debounce handlers, use startTransition
- CLS: always set image dimensions and reserve space for dynamic content
- Small, targeted fixes often deliver the biggest improvements
- Test on real devices — your development machine isn't representative
how-totutorialguidedev.toaiserverssl