Web Performance Optimization: A Practical Guide for Developers

Learn proven techniques to improve Core Web Vitals, reduce bundle size, optimise images and fonts, and deliver fast experiences for all users.

By Belqassim Djafer · Published · Updated

Why Performance Is a Feature

A 100ms delay in page load time correlates with a 1% drop in conversion rate (Amazon, 2006). Modern studies show even more sensitivity: users abandon mobile sites that take more than 3 seconds to load. Google's Core Web Vitals directly influence search rankings since the 2021 Page Experience update.

Performance is not a "nice to have" — it is core product quality.

Core Web Vitals

Google's user-centric performance metrics:

Metric What It Measures Good Needs Work Poor
LCP (Largest Contentful Paint) Loading performance ≤ 2.5s 2.5–4s > 4s
INP (Interaction to Next Paint) Responsiveness ≤ 200ms 200–500ms > 500ms
CLS (Cumulative Layout Shift) Visual stability ≤ 0.1 0.1–0.25 > 0.25

Measure with: Chrome DevTools, Lighthouse, PageSpeed Insights, WebPageTest.

JavaScript Bundle Optimization

Code Splitting

Never ship one giant JavaScript bundle. Split by route and lazy-load:

// React with React.lazy + Suspense
import { lazy, Suspense } from "react";

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

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Dashboard />
    </Suspense>
  );
}
// Vite / Rollup: manual chunks
// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ["react", "react-dom"],
          charts: ["recharts", "d3"],
        }
      }
    }
  }
};

Tree Shaking

Ensure your build tool eliminates dead code:

// Import only what you need (tree-shakeable)
import { format } from "date-fns";

// Avoid: imports everything from lodash
import _ from "lodash";

// Better: cherry-pick functions
import debounce from "lodash/debounce";

// Best: use native alternatives where possible
const debounced = setTimeout(() => fn(), 300); // No library needed

Analyse Your Bundle

# Vite bundle analyser
npx vite-bundle-visualizer

# webpack
npx webpack-bundle-analyzer stats.json

Image Optimization

Images are often the heaviest part of a page.

Use Modern Formats

<!-- WebP with JPEG fallback -->
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero image" width="800" height="500">
</picture>

Savings by format (typical):

  • JPEG → WebP: 25–34% smaller
  • JPEG → AVIF: 50% smaller
  • PNG → WebP: up to 90% smaller for photos

Lazy Loading

<!-- Native lazy loading: defers off-screen images -->
<img src="image.jpg" loading="lazy" alt="..." width="600" height="400">

Always Specify Dimensions

Missing width/height causes layout shift (CLS):

<!-- Good: prevents layout shift -->
<img src="photo.jpg" width="600" height="400" alt="...">

<!-- Bad: browser doesn't know size until loaded -->
<img src="photo.jpg" alt="...">

Font Optimization

Fonts are often a top cause of render-blocking and LCP delays.

<!-- Preconnect to font origin -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- Preload critical fonts -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
/* font-display: swap eliminates invisible text during load */
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter.woff2") format("woff2");
  font-display: swap;
}

Variable Fonts

Replace multiple font files (Regular, Bold, Italic) with one variable font:

@font-face {
  font-family: "Inter";
  src: url("Inter.var.woff2") format("woff2");
  font-weight: 100 900;    /* Entire weight range in one file */
  font-style: normal;
}

Critical CSS

Inline CSS for above-the-fold content to eliminate render-blocking:

<head>
  <!-- Critical CSS inlined: no round-trip needed -->
  <style>
    /* Only the styles needed for the first visible screen */
    body { margin: 0; font-family: system-ui; }
    .header { height: 64px; background: #fff; }
    .hero { min-height: 400px; }
  </style>

  <!-- Non-critical CSS loaded asynchronously -->
  <link rel="preload" href="/styles/app.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
</head>

Caching Strategy

# Content-addressable assets (hash in filename): cache forever
/js/app.a1b2c3d4.js       Cache-Control: max-age=31536000, immutable

# HTML and API responses: revalidate quickly
/index.html               Cache-Control: no-cache
/api/data                 Cache-Control: private, max-age=60

HTTP/2 and Compression

# nginx: enable gzip and brotli
gzip on;
gzip_types text/plain text/css application/javascript application/json;
gzip_min_length 1024;

# Brotli (better compression than gzip)
brotli on;
brotli_types text/plain text/css application/javascript application/json;

Brotli typically achieves 15–25% better compression than gzip for text assets.

Performance Checklist

Load performance:

  • Largest element visible without scrolling loads ≤ 2.5s
  • Images have explicit width/height and use loading="lazy"
  • Images served in WebP or AVIF
  • JavaScript split by route, largest vendor chunk < 200KB
  • Fonts preloaded and use font-display: swap
  • Critical CSS inlined
  • Assets compressed with gzip or brotli

Interactivity:

  • Long tasks (> 50ms) on the main thread identified and broken up
  • Input handlers are non-blocking
  • Third-party scripts loaded async or deferred

Visual stability:

  • No layout shifts after page load (ads, embeds, images all have reserved space)
  • Web fonts don't cause text reflow

Conclusion

Performance optimization is incremental. Start by measuring with Lighthouse or WebPageTest, identify the biggest bottlenecks (usually images or JavaScript), fix those first, and remeasure. A 20% improvement in LCP can meaningfully increase conversion rates and search visibility.

Related Tools