Back to Articles
Web Development Jan 4, 2026 · 15 min read

Web Performance Optimization Techniques

Daniel

Software Developer

I recently audited my blog’s performance and was shocked — my Lighthouse score had dropped from 98 to 74 over six months. A few careless additions had destroyed my Core Web Vitals. Here is how I diagnosed and fixed every issue, with real numbers.

Starting Point: The Audit

Before fixing anything, I ran a full performance audit using three tools:

  • Lighthouse 12: Overall score 74, LCP 4.2s, CLS 0.18
  • WebPageTest: Time to Interactive 5.8s, First Byte 180ms
  • Chrome DevTools Performance panel: Revealed layout shifts and excessive main thread work

The two biggest offenders: unoptimized images (contributing 2.1s to LCP) and render-blocking JavaScript (adding 1.3s to TTI). Let me walk through how I fixed each one.

Image Optimization: The Biggest Win

Images are typically the largest assets on a page. I had 12 hero images loading at full resolution — some were 3MB each. Here’s my optimization pipeline:

Modern Formats: WebP and AVIF

I switched all images to AVIF with WebP fallback. The file size reduction was dramatic:

<picture>
  <source srcset="hero.avif" type="image/avif" />
  <source srcset="hero.webp" type="image/webp" />
  <img 
    src="hero.jpg" 
    alt="Hero image"
    width="1200"
    height="630"
    loading="lazy"
    decoding="async"
  />
</picture>

Before: JPEG hero image — 2.4MB
After: AVIF version — 180KB (93% reduction)

Responsive Images with srcset

Don’t serve a 2400px image to a 375px phone screen. Use srcset to serve the right size:

<img
  srcset="
    hero-400w.avif 400w,
    hero-800w.avif 800w,
    hero-1200w.avif 1200w,
    hero-1600w.avif 1600w
  "
  sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 1200px"
  src="hero-1200w.avif"
  alt="Hero image"
  loading="lazy"
  decoding="async"
/>

The Image CDN Approach

For dynamic sites, use an image CDN that transforms on-the-fly:

<!-- Cloudflare Image Resizing -->
<img src="https://example.com/cdn-cgi/image/width=800,format=auto/original.jpg" />

<!-- Next.js Image Component (auto-optimizes) -->
import Image from 'next/image';
<Image
  src="/hero.jpg"
  width={1200}
  height={630}
  sizes="(max-width: 768px) 100vw, 50vw"
  priority={true}
/>

Image optimization alone improved my LCP from 4.2s to 1.8s. That’s a 57% improvement from one change.

JavaScript: The Silent Killer

My blog had accumulated three analytics scripts, a chat widget, and an email popup — all loaded synchronously in the <head>. Each one was adding 200-400ms to my page load.

Audit Your Scripts

I used Chrome DevTools’ Coverage tab to find unused JavaScript. The results were embarrassing:

  • Main bundle: 45KB gzipped (acceptable)
  • Analytics scripts: 120KB gzipped (loaded on every page)
  • Chat widget: 85KB gzipped (loaded even though 90% of visitors never open it)

The Fix: Dynamic Imports

I moved non-critical scripts to load after the page was interactive:

<!-- Instead of this (render-blocking) -->
<script src="https://analytics.example.com/script.js"></script>

<!-- Do this (deferred) -->
<script>
  // Load analytics after the page is interactive
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      const script = document.createElement('script');
      script.src = 'https://analytics.example.com/script.js';
      document.head.appendChild(script);
    });
  } else {
    // Fallback for browsers without requestIdleCallback
    window.addEventListener('load', () => {
      setTimeout(() => {
        const script = document.createElement('script');
        script.src = 'https://analytics.example.com/script.js';
        document.head.appendChild(script);
      }, 2000);
    });
  }
</script>

Tree Shaking in Practice

If you’re using a bundler (Vite, webpack, esbuild), make sure tree shaking actually works. I found that importing an entire library when I only needed one function was a common problem:

// BAD: Imports entire lodash (70KB)
import _ from 'lodash';
_.debounce(fn, 300);

// GOOD: Import only what you need (1KB)
import debounce from 'lodash/debounce';
debounce(fn, 300);

// BETTER: Use native (0KB)
let timeoutId;
function debounce(fn, delay) {
  clearTimeout(timeoutId);
  timeoutId = setTimeout(fn, delay);
}

After removing unused JavaScript, my Time to Interactive dropped from 5.8s to 2.1s.

Caching Strategies That Actually Work

Caching is where most developers get confused. Here’s the simple mental model:

The Cache-First Approach (For Static Assets)

For assets that rarely change (fonts, CSS, images), cache aggressively:

// Service Worker caching for static assets
const STATIC_CACHE = 'static-v3';

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE).then((cache) => {
      return cache.addAll([
        '/css/global.css',
        '/fonts/inter-var.woff2',
        '/js/main.js',
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // Return cached version, fetch in background to update
      if (response) {
        fetch(event.request).then((freshResponse) => {
          caches.open(STATIC_CACHE).then((cache) => {
            cache.put(event.request, freshResponse);
          });
        });
        return response;
      }
      return fetch(event.request);
    })
  );
});

The Stale-While-Revalidate Pattern

For content that changes occasionally (blog posts, user profiles):

// Next.js revalidation
export const revalidate = 3600; // Revalidate every hour

// Or on-demand revalidation
export async function POST(request) {
  const { tag } = await request.json();
  revalidateTag(tag);
  return Response.json({ revalidated: true });
}

// Fetch with cache tags
const post = await fetch('/api/posts/123', {
  next: { tags: ['post-123'] },
});

CDN Caching Headers

Set the right Cache-Control headers for different asset types:

# Static assets (hashed filenames like main.a1b2c3.js)
Cache-Control: public, max-age=31536000, immutable

# HTML pages (need fresh content)
Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400

# API responses
Cache-Control: private, no-cache

Layout Shift Prevention

Layout shifts (CLS) are the most frustrating user experience issue. Users click something, the page shifts, and they click the wrong button. Here’s how I eliminated CLS to 0:

Always Set Image Dimensions

Never load an image without explicit width and height:

<!-- BAD: Image loads and pushes content down -->
<img src="photo.jpg" alt="Photo" />

<!-- GOOD: Browser reserves space immediately -->
<img src="photo.jpg" alt="Photo" width="800" height="600" />

<!-- For dynamically sized images, use aspect-ratio -->
<style>
  .aspect-ratio-16-9 {
    aspect-ratio: 16 / 9;
    width: 100%;
    object-fit: cover;
  }
</style>
<img class="aspect-ratio-16-9" src="photo.jpg" alt="Photo" />

Font Loading Strategy

Web fonts cause layout shifts when they load. Use font-display: optional for body text:

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-display: optional; /* Use system font if not cached */
  unicode-range: U+0000-00FF;
}

/* For critical UI elements, use swap */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-bold.woff2') format('woff2');
  font-display: swap; /* Show system font, then swap */
}

The Results

After all optimizations, here’s the before/after comparison:

MetricBeforeAfter
Lighthouse Score7498
LCP4.2s1.1s
CLS0.180.01
TTI5.8s1.9s
Total Page Weight4.2MB890KB

The Tools I Use

Performance optimization is a continuous process. These are the tools I check weekly:

  • Lighthouse CI: Automate performance checks on every deploy
  • Chrome DevTools Coverage: Find unused JavaScript
  • WebPageTest: Real-world performance from different locations
  • Core Web Vitals Report in Search Console: See how real users experience your site

Conclusion

Web performance isn’t about chasing a perfect Lighthouse score — it’s about respecting your users’ time and bandwidth. Every 100ms of latency costs you conversions. The techniques above aren’t theoretical — they’re the exact changes I made to cut my page weight by 79% and load time by 68%.

Start with images (biggest impact), then tackle JavaScript (second biggest), then fine-tune caching. Don’t try to optimize everything at once — measure, fix one thing, measure again. That’s the only way to make real progress.