Web Performance

Web performance directly impacts user experience, conversion rates, and search rankings. Google's Core Web Vitals are now established ranking signals, and users abandon pages that feel slow. This page covers measurement, optimization, and monitoring strategies with hands-on code.

Hub: Graphics & Web Design Related: Image Optimization | Web Fonts

Core Web Vitals

The three Core Web Vitals measure loading, interactivity, and visual stability:

Metric Target What It Measures
LCP (Largest Contentful Paint) < 2.5s Time until the largest visible element renders
INP (Interaction to Next Paint) < 200ms Latency of the slowest interaction
CLS (Cumulative Layout Shift) < 0.1 Sum of unexpected layout shifts

Measuring in the Field

// Using the web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,    // "good", "needs-improvement", or "poor"
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
  });
  // Use sendBeacon so data is sent even on page unload
  navigator.sendBeacon('/api/vitals', body);
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

Lighthouse Audits

Lighthouse provides lab-based performance scoring. Run it in CI to catch regressions before they reach production:

# CLI audit with performance budget
npx lighthouse https://example.com \
  --only-categories=performance \
  --budget-path=budget.json \
  --output=html \
  --output-path=./lighthouse-report.html

Performance budget file:

[
  {
    "resourceSizes": [
      { "resourceType": "script", "budget": 150 },
      { "resourceType": "image", "budget": 300 },
      { "resourceType": "font", "budget": 100 },
      { "resourceType": "total", "budget": 600 }
    ],
    "resourceCounts": [
      { "resourceType": "third-party", "budget": 5 }
    ],
    "timings": [
      { "metric": "largest-contentful-paint", "budget": 2500 },
      { "metric": "cumulative-layout-shift", "budget": 0.1 },
      { "metric": "interactive", "budget": 3500 }
    ]
  }
]

The Performance API

The Performance API provides precise timing data for custom measurements:

// Mark the start and end of an operation
performance.mark('data-fetch-start');

const data = await fetch('/api/products').then(r => r.json());

performance.mark('data-fetch-end');
performance.measure('data-fetch', 'data-fetch-start', 'data-fetch-end');

// Read the measurement
const [measure] = performance.getEntriesByName('data-fetch');
console.log(`Data fetch took ${measure.duration.toFixed(1)}ms`);

// Monitor long tasks
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.warn(`Long task detected: ${entry.duration.toFixed(0)}ms`);
    // Send to monitoring service
  }
});
observer.observe({ type: 'longtask', buffered: true });

// Navigation timing
window.addEventListener('load', () => {
  const nav = performance.getEntriesByType('navigation')[0];
  console.log(`DNS:      ${nav.domainLookupEnd - nav.domainLookupStart}ms`);
  console.log(`TCP:      ${nav.connectEnd - nav.connectStart}ms`);
  console.log(`TTFB:     ${nav.responseStart - nav.requestStart}ms`);
  console.log(`DOM Load: ${nav.domContentLoadedEventEnd - nav.startTime}ms`);
});

Lazy Loading Images and Iframes

Defer off-screen resources to reduce initial page weight:

<!-- Native lazy loading for images -->
<img src="/img/below-fold.webp"
     alt="Product gallery image"
     loading="lazy"
     decoding="async"
     width="800" height="600" />

<!-- Lazy-load iframes (YouTube embeds, maps) -->
<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ"
        loading="lazy"
        width="560" height="315"
        title="Product demo video"
        allow="accelerometer; autoplay; encrypted-media; gyroscope"
        allowfullscreen></iframe>

Do not lazy-load the LCP element. Identify it with Lighthouse or DevTools and add fetchpriority="high" instead:

<img src="/img/hero.webp" alt="Hero banner"
     fetchpriority="high" decoding="async"
     width="1200" height="600" />

Code Splitting with Dynamic import()

Reduce the initial JavaScript bundle by splitting code at route or feature boundaries:

// React lazy loading
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings  = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<div className="spinner">Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

// Vanilla JS: load a module on interaction
document.getElementById('open-editor').addEventListener('click', async () => {
  const { initEditor } = await import('./editor.js');
  initEditor(document.getElementById('editor-container'));
});

Webpack, Vite, and Rollup all recognize dynamic import() as a split point and generate separate chunks automatically.

Resource Hints

Resource hints tell the browser about resources it will need soon:

<head>
  <!-- Preconnect: establish early connection to critical origins -->
  <link rel="preconnect" href="https://cdn.example.com" />
  <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />

  <!-- Preload: fetch critical resources with high priority -->
  <link rel="preload" href="/fonts/Inter-var.woff2"
        as="font" type="font/woff2" crossorigin />
  <link rel="preload" href="/css/critical.css" as="style" />
  <link rel="preload" href="/img/hero.avif" as="image" type="image/avif" />

  <!-- Prefetch: fetch resources needed for likely next navigation -->
  <link rel="prefetch" href="/js/dashboard-chunk.js" />
  <link rel="prefetch" href="/api/user/preferences" as="fetch" />

  <!-- DNS Prefetch: resolve DNS for third-party domains -->
  <link rel="dns-prefetch" href="https://analytics.example.com" />
</head>
Hint When to Use
preconnect Third-party origins used on the current page
preload Critical resources discovered late (fonts, above-fold images)
prefetch Resources for the next likely navigation
dns-prefetch Third-party origins that may be needed

Font Optimization Recap

Fonts frequently cause render-blocking and layout shift. Key strategies:

<!-- Preload the primary font -->
<link rel="preload" href="/fonts/Inter-var.woff2"
      as="font" type="font/woff2" crossorigin />
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-var.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: swap;
}

/* Fallback font with matched metrics to minimize CLS */
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107%;
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

body {
  font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif;
}

Subset fonts to Latin-only (or your required character set) using pyftsubset to cut file sizes by 70-90 percent.

Image Optimization Recap

Images are the largest LCP candidates. Ensure every image is:

  1. Served in a modern format (AVIF > WebP > JPEG) via <picture>.
  2. Sized responsively with srcset and sizes.
  3. Compressed to an appropriate quality level (80 for WebP, 50 for AVIF).
  4. Lazy-loaded if below the fold, eagerly loaded with fetchpriority="high" if it is the LCP element.
  5. Served from a CDN with immutable cache headers.

Render-Blocking Resources

CSS and synchronous JavaScript in the <head> block rendering. Strategies to mitigate:

<!-- Inline critical CSS for above-the-fold content -->
<style>
  /* Critical CSS inlined here -- generated by tools like critters */
  .hero { min-height: 100dvh; display: grid; place-items: center; }
</style>

<!-- Defer non-critical CSS -->
<link rel="stylesheet" href="/css/main.css" media="print"
      onload="this.media='all'" />
<noscript><link rel="stylesheet" href="/css/main.css" /></noscript>

<!-- Defer JavaScript -->
<script src="/js/app.js" defer></script>

<!-- Module scripts are deferred by default -->
<script type="module" src="/js/app.mjs"></script>

The defer attribute downloads the script in parallel with HTML parsing and executes it after parsing completes. async downloads in parallel but executes immediately upon completion, which can block parsing. Use defer for scripts that depend on the DOM; use async for independent analytics scripts.

Performance Checklist

  • Inline critical CSS, defer the rest.
  • Use defer or type="module" for scripts.
  • Preload LCP image and primary font.
  • Lazy-load below-fold images and iframes.
  • Code-split JavaScript at route boundaries.
  • Compress images to modern formats.
  • Subset and self-host fonts with font-display: swap.
  • Set performance budgets and enforce them in CI.
  • Monitor Core Web Vitals in the field with the web-vitals library.

Performance is not a one-time fix. It is a continuous practice of measuring, optimizing, and monitoring. Every spoke in this hub -- SVG, images, CSS, fonts, responsive design, and accessibility -- feeds into the performance story. Treat this page as the integration point where all those optimizations converge.