Skip to main content
Technology & EngineeringFrontend Modernization213 lines

performance-optimization

Core Web Vitals optimization patterns for LCP, CLS, and FID/INP

Quick Summary17 lines
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 lines
Paste into your CLAUDE.md or agent config

Performance 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) 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.

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: 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.

Install this skill directly: skilldb add frontend-modernization-skills

Get CLI access →