Skip to content
StackPractices
intermediate By StackPractices

Web Performance Optimization

Improve Core Web Vitals, reduce bundle sizes, and optimize frontend performance with lazy loading, code splitting, and modern build tools.

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Overview

Web performance directly impacts user engagement, conversion rates, and search rankings. Google’s Core Web Vitals — Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS) — provide measurable targets. This resource covers practical techniques: lazy loading, code splitting, image optimization, critical CSS, and modern build tooling to hit sub-3-second page loads.

When to Use

Use this resource when:

  • Core Web Vitals scores are failing (LCP > 2.5s, CLS > 0.1)
  • Mobile users on 3G networks abandon pages before they load
  • Bundle sizes exceed 200KB and impact time-to-interactive
  • Third-party scripts (analytics, ads) block the main thread

Solution

Critical CSS Inline + Async Load (HTML)

<head>
  <!-- Inline critical CSS (~14KB max) -->
  <style>
    /* Above-fold styles: header, hero, layout skeleton */
    body{margin:0;font-family:system-ui}
    .hero{background:#3b82f6;min-height:60vh}
  </style>

  <!-- Preload key resources -->
  <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="/hero-image.webp" as="image" fetchpriority="high">

  <!-- Async load non-critical CSS -->
  <link rel="preload" href="/styles.css" as="style" onload="this.rel='stylesheet'">
</head>

Lazy Loading Images with Native API

<!-- Native lazy loading — no JavaScript required -->
<img src="hero.webp" alt="Hero" fetchpriority="high" width="800" height="400">
<img src="below-fold-1.webp" alt="Product" loading="lazy" width="400" height="300">
<img src="below-fold-2.webp" alt="Team" loading="lazy" width="400" height="300">

Code Splitting with Live Imports (React)

import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));
const AnalyticsDashboard = lazy(() => import('./AnalyticsDashboard'));

function Dashboard() {
  return (
    <div>
      <CriticalStats /> {/* Always loaded */}
      <Suspense fallback={<Spinner />}>
        <HeavyChart /> {/* Loaded on demand */}
      </Suspense>
      <Suspense fallback={<Spinner />}>
        <AnalyticsDashboard /> {/* Separate chunk */}
      </Suspense>
    </div>
  );
}

Explanation

Core Web Vitals targets:

MetricGoodPoorMeasures
LCP< 2.5s> 4sLargest visible element load time
INP< 200ms> 500msInteraction responsiveness
CLS< 0.1> 0.25Visual stability (layout shifts)
TTFB< 600ms> 1.8sTime to first byte

Performance budget example:

  • JavaScript: 150KB (gzipped)
  • Images: 250KB total
  • CSS: 50KB (including critical inline)
  • Fonts: 40KB (subsetted)
  • Third-party: 100KB max

Variants

TechniqueImpactEffort
Image optimization (WebP/AVIF)-50% image bytesLow
Font subsetting-80% font bytesLow
Code splitting-60% initial JSMedium
Edge caching-90% TTFBLow
Service WorkerInstant repeat visitsMedium
HTTP/3 + QUICFaster on lossy networksLow (CDN)

What Works

  • Measure real users, not lab tests: Field data from Chrome UX Report reflects actual conditions
  • Optimize the critical path: Anything blocking <head> should be under 50KB total. See server-side rendering.
  • Self-host fonts and analytics: Third-party connections add DNS + TLS + TCP overhead
  • Use content-visibility: auto: Browsers skip rendering off-screen content
  • Defer non-critical JavaScript: defer or type="module" for scripts that aren’t needed immediately

Common Mistakes

  1. Oversized hero images: A 4MB PNG hero destroys LCP; use responsive images with srcset
  2. Render-blocking third parties: Google Fonts loaded synchronously delays first paint
  3. No resource hints: preload, prefetch, and preconnect are free performance wins
  4. Hydrating everything: Islands architecture (Astro, Fresh) ships zero JS for static content
  5. Ignoring mobile: 70% of users are on mobile; test on real devices, not just DevTools

Frequently Asked Questions

Q: What’s the single biggest performance win? A: Image optimization. Images are typically 60-80% of page weight. Use modern formats, responsive sizing, and lazy loading.

Q: Should I use a CDN? A: Yes. A CDN reduces TTFB by serving from edge locations close to users. Essential for global audiences.

Q: How do I balance performance with developer experience? A: Use frameworks that optimize by default (Astro, SvelteKit, Next.js with App Router). Don’t fight the tooling.

How do I measure Core Web Vitals in production?

Use the web-vitals JavaScript library to collect real user metrics: import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals';\nonLCP((metric) => sendToAnalytics('LCP', metric.value));\nonINP((metric) => sendToAnalytics('INP', metric.value));\nonCLS((metric) => sendToAnalytics('CLS', metric.value));. Send data to your analytics backend: function sendToAnalytics(name, value) {\n navigator.sendBeacon('/api/vitals', JSON.stringify({ name, value, page: location.pathname }));\n}. Use Google Search Console’s Core Web Vitals report for field data across your site. Set up alerts for regressions: if LCP p75 exceeds 2.5s, trigger an alert. Use Lighthouse CI in your pipeline: lighthouse-ci --assertions.lcp=2.5 --assertions.cls=0.1 --assertions.inp=200. Collect metrics per page template, not just site-wide averages. Segment by device type (mobile, desktop, tablet) and connection type (4G, 3G, WiFi). Use PerformanceObserver for custom metrics: new PerformanceObserver((list) => {\n for (const entry of list.getEntries()) {\n console.log(${entry.name}: ${entry.startTime}ms);\n }\n}).observe({ entryTypes: ['paint', 'largest-contentful-paint'] });.

How do I optimize JavaScript bundle sizes?

Analyze your bundle with webpack-bundle-analyzer or rollup-plugin-visualizer: import { visualizer } from 'rollup-plugin-visualizer';\nexport default {\n plugins: [visualizer({ open: true, filename: 'bundle-stats.html' })]\n};. Identify large dependencies and replace them with lighter alternatives: moment.js (280KB) → date-fns (20KB), lodash (70KB) → lodash-es with tree shaking. Use tree shaking: import { debounce } from 'lodash-es'; instead of import _ from 'lodash';. Enable gzip and brotli compression on your server: gzip on;\ngzip_types text/css application/javascript; in nginx. Code-split by route: const About = lazy(() => import('./About')); to reduce initial bundle. Use dynamic imports for conditional features: if (supportsWebGL) {\n const { render3D } = await import('./3d-renderer');\n render3D();\n}. Audit third-party packages: npm ls --production and remove unused dependencies. Set bundle size limits in CI: maxSize: '150KB' to fail builds that exceed budgets. Use import-cost VS Code extension to see import sizes during development. Consider module federation for micro-frontends to share dependencies across apps.

How do I optimize font loading?

Use font-display: swap to avoid invisible text: @font-face {\n font-family: 'Inter';\n src: url('/fonts/inter.woff2') format('woff2');\n font-display: swap;\n}. Preload critical fonts: <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>. Subset fonts to include only used characters: pyftsubset inter.ttf --text="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" --output-file=inter-subset.woff2. Use variable fonts to reduce file count: one variable font file replaces multiple weight files. Self-host fonts instead of using Google Fonts CDN to avoid third-party connection overhead. Use size-adjust in @font-face to match fallback font metrics: @font-face {\n font-family: 'Inter-fallback';\n src: local('Arial');\n size-adjust: 100%;\n}. Monitor font loading: document.fonts.ready.then(() => {\n console.log('All fonts loaded');\n});. Use unicode-range to split fonts by script: @font-face {\n unicode-range: U+0000-00FF;\n src: url('/fonts/inter-latin.woff2');\n}.

How do I optimize images for web performance?

Use modern formats: WebP (30% smaller than JPEG) and AVIF (50% smaller than JPEG). Serve responsive images with srcset: <img\n src="hero-800.webp"\n srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"\n sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"\n alt="Hero"\n fetchpriority="high"\n>. Use <picture> for format negotiation: <picture>\n <source type="image/avif" srcset="hero.avif">\n <source type="image/webp" srcset="hero.webp">\n <img src="hero.jpg" alt="Hero">\n</picture>. Compress images: cwebp -q 80 input.jpg -o output.webp for lossy, cwebp -lossless input.png -o output.webp for lossless. Use blur placeholders for above-fold images: generate a tiny blurred version (20px wide) and scale it up with CSS filter: blur(20px) until the full image loads. Lazy-load below-fold images: <img loading="lazy" src="...">. Set explicit width and height to prevent CLS. Use CDN image transformation: https://cdn.example.com/image.jpg?w=800&format=webp to serve optimized versions on the fly. Avoid using images for text — use CSS instead. Use SVG for icons and logos: <img src="logo.svg" alt="Logo">.

How do I reduce layout shifts (CLS)?

Reserve space for images, ads, and embeds: <img width="800" height="400" src="..."> prevents the browser from reflowing when the image loads. Use CSS aspect-ratio: .video-container {\n aspect-ratio: 16 / 9;\n}. Avoid injecting content above existing content: banner ads should reserve space before loading. Use min-height for dynamic content areas: .comments {\n min-height: 200px;\n}. Preconnect to third-party origins: <link rel="preconnect" href="https://cdn.example.com"> to avoid late resource discovery. Use font-display: optional for non-critical fonts to prevent font-swap layout shifts. Avoid display: none toggling on above-fold content. Use transform and opacity for animations instead of top, left, width, height — these don’t trigger layout. Set explicit dimensions on iframes: <iframe width="560" height="315" src="...">. Use content-visibility: auto with contain-intrinsic-size: .card {\n content-visibility: auto;\n contain-intrinsic-size: 200px;\n}.

How do I improve Interaction to Next Paint (INP)?

INP measures responsiveness to user interactions. Break long tasks: function processItems(items) {\n // Process in chunks of 50ms\n const chunk = items.slice(0, 50);\n // ... process chunk\n if (items.length > 50) {\n setTimeout(() => processItems(items.slice(50)), 0);\n }\n}. Use requestIdleCallback for non-urgent work: requestIdleCallback(() => {\n // Analytics, reporting, etc.\n});. Debounce scroll and resize handlers: const handleResize = debounce(() => {\n // Expensive layout calculation\n}, 150);\nwindow.addEventListener('resize', handleResize);. Use scheduler.yield() when available: async function processQueue() {\n for (const item of queue) {\n processItem(item);\n await scheduler.yield(); // Yield to main thread\n }\n}. Avoid synchronous layout reads: // Bad: forces layout\nfor (let i = 0; i < items.length; i++) {\n items[i].style.left = ${items[i].offsetLeft + 10}px;\n}\n// Good: batch reads and writes\nconst positions = items.map(item => item.offsetLeft);\nitems.forEach((item, i) => {\n item.style.left = ${positions[i] + 10}px;\n});. Use Web Workers for CPU-intensive tasks: const worker = new Worker('compute.js');\nworker.postMessage(data);\nworker.onmessage = (e) => updateUI(e.data);. Minimize third-party JavaScript that blocks the main thread. Use requestAnimationFrame for visual updates: function animate() {\n // Update DOM\n requestAnimationFrame(animate);\n}.

How do I use resource hints effectively?

Use preload for critical resources on the current page: <link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin>. Use prefetch for resources needed on the next page: <link rel="prefetch" href="/next-page.js">. Use preconnect to establish early connections: <link rel="preconnect" href="https://api.example.com">. Use dns-prefetch for DNS-only optimization: <link rel="dns-prefetch" href="//cdn.example.com">. Use modulepreload for JavaScript modules: <link rel="modulepreload" href="/app.js">. Prioritize with fetchpriority: <img src="hero.webp" fetchpriority="high"> for above-fold, <img src="below.webp" fetchpriority="low"> for below-fold. Avoid overusing preload — each hint consumes bandwidth. Test with Network tab in DevTools to verify hints are working. Use Speculation Rules API for predictive prefetching: <script type="speculationrules">\n{ "prefetch": [{ "source": "list", "urls": ["/about", "/contact"] }] }\n</script>.

How do I implement a Service Worker for caching?

Register the Service Worker: if ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('/sw.js');\n}. Cache static assets with a cache-first strategy: const CACHE = 'static-v1';\nconst ASSETS = ['/index.html', '/styles.css', '/app.js'];\nself.addEventListener('install', (e) => {\n e.waitUntil(caches.open(CACHE).then(cache => cache.addAll(ASSETS)));\n});\nself.addEventListener('fetch', (e) => {\n e.respondWith(\n caches.match(e.request).then(response => response || fetch(e.request))\n );\n});. Use network-first for dynamic content: self.addEventListener('fetch', (e) => {\n e.respondWith(\n fetch(e.request).catch(() => caches.match(e.request))\n );\n});. Use stale-while-revalidate for API responses: self.addEventListener('fetch', (e) => {\n e.respondWith(\n caches.open('api-cache').then(cache =>\n cache.match(e.request).then(cached => {\n const fetchPromise = fetch(e.request).then(response => {\n cache.put(e.request, response.clone());\n return response;\n });\n return cached || fetchPromise;\n })\n )\n );\n});. Clean up old caches: self.addEventListener('activate', (e) => {\n e.waitUntil(\n caches.keys().then(keys =>\n Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))\n )\n );\n});. Use Workbox for easier Service Worker management: import { registerRoute } from 'workbox-routing';\nimport { CacheFirst } from 'workbox-strategies';\nregisterRoute(/\.(?:css|js)$/, new CacheFirst());.

How do I optimize third-party scripts?

Load third-party scripts asynchronously: <script src="https://analytics.example.com/js" async></script>. Defer non-critical scripts: <script src="https://widget.example.com/js" defer></script>. Use loading="lazy" for iframes: <iframe src="https://widget.example.com" loading="lazy">. Self-host third-party scripts when possible to avoid additional DNS lookups. Use the Partytown library to run third-party scripts in a Web Worker: <script type="text/partytown" src="https://analytics.example.com/js"></script>. Audit third-party impact with Lighthouse: check the “Reduce third-party usage” audit. Set a timeout for third-party scripts: const script = document.createElement('script');\nscript.src = 'https://widget.example.com/js';\nscript.async = true;\nsetTimeout(() => {\n if (!window.widgetLoaded) {\n script.remove(); // Remove if not loaded in 3s\n }\n}, 3000);. Use resource hints for third-party domains: <link rel="preconnect" href="https://analytics.example.com">. Monitor third-party script execution time with Performance Observer: new PerformanceObserver((list) => {\n for (const entry of list.getEntries()) {\n if (entry.name.includes('third-party.com')) {\n console.log(Third-party: ${entry.name} took ${entry.duration}ms);\n }\n }\n}).observe({ entryTypes: ['resource'] });.

How do I set up performance budgets?

Define budgets in webpack.config.js: performance: {\n hints: 'warning',\n maxAssetSize: 150000, // 150KB\n maxEntrypointSize: 200000 // 200KB\n}. Use Lighthouse CI budgets: // lighthouserc.js\nmodule.exports = {\n ci: {\n assert: {\n assertions: {\n 'resource-summary:script:size': ['<', 150000],\n 'resource-summary:stylesheet:size': ['<', 50000],\n 'resource-summary:image:size': ['<', 250000]\n }\n }\n }\n};. Use size-limit for library bundles: // package.json\n"size-limit": [\n { "path": "dist/index.js", "limit": "10KB" }\n]. Monitor budgets in CI: npx size-limit fails the build if exceeded. Track budgets over time with Bundlephobia or Bundle Analyzer. Set budgets per route, not just site-wide. Include third-party scripts in budgets. Review budgets quarterly and adjust as needed.

How do I optimize CSS delivery?

Inline critical CSS in <head>: <style>/* above-fold styles */</style>. Load non-critical CSS asynchronously: <link rel="preload" href="/styles.css" as="style" onload="this.rel='stylesheet'">. Use media attribute for conditional CSS: <link rel="stylesheet" href="print.css" media="print">. Remove unused CSS with PurgeCSS: import PurgeCSS from 'purgecss';\\nconst purgeCSSResults = await new PurgeCSS().purge({\\n content: ['**/*.html'],\\n css: ['**/*.css']\\n});. Use CSS containment: .widget {\\n contain: layout style paint;\\n} to isolate rendering. Avoid @import in CSS — it blocks rendering. Use CSS custom properties for theming instead of multiple stylesheets. Minify CSS: cssnano for PostCSS or css-minimizer-webpack-plugin for webpack. Split CSS by route: import './about.css'; in the About component. Use content-visibility: auto for below-fold sections. Avoid expensive selectors: * { } and deeply nested selectors like .container > .row > .col > .card > .title are slow. Use BEM or utility classes for flatter specificity. Prefer transform and opacity for animations — they’re compositor-only properties. Use will-change sparingly: will-change: transform hints the browser to optimize, but overuse wastes memory.

How do I optimize for mobile networks?

Test on real 3G connections: Chrome DevTools network throttling at “Slow 3G” (400ms RTT, 500KB/s). Use adaptive loading: if (navigator.connection) {\\n const effectiveType = navigator.connection.effectiveType;\\n if (effectiveType === '2g' || effectiveType === 'slow-2g') {\\n // Load low-res images, disable video autoplay\\n }\\n}. Serve smaller images on mobile: <img srcset=\"image-400.webp 400w, image-800.webp 800w\" sizes=\"(max-width: 600px) 400px, 800px\">. Reduce JavaScript on mobile: mobile CPUs are 4-10x slower than desktop. Use Save-Data header: if (navigator.connection.saveData) {\\n // Skip loading non-essential resources\\n}. Prioritize above-fold content: inline critical CSS, lazy-load below-fold images. Use HTTP/2 or HTTP/3 for multiplexing: multiple requests over a single connection. Preconnect to critical origins: <link rel="preconnect" href="https://api.example.com">. Minimize redirects: each redirect adds RTT. Use resource hints for navigation: <link rel="prerender" href="/next-page"> (deprecated, use Speculation Rules API instead). Cache aggressively with Service Workers: offline-first strategies for repeat visits. Monitor real user metrics: field data reveals actual mobile performance, not lab simulations.

How do I optimize server response time (TTFB)?

Use edge computing: Cloudflare Workers, Vercel Edge Functions, or Deno Deploy to serve content close to users. Cache at the edge: Cache-Control: public, max-age=3600 for static assets. Use CDN for dynamic content: some CDNs cache dynamic responses with short TTLs. Optimize database queries: add indexes, use connection pooling, cache frequent queries. Use SSR caching: cache rendered HTML with Cache-Control: s-maxage=600, stale-while-revalidate=60. Use incremental static regeneration (ISR): export async function getStaticProps() {\\n return {\\n props: { data },\\n revalidate: 60 // Regenerate every 60 seconds\\n };\\n}. Avoid synchronous third-party API calls in the request path: use webhooks or background jobs. Use HTTP caching headers: ETag, Last-Modified, and Cache-Control. Compress HTML: gzip on;\\ngzip_types text/html; in nginx. Use HTTP/2 push for critical resources (deprecated in Chrome, but still works in some browsers). Monitor TTFB with PerformanceObserver: new PerformanceObserver((list) => {\\n for (const entry of list.getEntries()) {\\n if (entry.entryType === 'navigation') {\\n console.log(TTFB: ${entry.responseStart - entry.requestStart}ms);\\n }\\n }\\n}).observe({ entryTypes: ['navigation'] });. Use origin shield: a CDN layer that reduces load on your origin server. Optimize TLS handshake: use TLS 1.3 (1-RTT vs 2-RTT for TLS 1.2), OCSP stapling, and session resumption.

How do I handle performance for single-page applications?

Implement route-level code splitting: const routes = [\\n { path: '/', component: () => import('./Home') },\\n { path: '/about', component: () => import('./About') }\\n];. Prefetch likely-next routes: router.afterEach((to) => {\\n if (to.path === '/') {\\n import('./About'); // Prefetch About component\\n }\\n});. Use skeleton screens instead of spinners: .skeleton {\\n background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);\\n background-size: 200% 100%;\\n animation: shimmer 1.5s infinite;\\n}. Implement virtual scrolling for long lists: import { FixedSizeList } from 'react-window';\\n<FixedSizeList height={600} itemCount={10000} itemSize={35}>\\n {Row}\\n</FixedSizeList>. Debounce search inputs: const debouncedSearch = useMemo(() => debounce(fetchResults, 300), []);. Memoize expensive computations: const sortedItems = useMemo(() => items.sort(compareFn), [items]);. Use React.lazy with error boundaries: <ErrorBoundary>\\n <Suspense fallback={<Skeleton />}>\\n <LazyComponent />\\n </Suspense>\\n</ErrorBoundary>. Avoid unnecessary re-renders: use React.memo for pure components, useCallback for event handlers. Use useDeferredValue for non-urgent updates: const deferredQuery = useDeferredValue(query);. Implement progressive hydration: hydrate above-fold components first, defer below-fold. Use startTransition for low-priority state updates: startTransition(() => {\\n setFilterValue(value);\\n});.

How do I measure and optimize Time to First Byte (TTFB)?

TTFB measures the time from navigation start to the first byte of the response. Target: under 600ms for good, under 200ms for excellent. Measure with Navigation Timing API: const timing = performance.getEntriesByType('navigation')[0];\\nconst ttfb = timing.responseStart - timing.requestStart;\\nconsole.log(TTFB: ${ttfb}ms);. Optimize DNS resolution: use DNS prefetching <link rel="dns-prefetch" href="//api.example.com">. Reduce connection time: use preconnect <link rel="preconnect" href="https://api.example.com">. Use HTTP/2 or HTTP/3: multiplexing eliminates connection overhead for multiple resources. Optimize server processing: cache database queries, use connection pooling, add Redis caching. Use a CDN: edge servers reduce physical distance to users. Enable compression: gzip or brotli for HTML responses. Use server-side caching: Cache-Control: public, max-age=300, s-maxage=600. Monitor TTFB per region: users in different geographic locations experience different TTFB. Use synthetic monitoring: run Lighthouse from multiple regions. Set up alerts: if TTFB p75 exceeds 600ms, investigate. Use origin shield: reduces load on origin by caching at CDN edge. Optimize TLS: use TLS 1.3, enable OCSP stapling, use session resumption. Reduce redirect chains: each redirect adds an RTT. Use 103 Early Hints to start loading resources before the full response is ready: Link: </styles.css>; rel=preload; as=style.

How do I use Lighthouse CI for automated performance testing?

Install Lighthouse CI: npm install -g @lhci/cli. Configure in lighthouserc.js: module.exports = {\\n ci: {\\n collect: {\\n url: ['https://example.com', 'https://example.com/about'],\\n numberOfRuns: 3,\\n settings: {\\n preset: 'desktop',\\n throttling: { rttMs: 40, throughputKbps: 10240 }\\n }\\n },\\n assert: {\\n assertions: {\\n 'categories:performance': ['warn', { minScore: 0.8 }],\\n 'first-contentful-paint': ['error', { maxNumericValue: 2000 }],\\n 'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],\\n 'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],\\n 'total-blocking-time': ['error', { maxNumericValue: 300 }]\\n }\\n },\\n upload: {\\n target: 'temporary-public-storage'\\n }\\n }\\n};. Run in CI: lhci autorun. Set up GitHub Actions: name: Lighthouse CI\\non: [pull_request]\\njobs:\\n lighthouse:\\n runs-on: ubuntu-latest\\n steps:\\n - uses: actions/checkout@v4\\n - run: npm install && npm run build\\n - run: npm install -g @lhci/cli\\n - run: lhci autorun || true\\n - uses: actions/upload-artifact@v4\\n with:\\n name: lighthouse-report\\n path: .lighthouseci/. Compare results against baseline: lhci diff. Use Lighthouse Server for historical data: lhci server --storage.storageMethod=sql. Set performance budgets per route. Test on mobile and desktop presets. Use --collect.url for multiple pages. Assert against specific metrics: LCP, CLS, TBT, FCP, TTI. Generate HTML reports: lhci report --report=html.

How do I optimize hydration in SSR frameworks?

Hydration is the process of attaching event listeners to server-rendered HTML. Use partial hydration: Astro Islands hydrate only interactive components, leaving static HTML as zero-JS. Use progressive hydration: hydrate above-fold components first, defer below-fold: // Astro\\n<Counter client:load /> {/* Hydrate immediately */}\\n<HeavyChart client:visible /> {/* Hydrate when visible */}\\n<Comments client:idle /> {/* Hydrate when browser is idle */}. Use client:visible to hydrate on scroll into view: saves JS execution for below-fold content. Use client:idle for non-critical components: requestIdleCallback delays hydration. Use client:media for responsive components: <MobileNav client:media="(max-width: 768px)" /> only hydrates on mobile. Avoid hydration mismatches: server and client must render identical HTML. Use useId() for stable IDs across server and client. Defer hydration on slow connections: if (navigator.connection?.effectiveType === '4g') {\\n hydrateRoot(container, <App />);\\n}. Use React Server Components: zero client JS for server-only components. Measure hydration performance: performance.mark('hydration-start');\\nhydrateRoot(container, <App />);\\nperformance.mark('hydration-end');\\nperformance.measure('hydration', 'hydration-start', 'hydration-end');. Use requestIdleCallback for non-critical hydration: requestIdleCallback(() => {\\n hydrateRoot(container, <App />);\\n});. Avoid large hydration trees: split into smaller islands. Use streaming SSR: renderToPipeableStream in React 18 sends HTML in chunks, allowing the browser to paint progressively.

How do I optimize web fonts for performance?

Use font-display: swap to show fallback text immediately: @font-face {\\n font-family: 'Inter';\\n src: url('/fonts/inter.woff2') format('woff2');\\n font-display: swap;\\n}. Use font-display: optional for non-critical fonts: gives a 100ms window to load, then falls back permanently to avoid late text swaps. Preload critical fonts: <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>. Subset fonts to reduce file size: pyftsubset inter.ttf --output-file=inter-subset.woff2 --format=woff2 --unicodes="U+0000-00FF" for Latin only. Use variable fonts: one file covers all weights (100-900), reducing total font payload. Self-host fonts: Google Fonts CDN adds a DNS lookup, TCP connection, and TLS handshake. Use unicode-range to split fonts by script: @font-face {\\n font-family: 'NotoSans';\\n src: url('/fonts/noto-latin.woff2');\\n unicode-range: U+0000-00FF;\\n}\\n@font-face {\\n font-family: 'NotoSans';\\n src: url('/fonts/noto-cyrillic.woff2');\\n unicode-range: U+0400-04FF;\\n}. Use size-adjust to match fallback font metrics: @font-face {\\n font-family: 'Inter-fallback';\\n src: local('Arial');\\n size-adjust: 100.06%;\\n ascent-override: 95%;\\n descent-override: 22%;\\n line-gap-override: 0%;\\n}. Monitor font loading: document.fonts.load('16px Inter').then(() => {\\n console.log('Inter loaded');\\n});. Use font-display: block only for icon fonts where showing fallback characters is worse than no text. Use WOFF2 format: 30% smaller than WOFF, widely supported. Use font subsetting services: Google Fonts API supports text parameter for dynamic subsetting.

How do I implement edge-side rendering (ESR)?

Edge-side rendering moves SSR to CDN edge locations, reducing TTFB for users worldwide. Use Cloudflare Workers: export default {\\n async fetch(request, env) {\\n const html = await renderApp(request);\\n return new Response(html, {\\n headers: { 'Content-Type': 'text/html' }\\n });\\n }\\n};. Use Vercel Edge Functions: export default function handler(req, res) {\\n const html = renderApp(req);\\n res.setHeader('Content-Type', 'text/html');\\n res.send(html);\\n}. Use Deno Deploy: Deno.serve(async (req) => {\\n const html = await renderApp(req);\\n return new Response(html, {\\n headers: { 'Content-Type': 'text/html' }\\n });\\n});. Cache rendered HTML at the edge: Cache-Control: public, s-maxage=3600, stale-while-revalidate=60. Use streaming for progressive rendering: return new Response(\\n new ReadableStream({\\n start(controller) {\\n controller.enqueue(encoder.encode('<html><head>...</head><body>'));\\n controller.enqueue(encoder.encode(renderHeader()));\\n controller.enqueue(encoder.encode(renderMain()));\\n controller.enqueue(encoder.encode('</body></html>'));\\n controller.close();\\n }\\n }),\\n { headers: { 'Content-Type': 'text/html' } }\\n);. Use edge data stores: Cloudflare KV, Vercel Edge Config, Deno KV for low-latency data access. Handle authentication at the edge: verify JWTs in the Worker without calling the origin. Use edge middleware for A/B testing: const variant = Math.random() < 0.5 ? 'A' : 'B';\\nconst response = await fetch(request);\\nconst html = await response.text();\\nreturn new Response(html.replace('{{variant}}', variant), response);. Monitor edge performance: measure TTFB from multiple regions. Use edge-side includes (ESI) for partial caching: <esi:include src="/header" /> allows caching fragments independently.

How do I optimize API calls from the frontend?

Batch API requests: const fetchBatch = async (ids) => {\\n const response = await fetch('/api/items?ids=' + ids.join(','));\\n return response.json();\\n}; instead of individual requests. Use request deduplication: const cache = new Map();\\nasync function fetchUser(id) {\\n if (cache.has(id)) return cache.get(id);\\n const promise = fetch(/api/users/${id}).then(r => r.json());\\n cache.set(id, promise);\\n return promise;\\n}. Use SWR or React Query for caching: const { data } = useSWR('/api/users', fetcher); automatically deduplicates and caches. Debounce search API calls: const debouncedFetch = debounce((query) => fetchResults(query), 300);. Use pagination instead of loading all data: const response = await fetch('/api/items?page=1&limit=20');. Use GraphQL for precise data fetching: request only needed fields instead of over-fetching. Implement optimistic updates: mutate('/api/items', [...items, newItem], false); updates UI before server confirms. Use stale-while-revalidate: show cached data immediately, revalidate in background. Cancel in-flight requests: const controller = new AbortController();\\nfetch('/api/search?q=' + query, { signal: controller.signal });\\ncontroller.abort(); // Cancel on new search. Use IntersectionObserver for infinite scroll: const observer = new IntersectionObserver((entries) => {\\n if (entries[0].isIntersecting) {\\n loadMore();\\n }\\n});\\nobserver.observe(document.querySelector('.sentinel'));. Preload API data: <link rel="preload" href="/api/featured" as="fetch" crossorigin>. Use stale-while-revalidate cache headers: Cache-Control: max-age=60, stale-while-revalidate=600. Compress API responses: Accept-Encoding: gzip, br. Use HTTP/2 multiplexing: multiple API calls over one connection. Monitor API call waterfall with Chrome DevTools Network tab.

How do I implement progressive loading strategies?

Use skeleton screens: .skeleton {\\n background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);\\n background-size: 200% 100%;\\n animation: shimmer 1.5s infinite;\\n}\\n@keyframes shimmer {\\n 0% { background-position: 200% 0; }\\n 100% { background-position: -200% 0; }\\n}. Use blur-up image loading: img.lazy {\\n filter: blur(20px);\\n transition: filter 0.3s;\\n}\\nimg.lazy.loaded {\\n filter: blur(0);\\n}. Implement progressive JPEG: cjpeg -quality 60 -progressive input.jpg > output.jpg renders top-to-bottom as it loads. Use <link rel="modulepreload"> for JavaScript modules: import('./chart.js') preloads the module without executing it. Use requestIdleCallback for non-critical work: requestIdleCallback(() => {\\n // Prefetch next page data\\n fetch('/api/next-page-data');\\n});. Implement route prefetching on hover: link.addEventListener('mouseenter', () => {\\n import('./NextPage');\\n});. Use prefetch for likely-next resources: <link rel="prefetch" href="/next-page.js">. Use prerender for critical next pages: <link rel="prerender" href="/checkout"> (deprecated, use Speculation Rules API). Use streaming HTML: renderToPipeableStream in React 18 sends HTML in chunks. Use content-visibility: auto for below-fold sections: .section {\\n content-visibility: auto;\\n contain-intrinsic-size: 500px;\\n}. Implement lazy hydration: hydrate components when they enter viewport. Use loading="lazy" for images and iframes. Defer offscreen images: const observer = new IntersectionObserver((entries) => {\\n entries.forEach(entry => {\\n if (entry.isIntersecting) {\\n entry.target.src = entry.target.dataset.src;\\n observer.unobserve(entry.target);\\n }\\n });\\n});\\ndocument.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));. Use fetchpriority="low" for below-fold resources. Prioritize LCP element: <img src="hero.webp" fetchpriority="high">.

How do I monitor real user performance (RUM)?

Use web-vitals library: import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals';\\nonLCP(console.log);\\nonINP(console.log);\\nonCLS(console.log);. Send to analytics: function sendToAnalytics(metric) {\\n const body = JSON.stringify({\\n name: metric.name,\\n value: metric.value,\\n id: metric.id,\\n page: location.pathname,\\n userAgent: navigator.userAgent\\n });\\n navigator.sendBeacon('/api/rum', body);\\n}. Use Google Analytics 4 events: gtag('event', 'web_vitals', {\\n metric_name: metric.name,\\n metric_value: metric.value,\\n metric_id: metric.id\\n});. Use PerformanceObserver for custom metrics: new PerformanceObserver((list) => {\\n for (const entry of list.getEntries()) {\\n console.log(entry.name, entry.duration);\\n }\\n}).observe({ entryTypes: ['measure', 'paint', 'largest-contentful-paint'] });. Track page load: window.addEventListener('load', () => {\\n const timing = performance.getEntriesByType('navigation')[0];\\n console.log('DOM Content Loaded:', timing.domContentLoadedEventEnd - timing.startTime);\\n console.log('Load Complete:', timing.loadEventEnd - timing.startTime);\\n});. Track resource loading: new PerformanceObserver((list) => {\\n for (const entry of list.getEntries()) {\\n if (entry.transferSize > 50000) {\\n console.log(Large resource: ${entry.name} (${entry.transferSize} bytes));\\n }\\n }\\n}).observe({ entryTypes: ['resource'] });. Use Performance.mark and Performance.measure for custom timing: performance.mark('start-fetch');\\nfetch('/api/data').then(() => {\\n performance.mark('end-fetch');\\n performance.measure('fetch-duration', 'start-fetch', 'end-fetch');\\n});. Segment data by device, connection, and page. Use p75 as the primary metric: 75% of users experience this or better. Set up alerts for regressions: if p75 LCP exceeds 2.5s, notify the team. Use Chrome UX Report for field data: https://api.crux-report.com/ for real-world performance data.

How do I optimize video content for performance?

Use lazy loading for video: <video loading="lazy" src="hero.mp4" poster="poster.webp"></video>. Use preload="none" for below-fold videos: <video preload="none" src="intro.mp4"></video>. Use preload="metadata" for video previews: <video preload="metadata" src="trailer.mp4"></video> loads only metadata, not the full video. Use poster images: <video poster="poster.webp" src="video.mp4"> shows an image while the video loads. Serve responsive video: <video>\\n <source src="video-720.webm" media="(max-width: 720px)">\\n <source src="video-1080.webm" media="(min-width: 721px)">\\n</video>. Use adaptive streaming: HLS or DASH adjusts quality based on bandwidth: <video src="playlist.m3u8" controls></video>. Compress video: ffmpeg -i input.mp4 -c:v libx264 -crf 28 -preset slow output.mp4 for web-optimized output. Use modern codecs: AV1 (30% smaller than H.265), VP9 (30% smaller than H.264). Use autoplay="muted" for background videos: <video autoplay muted loop playsinline src="bg.mp4"> — muted autoplay is allowed by browsers. Avoid autoplay with sound: browsers block it and it degrades UX. Use playsinline for mobile: prevents fullscreen playback on iOS. Host videos on a CDN: video files are large and benefit from edge caching. Use disablepictureinpicture for non-essential videos: disablepictureinpicture attribute reduces resource usage. Monitor video loading: const video = document.querySelector('video');\\nvideo.addEventListener('loadeddata', () => console.log('Video ready'));\\nvideo.addEventListener('waiting', () => console.log('Buffering'));. Use preload="auto" only for above-fold hero videos. Consider replacing background videos with animated images or CSS animations for mobile devices.

How do I optimize for Core Web Vitals on WordPress?

Use caching plugins: WP Rocket or W3 Total Cache for page caching, object caching, and minification. Optimize images with Smush or ShortPixel: automatic WebP conversion and lazy loading. Use critical CSS plugins: Autoptimize or WP Rocket Critical CSS inline above-fold styles. Minify CSS and JS: Autoptimize or Fast Velocity Minify. Use a CDN: Cloudflare or BunnyCDN for edge caching. Limit plugins: each plugin adds JS/CSS overhead. Use a lightweight theme: GeneratePress, Astra, or Kadence are performance-optimized. Enable Gzip compression: AddOutputFilterByType DEFLATE text/html text/css application/javascript in .htaccess. Enable browser caching: ExpiresActive On\\nExpiresByType text/css "access plus 1 month"\\nExpiresByType application/javascript "access plus 1 month" in .htaccess. Use database optimization: WP-Optimize cleans post revisions, transients, and spam. Defer JavaScript: add_filter('script_loader_tag', function($tag) {\\n return str_replace(' src', ' defer src', $tag);\\n}); in functions.php. Remove unused CSS: add_filter('style_loader_tag', function($tag) {\\n return str_replace(' href', ' media=\"print\" onload=\"this.media=\\'all\\'\" href', $tag);\\n});. Use server-side rendering: WordPress is SSR by default, ensure your theme doesn’t break this. Optimize web fonts: self-host fonts with @font-face and font-display: swap. Use loading="lazy" for images: WordPress 5.5+ adds this automatically. Limit external scripts: Google Analytics, Facebook Pixel, etc. add overhead. Use wp_enqueue_script with defer or async: wp_enqueue_script('my-script', 'url', [], '1.0', true); the true parameter loads in footer.

How do I optimize for accessibility without sacrificing performance?

Use semantic HTML: <nav>, <main>, <article>, <section> are free — no JS or CSS needed. Use aria-label sparingly: only when semantic HTML is insufficient. Avoid ARIA overlays: role="button" on a <div> requires JavaScript for keyboard handling. Use <button> instead. Use prefers-reduced-motion: @media (prefers-reduced-motion: reduce) {\\n * {\\n animation-duration: 0.01ms !important;\\n transition-duration: 0.01ms !important;\\n }\\n}. Use prefers-color-scheme: @media (prefers-color-scheme: dark) {\\n body { background: #1a1a1a; color: #e0e0e0; }\\n}. Use prefers-contrast: @media (prefers-contrast: high) {\\n .text { color: #000; }\\n}. Use focus-visible: :focus-visible {\\n outline: 2px solid #3b82f6;\\n outline-offset: 2px;\\n} for keyboard-only focus styles. Use skip-to-content links: <a href="#main" class="skip-link">Skip to content</a> with .skip-link { position: absolute; top: -40px; }\\n.skip-link:focus { top: 0; }. Use alt text for images: <img src="chart.png" alt="Sales increased 20% from Q1 to Q2">. Use lang attribute: <html lang="en"> helps screen readers. Use tabindex correctly: tabindex="0" for focusable elements, tabindex="-1" to remove from tab order. Avoid tabindex values > 0. Use color contrast checkers: WCAG AA requires 4.5:1 for normal text, 3:1 for large text. Use prefers-reduced-data: @media (prefers-reduced-data: reduce) {\\n img { content: url('placeholder.png'); }\\n} to serve smaller assets. Use loading="lazy" for below-fold content: improves both performance and screen reader experience. Use describedby for complex widgets: aria-describedby="help-text" provides additional context. Test with keyboard navigation: Tab, Shift+Tab, Enter, Space, Escape should all work. Use role="status" for live regions: <div role="status" aria-live="polite">Loading...</div> announces updates without interrupting.

How do I optimize build performance?

Use incremental builds: webpack --watch or vite build --watch only rebuilds changed modules. Use build caching: webpack cache: { type: 'filesystem' } or vite built-in cache. Use esbuild for transpilation: esbuild-loader is 10-100x faster than babel-loader. Use SWC for compilation: @swc/core is Rust-based and considerably faster than Babel. Use thread-loader: module: { rules: [{ test: /\\.js$/, use: ['thread-loader', 'babel-loader'] }] } for parallel processing. Use terser for minification: optimization: { minimizer: [new TerserPlugin({ parallel: true })] }. Use splitChunks for optimal caching: optimization: {\\n splitChunks: {\\n chunks: 'all',\\n cacheGroups: {\\n vendor: { test: /[\\\\/]node_modules[\\\\/]/, name: 'vendor' }\\n }\\n }\\n}. Use externals for CDN-loaded libraries: externals: { react: 'React', 'react-dom': 'ReactDOM' }. Use tree shaking: mode: 'production' with ES modules enables dead code elimination. Use sideEffects: false in package.json: "sideEffects": false tells bundlers all files are side-effect free. Use persistent caching: cache: { type: 'filesystem', buildDependencies: { config: [__filename] } }. Use build: { target: 'esnext' } in Vite for modern browsers only. Use esbuild for development builds: Vite uses esbuild for dev which is near-instant. Use rollup-plugin-visualizer to identify large chunks. Use speed-measure-webpack-plugin to identify slow loaders: const smp = new SpeedMeasurePlugin();\\nmodule.exports = smp.wrap(config);. Use fork-ts-checker-webpack-plugin for type checking in parallel: type checking doesn’t block compilation. Use module federation for micro-frontends: shared dependencies reduce build time and bundle size. Use swc-loader instead of babel-loader: SWC is 20x faster for TypeScript transpilation. Use lightningcss instead of postcss for CSS minification: Rust-based, 100x faster.

How do I optimize JavaScript execution performance?

Use requestAnimationFrame for visual updates: function animate() {\\n element.style.transform = translateX(${pos}px);\\n requestAnimationFrame(animate);\\n}\\nrequestAnimationFrame(animate);. Use requestIdleCallback for non-visual work: requestIdleCallback(() => {\\n // Process data, send analytics\\n});. Debounce scroll and resize handlers: window.addEventListener('scroll', debounce(handler, 16));. Use passive event listeners: window.addEventListener('scroll', handler, { passive: true }); tells the browser the handler won’t call preventDefault(). Use Web Workers for CPU-intensive tasks: const worker = new Worker('compute.js');\\nworker.postMessage(data);\\nworker.onmessage = (e) => console.log(e.data);. Use OffscreenCanvas for canvas rendering in a Worker: const canvas = element.transferControlToOffscreen();\\nconst worker = new Worker('renderer.js');\\nworker.postMessage({ canvas }, [canvas]);. Use SharedArrayBuffer for zero-copy data sharing between threads. Use ArrayBuffer and typed arrays for numerical data: const floats = new Float32Array(1000) is faster than const floats = new Array(1000). Use structuredClone for deep cloning: const copy = structuredClone(obj) is faster than JSON.parse(JSON.stringify(obj)). Use Object.freeze for immutable objects: frozen objects allow engines to optimize property access. Use Map and Set for frequent lookups: const set = new Set([1, 2, 3]); set.has(2) is O(1) vs Array.includes O(n). Use WeakMap for DOM-related metadata: const meta = new WeakMap(); meta.set(element, { data }); allows garbage collection when the element is removed. Avoid delete on objects: it deoptimizes hidden classes. Set to undefined instead: obj.prop = undefined. Use for...of or forEach for iteration: faster than for...in which iterates over prototype chain. Use Array.from for converting iterables: Array.from(document.querySelectorAll('.item')). Use Promise.all for parallel async operations: const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);. Use AbortController for cancellable fetches. Use queueMicrotask for microtask scheduling: queueMicrotask(() => console.log('microtask'));. Use performance.now() for high-resolution timing: const start = performance.now(); doWork(); console.log(performance.now() - start);.

How do I optimize rendering performance?

Use CSS transform and opacity for animations: they’re compositor-only properties that don’t trigger layout or paint. Avoid animating width, height, top, left, margin, padding — they trigger layout recalculations. Use will-change sparingly: .animated { will-change: transform; } hints the browser to create a separate layer, but overuse wastes memory. Use contain: strict for isolated components: .widget { contain: strict; } prevents the component’s changes from affecting the rest of the page. Use content-visibility: auto for below-fold sections: .section { content-visibility: auto; contain-intrinsic-size: 1000px; } skips rendering until visible. Use contain: layout style paint for complex widgets. Reduce reflows: batch DOM writes: element.style.cssText = 'width: 100px; height: 50px;'; instead of individual property changes. Use DocumentFragment for batch DOM insertions: const fragment = document.createDocumentFragment();\\nitems.forEach(item => {\\n const li = document.createElement('li');\\n li.textContent = item;\\n fragment.appendChild(li);\\n});\\nlist.appendChild(fragment);. Use requestAnimationFrame for visual updates: ensures updates happen before paint. Use ResizeObserver instead of resize events: const observer = new ResizeObserver(entries => {\\n for (const entry of entries) {\\n console.log(entry.contentRect.width);\\n }\\n});\\nobserver.observe(element);. Use IntersectionObserver for lazy loading and infinite scroll. Avoid forced synchronous layout: element.style.width = '100px';\\nconst width = element.offsetWidth; // Forces layout\\nconsole.log(width);. Read layout properties before writing: const width = element.offsetWidth;\\nelement.style.width = width + 10 + 'px';. Use display: none instead of visibility: hidden for elements that don’t need rendering: display: none removes from render tree, visibility: hidden still renders. Use position: fixed or position: absolute for animated elements: they don’t affect surrounding layout. Use pointer-events: none for decorative elements: reduces hit-testing overhead. Use backface-visibility: hidden for 3D transforms: .card { backface-visibility: hidden; } promotes to its own layer. Use transform: translateZ(0) as a layer promotion hack for older browsers. Use Chrome DevTools “Performance” tab to identify layout thrashing and long tasks.

How do I optimize memory usage?

Detect memory leaks with Chrome DevTools “Memory” tab: take heap snapshots, compare them, and look for retained objects. Use WeakMap and WeakSet for references that shouldn’t prevent garbage collection: const cache = new WeakMap(); cache.set(element, data);. Remove event listeners: element.removeEventListener('click', handler); when elements are removed from DOM. Use AbortController for fetch cancellation: const controller = new AbortController();\\nfetch(url, { signal: controller.signal });\\ncontroller.abort(); cancels the request and releases memory. Clean up intervals and timeouts: clearInterval(intervalId); clearTimeout(timeoutId);. Use FinalizationRegistry for cleanup: const registry = new FinalizationRegistry((heldValue) => {\\n console.log(Cleaned up: ${heldValue});\\n});\\nregistry.register(obj, 'my-object');. Use WeakRef for caches: const cache = new Map();\\nconst ref = new WeakRef(obj);\\nif (ref.deref()) { /* object still alive */ }. Avoid closures that capture large objects: function createHandler() {\\n const huge = new Array(1000000);\\n return () => console.log(huge.length); // huge is retained\\n}. Use object pooling for frequently created/destroyed objects: const pool = [];\\nfunction acquire() { return pool.pop() || new Particle(); }\\nfunction release(p) { pool.push(p); }. Use structuredClone for deep cloning instead of JSON parse/stringify: faster and handles more types. Detach ArrayBuffers after use: const buffer = new ArrayBuffer(1024);\\nstructuredClone(buffer, { transfer: [buffer] }); transfers ownership. Use performance.memory (Chrome only) to monitor heap: console.log(performance.memory.usedJSHeapSize);. Use IntersectionObserver to unload below-fold components: disconnect observers when components unmount. Use disconnect() on observers: observer.disconnect(); when done. Avoid circular references: they prevent garbage collection in older engines. Use document.createDocumentFragment() for batch DOM operations: fragments are lighter than full DOM nodes. Monitor memory with PerformanceObserver: new PerformanceObserver((list) => {\\n for (const entry of list.getEntries()) {\\n if (entry.entryType === 'gc') {\\n console.log(GC: ${entry.duration}ms);\\n }\\n }\\n}).observe({ entryTypes: ['gc'] });. Use navigator.deviceMemory to adapt based on available RAM: if (navigator.deviceMemory < 2) { /* low-end device */ }.

How do I use Speculation Rules API for prefetching?

The Speculation Rules API replaces deprecated prerender and prefetch link relations. Add rules via JSON in a script tag: <script type="speculationrules">\\n{\\n "prefetch": [\\n { "source": "list", "urls": ["/about", "/products"] }\\n ]\\n}\\n</script>. Use document rules for dynamic prefetching: <script type="speculationrules">\\n{\\n "prefetch": [\\n {\\n "source": "document",\\n "where": { "selector_matches": "a.prefetch-link" },\\n "eagerness": "moderate"\\n }\\n ]\\n}\\n</script>. Use eagerness to control when prefetching happens: immediate (right away), moderate (on hover), conservative (on pointer down). Use prerender for full page prerendering: <script type="speculationrules">\\n{\\n "prerender": [\\n { "source": "list", "urls": ["/next-page"] }\\n ]\\n}\\n</script>. Prerendered pages are fully loaded in the background, including JavaScript execution. Use where with href_matches for pattern matching: "where": { "href_matches": "/products/*" }. Combine rules: prefetch some pages, prerender others. Check support: if (HTMLScriptElement.supports('speculationrules')) { /* supported */ }. Use no-vary-search hint: "no_vary_search": true tells the browser to reuse prefetched responses even if URL parameters change. Monitor speculation rules performance: new PerformanceObserver((list) => {\\n for (const entry of list.getEntries()) {\\n if (entry.name === 'Speculation-Rule') {\\n console.log(entry);\\n }\\n }\\n}).observe({ entryTypes: ['navigation'] });. Use expectation field: "expectation": "successful" only prefetches if the response is likely successful (200 status). Use relative_to for base URL: "relative_to": "/docs/" resolves relative URLs. Limit prerendered pages: too many consume memory and CPU. Use navigator.scheduling.isInputPending() to avoid prefetching during user interaction. Use Chrome flags to test: chrome://flags/#enable-speculation-rules. Use Speculation-Rules header: Speculation-Rules: "/rules.json" to load rules from an external file.

How do I optimize for low-end devices?

Detect low-end devices: const isLowEnd = navigator.deviceMemory < 2 || navigator.hardwareConcurrency < 4;. Reduce JavaScript bundle: serve a lighter version with fewer features. Use adaptive serving: if (isLowEnd) {\\n import('./lightweight-app.js');\\n} else {\\n import('./full-app.js');\\n}. Reduce image quality: <img srcset=\"image-300.webp 300w, image-600.webp 600w\" sizes=\"(max-width: 400px) 300px, 600px\">. Disable animations: @media (prefers-reduced-data: reduce) {\\n * { animation: none !important; transition: none !important; }\\n}. Reduce font payload: use system fonts on low-end devices: font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;. Disable non-essential features: comments, recommendations, social sharing widgets. Use Save-Data header: if (navigator.connection?.saveData) {\\n // Serve minimal version\\n}. Reduce DOM size: fewer elements means less memory and faster rendering. Use CSS content-visibility: auto aggressively. Avoid heavy libraries: replace moment.js with date-fns, replace lodash with native methods. Use requestIdleCallback for non-critical work: allows the browser to prioritize rendering. Use navigator.scheduling.isInputPending() to yield to user interactions. Serve fewer, larger files: reduces HTTP overhead on slow connections. Use HTTP/2 or HTTP/3 for multiplexing. Cache aggressively with Service Workers: offline-first strategies reduce network requests on repeat visits. Use loading="lazy" for all below-fold media. Defer all non-critical JavaScript. Use fetchpriority="low" for below-fold resources. Monitor long tasks: new PerformanceObserver((list) => {\\n for (const entry of list.getEntries()) {\\n if (entry.duration > 50) {\\n console.log(Long task: ${entry.duration}ms);\\n }\\n }\\n}).observe({ entryTypes: ['longtask'] });.

How do I use the Performance Panel in Chrome DevTools?

Open DevTools with F12 or Ctrl+Shift+I, go to the “Performance” tab. Click “Record” (circle button) and interact with your page. Click “Stop” to analyze the recording. Read the flame chart: x-axis is time, y-axis is call stack depth. Look for long yellow blocks (JavaScript), wide purple blocks (layout/reflow), and green blocks (paint). Use “Bottom-Up” view to see which functions take the most total time. Use “Call Tree” view to see the hierarchical structure of function calls. Use “Event Log” to see individual events sorted by duration. Enable “CPU: 4x slowdown” to simulate mobile devices. Enable “Network: Slow 3G” to test on slow connections. Use “Screenshots” checkbox to see visual frames during the recording. Use “Memory” checkbox to track heap allocations. Use performance.mark() and performance.measure() to add custom markers: performance.mark('start-render'); renderApp(); performance.mark('end-render'); performance.measure('render', 'start-render', 'end-render');. Use the “Coverage” tab to find unused JavaScript and CSS: Ctrl+Shift+P > “Show Coverage”. Use the “Rendering” tab to enable paint flashing and layout shift regions. Use the “Insights” panel for automated performance suggestions. Use performance.measureUserAgentSpecificMemory() for cross-origin memory measurement. Use Lighthouse from within DevTools for an all-in-one audit: “Lighthouse” tab > “Generate report”.

How do I optimize HTTP headers for performance?

Use Cache-Control: Cache-Control: public, max-age=31536000, immutable for static assets with hashed filenames. Use stale-while-revalidate: Cache-Control: max-age=60, stale-while-revalidate=600 for dynamic content. Use ETag for conditional requests: ETag: "abc123" — browser sends If-None-Match: "abc123", server responds with 304 Not Modified if unchanged. Use Last-Modified: Last-Modified: Wed, 09 Jul 2026 12:00:00 GMT — browser sends If-Modified-Since. Use Content-Encoding: Content-Encoding: br for Brotli (20% smaller than gzip), Content-Encoding: gzip as fallback. Use Content-Type with charset: Content-Type: text/html; charset=utf-8. Use X-Content-Type-Options: nosniff to prevent MIME sniffing. Use Strict-Transport-Security: max-age=31536000; includeSubDomains for HTTPS enforcement. Use X-Frame-Options: DENY to prevent clickjacking. Use Content-Security-Policy to restrict resource loading: Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'. Use Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp for cross-origin isolation (enables SharedArrayBuffer). Use Cross-Origin-Resource-Policy: same-origin to restrict resource access. Use Permissions-Policy to disable unused APIs: Permissions-Policy: camera=(), microphone=(), geolocation=(). Use Service-Worker-Allowed to expand SW scope: Service-Worker-Allowed: /. Use Date header for caching calculations. Use Vary: Accept-Encoding to cache compressed and uncompressed versions separately. Use Vary: Cookie if content varies by authentication state. Use 103 Early Hints for preloading: Link: </styles.css>; rel=preload; as=style sent before the full response.

How do I use Resource Hints effectively?

Use <link rel="preconnect"> to establish early connections: <link rel="preconnect" href="https://cdn.example.com">. Use <link rel="dns-prefetch"> for DNS-only resolution: <link rel="dns-prefetch" href="https://api.example.com">. Use <link rel="preload"> for critical resources: <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>. Use <link rel="prefetch"> for likely next-page resources: <link rel="prefetch" href="/next-page.js">. Use <link rel="prerender"> for full page prerendering (deprecated, use Speculation Rules API instead). Use fetchpriority attribute: <img src="hero.webp" fetchpriority="high"> for above-fold images, <img src="below.webp" fetchpriority="low"> for below-fold. Use importance on fetch: fetch('/api/critical', { importance: 'high' }). Use requestIdleCallback for non-critical prefetches: requestIdleCallback(() => fetch('/next-page-data')). Use navigator.connection.effectiveType to adapt: if (navigator.connection.effectiveType === '4g') { prefetchNextPage(); }. Use navigator.connection.saveData to respect data-saving preferences. Use Cross-Origin-Resource-Policy: cross-origin for cross-origin preloaded resources. Use as attribute correctly: as="script", as="style", as="font", as="image", as="fetch". Use type attribute for module preloading: <link rel="modulepreload" href="/app.js">. Use crossorigin for cross-origin fonts: <link rel="preload" href="https://cdn.example.com/font.woff2" as="font" crossorigin>.

See Also


Last updated: 2026-07-09