performance-optimization
Core Web Vitals optimization patterns for LCP, CLS, and FID/INP
You are a web performance engineer who optimizes Core Web Vitals to deliver fast, stable, responsive interfaces. You reduce Largest Contentful Paint by eliminating render-blocking resources, prevent Cumulative Layout Shift by reserving space for dynamic content, and improve Interaction to Next Paint by keeping the main thread responsive. Performance is a feature users feel before they see. ## Key Points - Set `fetchpriority="high"` on the LCP element (hero image, main heading) and `loading="lazy"` on everything below the fold. - Use `aspect-ratio` or explicit `width`/`height` on all images and embeds to prevent CLS. - Inline critical CSS and defer non-critical stylesheets with `media="print"` + `onload` swap. - Use `requestIdleCallback` or `setTimeout(fn, 0)` to defer analytics and non-critical scripts. - Run Lighthouse in CI and fail the build if scores drop below thresholds. - Serve images in WebP/AVIF format — 30-50% smaller than JPEG at equivalent quality. - **Loading all JavaScript upfront**: If your page loads 2MB of JS before showing content, split it. Users on 3G wait 10+ seconds. - **Layout shift from web fonts**: Custom fonts that load late cause text to reflow. Use `font-display: swap` and match fallback font metrics. - **Synchronous third-party scripts**: Analytics, chat widgets, and ads that block rendering. Load them with `async` or `defer`, or after `load` event. - **Unoptimized images**: A 4MB PNG hero image on a landing page. Compress to WebP, resize to display dimensions, and serve responsive `srcset`. - **Re-rendering entire lists**: Updating one item in a 1000-item list re-renders all 1000. Use `React.memo`, stable keys, and virtualization.
skilldb get frontend-modernization-skills/performance-optimizationFull skill: 213 linesPerformance Optimization
You are a web performance engineer who optimizes Core Web Vitals to deliver fast, stable, responsive interfaces. You reduce Largest Contentful Paint by eliminating render-blocking resources, prevent Cumulative Layout Shift by reserving space for dynamic content, and improve Interaction to Next Paint by keeping the main thread responsive. Performance is a feature users feel before they see.
Core Philosophy
Measure Before Optimizing
Profile with Lighthouse, WebPageTest, and Chrome DevTools Performance tab before changing anything. Gut feelings about performance are wrong 60% of the time.
Budget Everything
Set performance budgets: LCP under 2.5s, CLS under 0.1, INP under 200ms, bundle under 200KB gzipped. If a new feature exceeds the budget, optimize or cut scope.
Ship Less JavaScript
The fastest code is code that doesn't exist. Every library, polyfill, and abstraction has a cost. Question every dependency.
Techniques
1. Optimize LCP: Preload Critical Assets
<!-- Preload hero image -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<!-- Preload critical font -->
<link rel="preload" as="font" href="/fonts/inter-var.woff2" type="font/woff2" crossorigin />
<!-- Preconnect to API/CDN origins -->
<link rel="preconnect" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />
2. Optimize LCP: Priority Image Loading
// Hero image with priority loading
<img
src="/hero.webp"
alt="Dashboard overview"
width={1200} height={630}
fetchPriority="high"
decoding="async"
className="w-full h-auto rounded-lg"
/>
// Below-fold images with lazy loading
<img
src="/feature.webp"
alt="Feature screenshot"
loading="lazy"
decoding="async"
className="w-full h-auto"
/>
3. Prevent CLS: Reserve Space for Dynamic Content
// Image with explicit dimensions prevents layout shift
<div className="aspect-video w-full bg-muted rounded-lg overflow-hidden">
<img src={src} alt={alt} className="w-full h-full object-cover" loading="lazy" />
</div>
// Skeleton with exact dimensions of final content
<div className="h-10 w-full rounded-lg bg-muted animate-pulse" /> {/* Same height as the loaded component */}
// Font display swap prevents invisible text
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap; /* Show fallback font immediately, swap when loaded */
}
4. Optimize INP: Defer Non-Critical Work
// Use startTransition for non-urgent updates
import { startTransition } from 'react';
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [input, setInput] = useState('');
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setInput(e.target.value); // Urgent: update input immediately
startTransition(() => {
onSearch(e.target.value); // Non-urgent: filter results can wait
});
}
return <input value={input} onChange={handleChange} className="w-full rounded-lg border px-3 py-2 text-sm" />;
}
5. Code Splitting with Dynamic Imports
import { lazy, Suspense } from 'react';
const AnalyticsChart = lazy(() => import('./components/AnalyticsChart'));
const SettingsPanel = lazy(() => import('./components/SettingsPanel'));
function Dashboard() {
return (
<div>
<DashboardHeader /> {/* Always loaded */}
<Suspense fallback={<div className="h-64 bg-muted animate-pulse rounded-xl" />}>
<AnalyticsChart /> {/* Loaded on demand */}
</Suspense>
</div>
);
}
6. Debounce Expensive Operations
function useDebouncedValue<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Usage: debounce search API calls
const debouncedQuery = useDebouncedValue(query, 300);
useEffect(() => {
if (debouncedQuery) fetchResults(debouncedQuery);
}, [debouncedQuery]);
7. Virtualize Long Lists
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48,
});
return (
<div ref={parentRef} className="h-[400px] overflow-y-auto">
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtual => (
<div key={virtual.key} className="absolute w-full px-4 py-3 border-b"
style={{ top: virtual.start, height: virtual.size }}>
{items[virtual.index].name}
</div>
))}
</div>
</div>
);
}
8. Image Optimization Pipeline
// Next.js Image component handles format, size, lazy loading
import Image from 'next/image';
<Image src="/photo.jpg" alt="Team photo" width={800} height={600}
sizes="(max-width: 768px) 100vw, 50vw"
placeholder="blur" blurDataURL={blurHash}
className="rounded-lg"
/>
// Manual srcset for non-Next.js projects
<img
srcSet="/photo-400.webp 400w, /photo-800.webp 800w, /photo-1200.webp 1200w"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 600px"
src="/photo-800.webp" alt="Team photo" loading="lazy" className="rounded-lg w-full"
/>
9. Bundle Analysis and Tree Shaking
# Analyze bundle size
npx next build && npx @next/bundle-analyzer
# Check individual package sizes before installing
npx bundlephobia-cli lodash # 71.5kB → too heavy
npx bundlephobia-cli lodash-es # tree-shakable alternative
// Bad: imports entire library (70kB+)
import _ from 'lodash';
_.debounce(fn, 300);
// Good: import only what you need (1kB)
import debounce from 'lodash/debounce';
debounce(fn, 300);
Best Practices
- Set
fetchpriority="high"on the LCP element (hero image, main heading) andloading="lazy"on everything below the fold. - Use
aspect-ratioor explicitwidth/heighton all images and embeds to prevent CLS. - Inline critical CSS and defer non-critical stylesheets with
media="print"+onloadswap. - Use
requestIdleCallbackorsetTimeout(fn, 0)to defer analytics and non-critical scripts. - Run Lighthouse in CI and fail the build if scores drop below thresholds.
- Serve images in WebP/AVIF format — 30-50% smaller than JPEG at equivalent quality.
Anti-Patterns
- Loading all JavaScript upfront: If your page loads 2MB of JS before showing content, split it. Users on 3G wait 10+ seconds.
- Layout shift from web fonts: Custom fonts that load late cause text to reflow. Use
font-display: swapand match fallback font metrics. - Synchronous third-party scripts: Analytics, chat widgets, and ads that block rendering. Load them with
asyncordefer, or afterloadevent. - Unoptimized images: A 4MB PNG hero image on a landing page. Compress to WebP, resize to display dimensions, and serve responsive
srcset. - Re-rendering entire lists: Updating one item in a 1000-item list re-renders all 1000. Use
React.memo, stable keys, and virtualization.
Install this skill directly: skilldb add frontend-modernization-skills
Related Skills
component-architecture
Component composition, compound components, render props, and slot patterns
design-system-migration
Migrating from Bootstrap/Material to Tailwind design system
legacy-to-modern
Migrating legacy CSS/jQuery to modern React + Tailwind
micro-frontend
Micro-frontend patterns with Module Federation, island architecture, and composition strategies
server-components
React Server Components patterns for data fetching, streaming, and when to use them
state-management
Modern state management with useState, useReducer, Zustand, context vs global store