Verified by Garnet Grid

How to Optimize Web Performance: Core Web Vitals and Beyond

Achieve sub-2-second load times and pass Core Web Vitals. Covers image optimization, lazy loading, code splitting, CDN strategy, and Lighthouse auditing.

A 1-second delay in page load reduces conversions by 7%. A 3-second delay loses 53% of mobile visitors. Google uses Core Web Vitals as a ranking signal. Performance isn’t optional.


The Three Core Web Vitals

MetricWhat It MeasuresGoodNeeds WorkPoor
LCP (Largest Contentful Paint)Loading speed≤ 2.5s2.5-4.0s> 4.0s
INP (Interaction to Next Paint)Responsiveness≤ 200ms200-500ms> 500ms
CLS (Cumulative Layout Shift)Visual stability≤ 0.10.1-0.25> 0.25

Step 1: Audit Current Performance

# Lighthouse CLI audit
npx lighthouse https://yoursite.com \
  --output=json --output=html \
  --output-path=./lighthouse-report \
  --chrome-flags="--headless"

# PageSpeed Insights API
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://yoursite.com&strategy=mobile&key=$API_KEY" \
  | jq '.lighthouseResult.categories.performance.score'

Step 2: Optimize Images (Biggest Impact)

Images typically account for 50-80% of page weight.

2.1 Use Modern Formats

<!-- WebP with JPEG fallback -->
<picture>
  <source srcset="hero.webp" type="image/webp">
  <source srcset="hero.avif" type="image/avif">
  <img src="hero.jpg" alt="Hero image"
       width="1200" height="600"
       loading="lazy"
       decoding="async">
</picture>

2.2 Responsive Images

<img
  srcset="
    hero-400.webp 400w,
    hero-800.webp 800w,
    hero-1200.webp 1200w,
    hero-1600.webp 1600w
  "
  sizes="(max-width: 600px) 400px,
         (max-width: 1024px) 800px,
         1200px"
  src="hero-800.webp"
  alt="Hero image"
  width="1200"
  height="600"
  loading="lazy"
>

2.3 Batch Optimization Script

# Convert all images to WebP with quality 80
for img in *.{jpg,png}; do
  cwebp -q 80 "$img" -o "${img%.*}.webp"
done

# Generate responsive sizes
for img in *.webp; do
  for size in 400 800 1200 1600; do
    convert "$img" -resize "${size}x" "${img%.*}-${size}.webp"
  done
done

Step 3: Implement Code Splitting

// React — dynamic imports for route-based splitting
import { lazy, Suspense } from 'react';

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

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/reports" element={<Reports />} />
      </Routes>
    </Suspense>
  );
}

Step 4: Optimize CSS Delivery

<!-- Inline critical CSS -->
<style>
  /* Critical above-the-fold styles */
  body { margin: 0; font-family: system-ui; }
  .hero { min-height: 100vh; display: grid; place-items: center; }
  .nav { position: sticky; top: 0; z-index: 100; }
</style>

<!-- Defer non-critical CSS -->
<link rel="preload" href="styles.css" as="style"
      onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>

Step 5: CDN and Caching Strategy

# Nginx caching headers
location ~* \.(js|css|png|webp|avif|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

location ~* \.html$ {
    expires 10m;
    add_header Cache-Control "public, must-revalidate";
}

CDN Checklist

Asset TypeCache DurationCDN Strategy
Static assets (JS, CSS, images)1 year (fingerprinted)Edge cache
HTML pages10 minutesStale-while-revalidate
API responses0 (no-store)Origin only
Fonts1 yearEdge cache

Step 6: Fix CLS (Layout Shift)

/* Always specify dimensions for images and videos */
img, video {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;  /* Reserves space before load */
}

/* Reserve space for ads */
.ad-slot {
  min-height: 250px;
  background: #f0f0f0;
}

/* Prevent font swap layout shift */
@font-face {
  font-family: 'CustomFont';
  src: url('font.woff2') format('woff2');
  font-display: swap;
  size-adjust: 105%;  /* Match fallback font metrics */
}

Step 7: Preload Critical Resources

<head>
  <!-- Preload LCP image -->
  <link rel="preload" as="image" href="hero.webp" fetchpriority="high">

  <!-- Preconnect to third-party origins -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://cdn.yoursite.com">

  <!-- DNS prefetch for analytics -->
  <link rel="dns-prefetch" href="https://www.google-analytics.com">
</head>

Performance Budget

Set and enforce performance budgets:

MetricBudgetEnforcement
Total page weight< 1.5 MBCI/CD check
JavaScript bundle< 300 KB (gzipped)Webpack analyzer
CSS bundle< 50 KB (gzipped)Build check
LCP< 2.5 secondsLighthouse CI
INP< 200msReal User Monitoring
CLS< 0.1Lighthouse CI
# Lighthouse CI in GitHub Actions
npx @lhci/cli collect --url="https://staging.yoursite.com"
npx @lhci/cli assert \
  --preset=lighthouse:recommended \
  --assert.maxSize=1572864   # 1.5 MB

Performance Checklist

  • Lighthouse score > 90 on mobile
  • Images in WebP/AVIF with responsive srcset
  • Lazy loading on below-the-fold images
  • Code splitting for route-based chunks
  • Critical CSS inlined, non-critical deferred
  • CDN with 1-year cache on fingerprinted assets
  • All media has explicit width/height (no CLS)
  • Fonts use font-display: swap
  • Preload LCP image and preconnect third-party
  • Performance budget enforced in CI/CD

:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For performance audits, visit garnetgrid.com. :::