Web Performance Optimization Guide — Core Web Vitals, Images, Caching

How Web Performance Impacts Business

A 1-second delay in page loading reduces conversions by 7%. Web performance is not a technical issue — it is a business issue. If you walk into a restaurant and have to wait 30 minutes after ordering, most customers leave. Websites are no different.

Google has factored Core Web Vitals into search rankings since 2021. Sites with better performance also have an SEO advantage.

Understanding Core Web Vitals

MetricWhat it measuresGoodNeeds improvementPoor
LCP (Largest Contentful Paint)Render time of the largest content2.5s or less2.5-4sOver 4s
INP (Interaction to Next Paint)User interaction response time200ms or less200-500msOver 500ms
CLS (Cumulative Layout Shift)Visual stability (layout movement)0.1 or less0.1-0.25Over 0.25

Measurement Tools

# Measure performance with Lighthouse CLI
npm install -g lighthouse
lighthouse https://example.com --output html --output-path ./report.html

# Using PageSpeed Insights API
curl "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?\
url=https://example.com&\
category=performance&\
strategy=mobile"

Improving LCP: Render the Largest Content Fast

LCP measures the time until the largest image or text block in the viewport is rendered.

Key Causes and Solutions

<!-- 1. Optimize hero images — load the LCP element fast -->

<!-- Bad: lazy loading applied to LCP element -->
<img src="hero.jpg" loading="lazy" alt="Main banner" />

<!-- Good: LCP element loads immediately + preload -->
<link rel="preload" as="image" href="hero.webp" />
<img src="hero.webp"
     fetchpriority="high"
     alt="Main banner"
     width="1200"
     height="600" />
<!-- 2. Inline critical CSS -->
<head>
  <!-- Inline CSS needed for the first screen -->
  <style>
    /* Critical CSS — only styles essential for initial render */
    .hero { display: flex; align-items: center; min-height: 60vh; }
    .hero-title { font-size: 2.5rem; font-weight: 700; }
    .nav { display: flex; gap: 1rem; padding: 1rem; }
  </style>

  <!-- Load remaining CSS asynchronously -->
  <link rel="preload" as="style" href="/styles/main.css"
        onload="this.onload=null;this.rel='stylesheet'" />
</head>
// 3. Optimize server response time (improve TTFB)
// Response compression + cache headers in Express.js

import express from "express";
import compression from "compression";

const app = express();

// Enable Gzip/Brotli compression
app.use(compression({
  level: 6,         // Compression level (1-9, 6 is balanced)
  threshold: 1024   // Only compress files over 1KB
}));

// Set cache headers for static files
app.use("/static", express.static("public", {
  maxAge: "30d",           // 30-day cache
  immutable: true,         // When filenames include a hash
  etag: true
}));

// Always serve the latest HTML version
app.use((req, res, next) => {
  if (req.path.endsWith(".html") || !req.path.includes(".")) {
    res.setHeader("Cache-Control", "no-cache, must-revalidate");
  }
  next();
});

Improving CLS: Preventing Layout Shifts

CLS measures when elements suddenly shift during page loading. The experience of a button moving just as you try to click it is extremely frustrating.

<!-- 1. Specify dimensions for images/videos — reserve space in advance -->

<!-- Bad: no dimensions → layout shifts when image loads -->
<img src="photo.webp" alt="Photo" />

<!-- Good: set width/height or aspect-ratio -->
<img src="photo.webp" alt="Photo" width="800" height="600" />

<!-- Using CSS aspect-ratio -->
<style>
  .video-container {
    aspect-ratio: 16 / 9;
    width: 100%;
    background: #f0f0f0; /* Placeholder during loading */
  }
</style>
/* 2. Prevent layout shifts from web font loading */

/* font-display: swap — show system font before custom font loads */
@font-face {
  font-family: "Pretendard";
  src: url("/fonts/Pretendard.woff2") format("woff2");
  font-display: swap;          /* Allow FOUT, prevent CLS */
  size-adjust: 100%;           /* Minimize size difference on font swap */
}

/* 3. Reserve space for dynamic content in advance */
.ad-slot {
  min-height: 250px;           /* Reserve ad area in advance */
  background: #f5f5f5;
  contain: layout;             /* Isolate with CSS containment */
}

.skeleton-card {
  height: 200px;               /* Reserve space with skeleton UI */
  border-radius: 8px;
  background: linear-gradient(
    90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%
  );
  animation: shimmer 1.5s infinite;
}

Image Optimization

Images account for more than 50% of the total page weight on average.

<!-- 1. Use next-gen formats + fallback -->
<picture>
  <!-- AVIF — best compression (Chrome 85+, Firefox 93+) -->
  <source type="image/avif" srcset="photo.avif" />
  <!-- WebP — wide compatibility (all except IE) -->
  <source type="image/webp" srcset="photo.webp" />
  <!-- JPEG — fallback -->
  <img src="photo.jpg" alt="Photo" width="800" height="600"
       loading="lazy" decoding="async" />
</picture>

<!-- 2. Responsive images — serve the right image for screen size -->
<img srcset="photo-400w.webp 400w,
             photo-800w.webp 800w,
             photo-1200w.webp 1200w"
     sizes="(max-width: 600px) 400px,
            (max-width: 1024px) 800px,
            1200px"
     src="photo-800w.webp"
     alt="Responsive image"
     loading="lazy"
     decoding="async" />
FormatCompression (vs JPEG)Browser supportUse case
AVIF50% smallerModern browsersPhotos, illustrations
WebP30% smallerAll except IEGeneral purpose
JPEGBaselineAllFallback
PNGLargerAllWhen transparency is needed
SVGVery smallAllIcons, logos

Browser Caching Strategy

# Nginx caching configuration example

# Static assets — long-term cache (assumes hashed filenames)
location ~* \.(js|css|woff2|avif|webp|png|jpg|svg)$ {
    expires 365d;
    add_header Cache-Control "public, immutable";
}

# HTML — always revalidate
location ~* \.html$ {
    expires -1;
    add_header Cache-Control "no-cache, must-revalidate";
}

# API responses — no caching
location /api/ {
    add_header Cache-Control "no-store";
}
StrategyCache-ControlTargetDescription
Long-term cachemax-age=31536000, immutableJS, CSS, images (with hash)Invalidate cache by changing filename
Revalidateno-cacheHTMLCheck with server before each use
No cacheno-storeAPI, personal dataNever cache
Short cachemax-age=300Frequently changing dataCache for 5 minutes

Practical Tips

  • Measure first, optimize later: Do not optimize by intuition. Use Lighthouse and WebPageTest to pinpoint bottlenecks before making improvements.
  • Apply the highest-impact changes first: Image optimization (WebP conversion) and caching configuration alone can dramatically improve perceived performance.
  • Monitor bundle size: Use webpack-bundle-analyzer or source-map-explorer to inspect bundle composition. Unused libraries may be included.
  • Manage third-party scripts: Analytics, ads, and chat widgets are often the primary culprits for performance degradation. Apply defer or async.
  • Set a Performance Budget: Define budgets like “JS bundle under 300KB” and “LCP under 2 seconds”, then automatically check them in CI.

Was this article helpful?