Available for day contractsFrom 21st September I have availability for day and half day contracts. Please contact for more information.

Contact →
mikepreston.org

Web Performance Optimisation

Comprehensive guide to optimising web application performance across loading, rendering, and runtime.

Web Performance Optimisation

Comprehensive guide to optimising web application performance across loading, rendering, and runtime.

Overview

Web performance optimisation focuses on reducing page load times, improving render performance, and enhancing user experience through efficient resource delivery and code execution. Key metrics include First Contentful Paint (FCP), Largest Contentful Paint (LCP), Time to Interactive (TTI), and Cumulative Layout Shift (CLS).

HTMLCSSJavaScriptUser RequestDNS LookupTCP ConnectionServer ProcessingResource DownloadResource TypeParse HTMLParse CSSParse & Execute JSBuild DOMBuild CSSOMRender TreeLayoutPaintCompositeFirst ContentfulPaintHTMLCSSJavaScriptUser RequestDNS LookupTCP ConnectionServer ProcessingResource DownloadResource TypeParse HTMLParse CSSParse & Execute JSBuild DOMBuild CSSOMRender TreeLayoutPaintCompositeFirst ContentfulPaint

Critical Rendering Path

The critical rendering path is the sequence of steps browsers take to convert HTML, CSS, and JavaScript into rendered pixels.

Understanding the Rendering Pipeline

RenderJSCSSHTMLBrowserRenderJSCSSHTMLBrowserRequest & ParseDOM TreeRequest & ParseCSSOM TreeRequest & ExecuteDOM/CSSOM UpdatesCombine DOM + CSSOMRender TreeLayoutPaintCompositeRenderJSCSSHTMLBrowserRenderJSCSSHTMLBrowserRequest & ParseDOM TreeRequest & ParseCSSOM TreeRequest & ExecuteDOM/CSSOM UpdatesCombine DOM + CSSOMRender TreeLayoutPaintComposite

Optimising Critical Path

<!-- Inline critical CSS -->
<style>
  /* Above-the-fold styles */
  .hero { background: #333; color: #fff; padding: 2rem; }
  .header { position: fixed; top: 0; width: 100%; }
</style>

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

<!-- Async non-critical JavaScript -->
<script src="/scripts/analytics.js" async></script>

<!-- Defer JavaScript that modifies DOM -->
<script src="/scripts/app.js" defer></script>

Resource Prioritisation

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

<!-- DNS prefetch for future navigation -->
<link rel="dns-prefetch" href="https://api.example.com">

<!-- Preload critical resources -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/images/hero.jpg" as="image">

<!-- Prefetch resources for next page -->
<link rel="prefetch" href="/page2.html">

<!-- Preload an ES module and its dependency graph -->
<link rel="modulepreload" href="/scripts/app.mjs">

<!-- Prerender a likely next page (use the Speculation Rules API; -->
<!-- <link rel="prerender"> is deprecated and Chrome-only) -->
<script type="speculationrules">
{
  "prerender": [{ "urls": ["/checkout"] }]
}
</script>

Caching Strategies

Effective caching reduces server load and improves perceived performance.

Browser Caching

# nginx configuration
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

location ~* \.(html)$ {
    expires -1;
    add_header Cache-Control "no-cache, no-store, must-revalidate";
}

location /api/ {
    expires -1;
    add_header Cache-Control "no-store, private";
}

Cache-Control Headers

// Express.js examples
const express = require('express');
const app = express();

// Static assets with versioning
app.use('/static', express.static('public', {
    maxAge: '365d',
    immutable: true
}));

// API responses with ETag
app.get('/api/data', (req, res) => {
    const data = getData();
    const etag = generateETag(data);

    res.set({
        'Cache-Control': 'public, max-age=300, must-revalidate',
        'ETag': etag
    });

    if (req.headers['if-none-match'] === etag) {
        return res.status(304).end();
    }

    res.json(data);
});

// HTML pages with validation
app.get('/', (req, res) => {
    res.set({
        'Cache-Control': 'public, max-age=0, must-revalidate',
        'Last-Modified': new Date().toUTCString()
    });
    res.sendFile('index.html');
});

Service Worker Caching

// service-worker.js
const CACHE_VERSION = 'v1';
const CACHE_STATIC = `static-${CACHE_VERSION}`;
const CACHE_DYNAMIC = `dynamic-${CACHE_VERSION}`;

// Install event - cache static assets
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(CACHE_STATIC).then(cache => {
            return cache.addAll([
                '/',
                '/styles/main.css',
                '/scripts/app.js',
                '/images/logo.svg',
                '/offline.html'
            ]);
        })
    );
});

// Activate event - clean old caches
self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(keys => {
            return Promise.all(
                keys
                    .filter(key => key !== CACHE_STATIC && key !== CACHE_DYNAMIC)
                    .map(key => caches.delete(key))
            );
        })
    );
});

// Fetch event - network first, falling back to cache
self.addEventListener('fetch', event => {
    if (event.request.url.includes('/api/')) {
        // Network first for API calls
        event.respondWith(
            fetch(event.request)
                .then(response => {
                    const responseClone = response.clone();
                    caches.open(CACHE_DYNAMIC).then(cache => {
                        cache.put(event.request, responseClone);
                    });
                    return response;
                })
                .catch(() => caches.match(event.request))
        );
    } else {
        // Cache first for static assets
        event.respondWith(
            caches.match(event.request)
                .then(response => response || fetch(event.request))
                .catch(() => caches.match('/offline.html'))
        );
    }
});

CDN Configuration

// CloudFront cache behaviour example
{
    "ViewerProtocolPolicy": "redirect-to-https",
    "AllowedMethods": ["GET", "HEAD", "OPTIONS"],
    "CachedMethods": ["GET", "HEAD"],
    "Compress": true,
    "DefaultTTL": 86400,
    "MaxTTL": 31536000,
    "MinTTL": 0,
    "ForwardedValues": {
        "QueryString": false,
        "Cookies": {
            "Forward": "none"
        },
        "Headers": ["Origin", "Access-Control-Request-Method"]
    }
}
HitMissUserCDN CacheReturn CachedOrigin ServerProcess RequestReturn ResponseCache at CDNHitMissUserCDN CacheReturn CachedOrigin ServerProcess RequestReturn ResponseCache at CDN

Minification and Bundling

Reduce file sizes and HTTP requests through minification and bundling.

JavaScript Minification

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    mode: 'production',
    optimization: {
        minimize: true,
        minimizer: [
            new TerserPlugin({
                terserOptions: {
                    compress: {
                        drop_console: true,
                        drop_debugger: true,
                        pure_funcs: ['console.log']
                    },
                    mangle: {
                        safari10: true
                    },
                    output: {
                        comments: false,
                        ascii_only: true
                    }
                },
                extractComments: false
            })
        ],
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    priority: 10
                },
                common: {
                    minChunks: 2,
                    priority: 5,
                    reuseExistingChunk: true
                }
            }
        }
    }
};

CSS Optimisation

// postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer'),
        require('cssnano')({
            preset: ['default', {
                discardComments: {
                    removeAll: true
                },
                normalizeWhitespace: true,
                colormin: true,
                minifyFontValues: true,
                minifySelectors: true
            }]
        }),
        require('@fullhuman/postcss-purgecss')({
            content: ['./src/**/*.html', './src/**/*.js'],
            defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || []
        })
    ]
};

Tree Shaking

// package.json
{
    "sideEffects": [
        "*.css",
        "*.scss"
    ]
}

// Import only what you need
import { debounce, throttle } from 'lodash-es';  // Good
// import _ from 'lodash';  // Bad - imports everything

// ES6 modules for tree shaking
export const util1 = () => {};
export const util2 = () => {};

// Use named imports
import { util1 } from './utils';  // Good
// import * as utils from './utils';  // Bad

Code Splitting

// Dynamic imports for route-based splitting
const Home = () => import('./pages/Home.vue');
const About = () => import('./pages/About.vue');
const Dashboard = () => import('./pages/Dashboard.vue');

const routes = [
    { path: '/', component: Home },
    { path: '/about', component: About },
    { path: '/dashboard', component: Dashboard }
];

// Webpack magic comments
import(/* webpackChunkName: "charts" */ './charts').then(module => {
    const { renderChart } = module;
    renderChart(data);
});

// Conditional loading
if (condition) {
    import(/* webpackChunkName: "heavy-feature" */ './heavy-feature')
        .then(module => module.init());
}

Lazy Loading Techniques

Load resources only when needed to improve initial page load time.

Image Lazy Loading

<!-- Native lazy loading -->
<img src="image.jpg"
     alt="Description"
     loading="lazy"
     width="800"
     height="600">

<!-- Responsive images with lazy loading -->
<img srcset="small.jpg 400w,
             medium.jpg 800w,
             large.jpg 1200w"
     sizes="(max-width: 600px) 400px,
            (max-width: 1000px) 800px,
            1200px"
     src="medium.jpg"
     alt="Description"
     loading="lazy">
// Intersection Observer for custom lazy loading
const imageObserver = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            img.srcset = img.dataset.srcset || '';
            img.classList.remove('lazy');
            img.classList.add('loaded');
            observer.unobserve(img);
        }
    });
}, {
    rootMargin: '50px 0px',
    threshold: 0.01
});

document.querySelectorAll('img.lazy').forEach(img => {
    imageObserver.observe(img);
});

Component Lazy Loading

// React lazy loading
import React, { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
    return (
        <Suspense fallback={<div>Loading...</div>}>
            <HeavyComponent />
        </Suspense>
    );
}

// Vue 3 lazy loading
import { defineAsyncComponent } from 'vue';

export default {
    components: {
        AsyncComponent: defineAsyncComponent({
            loader: () => import('./AsyncComponent.vue'),
            loadingComponent: LoadingSpinner,
            delay: 200,
            timeout: 3000
        })
    }
};

Lazy Loading Third-Party Scripts

// Load analytics only when needed
function loadAnalytics() {
    if (window.ga) return;

    const script = document.createElement('script');
    script.async = true;
    script.src = 'https://www.google-analytics.com/analytics.js';
    document.head.appendChild(script);
}

// Load on user interaction
document.addEventListener('scroll', loadAnalytics, { once: true });
document.addEventListener('click', loadAnalytics, { once: true });

// Or delay until page is idle
if ('requestIdleCallback' in window) {
    requestIdleCallback(loadAnalytics, { timeout: 2000 });
} else {
    setTimeout(loadAnalytics, 2000);
}

Content Visibility API

/* Defer rendering of off-screen content */
.section {
    content-visibility: auto;
    contain-intrinsic-size: 0 500px;
}

/* Skip rendering until needed */
.hidden-section {
    content-visibility: hidden;
}

Image Optimisation

Optimise images for faster loading and better performance.

Responsive Images

<!-- Art direction with picture element -->
<picture>
    <source media="(max-width: 799px)"
            srcset="mobile-image.webp"
            type="image/webp">
    <source media="(max-width: 799px)"
            srcset="mobile-image.jpg">
    <source media="(min-width: 800px)"
            srcset="desktop-image.webp"
            type="image/webp">
    <source media="(min-width: 800px)"
            srcset="desktop-image.jpg">
    <img src="desktop-image.jpg" alt="Description">
</picture>

<!-- Density descriptors for retina displays -->
<img srcset="image-1x.jpg 1x,
             image-2x.jpg 2x,
             image-3x.jpg 3x"
     src="image-1x.jpg"
     alt="Description">

Modern Image Formats

<!-- WebP with fallback -->
<picture>
    <source srcset="image.avif" type="image/avif">
    <source srcset="image.webp" type="image/webp">
    <img src="image.jpg" alt="Description">
</picture>

Image Compression

// Sharp (Node.js) for server-side processing
const sharp = require('sharp');

async function optimiseImage(inputPath, outputPath) {
    await sharp(inputPath)
        .resize(1200, null, {
            withoutEnlargement: true,
            fit: 'inside'
        })
        .webp({ quality: 85 })
        .toFile(outputPath);
}

// Generate multiple sizes
async function generateResponsiveImages(input) {
    const sizes = [400, 800, 1200, 1600];

    await Promise.all(
        sizes.map(width =>
            sharp(input)
                .resize(width)
                .webp({ quality: 85 })
                .toFile(`output-${width}.webp`)
        )
    );
}

CSS Background Images

/* Responsive background images */
.hero {
    background-image: url('hero-small.jpg');
}

@media (min-width: 768px) {
    .hero {
        background-image: url('hero-medium.jpg');
    }
}

@media (min-width: 1200px) {
    .hero {
        background-image: url('hero-large.jpg');
    }
}

/* Modern format with fallback */
.hero {
    background-image: url('hero.jpg');
    background-image: image-set(
        url('hero.avif') type('image/avif'),
        url('hero.webp') type('image/webp'),
        url('hero.jpg') type('image/jpeg')
    );
}

Blur-up Technique

<!-- Low quality image placeholder -->
<div class="image-wrapper">
    <img class="placeholder"
         src="tiny-blurred-10x10.jpg"
         alt="Description">
    <img class="full-image"
         data-src="full-quality.jpg"
         alt="Description">
</div>
.image-wrapper {
    position: relative;
    overflow: hidden;
}

.placeholder {
    filter: blur(20px);
    transform: scale(1.1);
    transition: opacity 0.3s;
}

.full-image {
    position: absolute;
    top: 0;
    left: 0;
    opacity: 0;
    transition: opacity 0.3s;
}

.full-image.loaded {
    opacity: 1;
}

.full-image.loaded ~ .placeholder {
    opacity: 0;
}

Performance Monitoring

Measure and monitor performance to identify bottlenecks.

Web Vitals

// Using web-vitals library (v4+ API)
// The get*() functions were removed in v4; FID was retired in favour of INP.
import { onCLS, onINP, onFCP, onLCP, onTTFB } from 'web-vitals';

function sendToAnalytics({ name, value, id, delta }) {
    // Send to analytics endpoint
    navigator.sendBeacon('/analytics', JSON.stringify({
        metric: name,
        value: Math.round(value),
        id,
        delta: Math.round(delta)
    }));
}

onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onFCP(sendToAnalytics);
onLCP(sendToAnalytics);
onTTFB(sendToAnalytics);

Performance Observer API

// Monitor long tasks
const longTaskObserver = new PerformanceObserver(list => {
    for (const entry of list.getEntries()) {
        if (entry.duration > 50) {
            console.warn('Long task detected:', {
                duration: entry.duration,
                startTime: entry.startTime,
                name: entry.name
            });
        }
    }
});

longTaskObserver.observe({ entryTypes: ['longtask'] });

// Monitor resource timing
const resourceObserver = new PerformanceObserver(list => {
    for (const entry of list.getEntries()) {
        if (entry.duration > 1000) {
            console.warn('Slow resource:', {
                name: entry.name,
                duration: entry.duration,
                size: entry.transferSize,
                type: entry.initiatorType
            });
        }
    }
});

resourceObserver.observe({ entryTypes: ['resource'] });

// Monitor navigation timing
const navigationObserver = new PerformanceObserver(list => {
    const entry = list.getEntries()[0];
    console.log('Navigation metrics:', {
        dns: entry.domainLookupEnd - entry.domainLookupStart,
        tcp: entry.connectEnd - entry.connectStart,
        ttfb: entry.responseStart - entry.requestStart,
        download: entry.responseEnd - entry.responseStart,
        domInteractive: entry.domInteractive,
        domComplete: entry.domComplete,
        loadComplete: entry.loadEventEnd
    });
});

navigationObserver.observe({ entryTypes: ['navigation'] });

Custom Performance Marks

// Mark important events
performance.mark('component-init-start');
await initializeComponent();
performance.mark('component-init-end');

// Measure duration
performance.measure(
    'component-initialization',
    'component-init-start',
    'component-init-end'
);

// Get measurements
const measures = performance.getEntriesByType('measure');
measures.forEach(measure => {
    console.log(`${measure.name}: ${measure.duration}ms`);
});

// Clear marks when done
performance.clearMarks();
performance.clearMeasures();

Performance Budget

// webpack-bundle-analyzer configuration
const BundleBudgetPlugin = require('webpack-bundle-budget-plugin');

module.exports = {
    plugins: [
        new BundleBudgetPlugin({
            budgets: [
                {
                    resourceType: 'script',
                    maximumSize: 250 * 1024  // 250 KB
                },
                {
                    resourceType: 'style',
                    maximumSize: 100 * 1024  // 100 KB
                },
                {
                    resourceType: 'image',
                    maximumSize: 500 * 1024  // 500 KB
                }
            ]
        })
    ]
};
Under BudgetOver BudgetBuild ProcessCheck Bundle SizeDeployAlert TeamAnalyse BundleOptimise CodeUnder BudgetOver BudgetBuild ProcessCheck Bundle SizeDeployAlert TeamAnalyse BundleOptimise Code

Performance Monitoring Tools

Lighthouse CI

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push, pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install dependencies
        run: npm ci
      - name: Build
        run: npm run build
      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v9
        with:
          urls: |
            http://localhost:3000
            http://localhost:3000/about
          budgetPath: ./budget.json
          uploadArtifacts: true
// budget.json
[
    {
        "path": "/*",
        "timings": [
            {
                "metric": "interactive",
                "budget": 3000
            },
            {
                "metric": "first-contentful-paint",
                "budget": 1500
            }
        ],
        "resourceSizes": [
            {
                "resourceType": "script",
                "budget": 300
            },
            {
                "resourceType": "total",
                "budget": 1000
            }
        ]
    }
]

Real User Monitoring (RUM)

// Custom RUM implementation
class RUMTracker {
    constructor(endpoint) {
        this.endpoint = endpoint;
        this.metrics = {};
        this.init();
    }

    init() {
        // Capture navigation timing
        window.addEventListener('load', () => {
            const navTiming = performance.getEntriesByType('navigation')[0];
            this.metrics.navigation = {
                dns: navTiming.domainLookupEnd - navTiming.domainLookupStart,
                tcp: navTiming.connectEnd - navTiming.connectStart,
                ttfb: navTiming.responseStart - navTiming.requestStart,
                download: navTiming.responseEnd - navTiming.responseStart,
                domInteractive: navTiming.domInteractive,
                domComplete: navTiming.domComplete
            };

            this.send();
        });

        // Capture web vitals
        this.observeWebVitals();

        // Capture errors
        this.observeErrors();
    }

    observeWebVitals() {
        import('web-vitals').then(({ onCLS, onINP, onLCP }) => {
            onCLS(metric => this.metrics.cls = metric.value);
            onINP(metric => this.metrics.inp = metric.value);
            onLCP(metric => this.metrics.lcp = metric.value);
        });
    }

    observeErrors() {
        window.addEventListener('error', event => {
            this.metrics.errors = this.metrics.errors || [];
            this.metrics.errors.push({
                message: event.message,
                filename: event.filename,
                line: event.lineno,
                column: event.colno
            });
        });
    }

    send() {
        const data = {
            ...this.metrics,
            url: window.location.href,
            userAgent: navigator.userAgent,
            timestamp: Date.now()
        };

        navigator.sendBeacon(this.endpoint, JSON.stringify(data));
    }
}

// Initialize tracker
const rum = new RUMTracker('/api/metrics');

Common Bottlenecks and Solutions

Render-Blocking Resources

<!-- Problem: Blocking CSS -->
<link rel="stylesheet" href="styles.css">

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

<!-- Problem: Blocking JavaScript -->
<script src="app.js"></script>

<!-- Solution: Defer or async -->
<script src="app.js" defer></script>

Layout Thrashing

// Problem: Forced synchronous layout
function badResize() {
    elements.forEach(el => {
        const width = el.offsetWidth;  // Read
        el.style.width = width * 2 + 'px';  // Write
        const height = el.offsetHeight;  // Read - forces layout
        el.style.height = height * 2 + 'px';  // Write
    });
}

// Solution: Batch reads and writes
function goodResize() {
    // Batch all reads
    const dimensions = elements.map(el => ({
        width: el.offsetWidth,
        height: el.offsetHeight
    }));

    // Batch all writes
    elements.forEach((el, i) => {
        el.style.width = dimensions[i].width * 2 + 'px';
        el.style.height = dimensions[i].height * 2 + 'px';
    });
}

// Or use requestAnimationFrame
function optimisedResize() {
    if (resizeScheduled) return;
    resizeScheduled = true;

    requestAnimationFrame(() => {
        goodResize();
        resizeScheduled = false;
    });
}

Memory Leaks

// Problem: Event listeners not cleaned up
class Component {
    constructor() {
        window.addEventListener('resize', this.handleResize);
    }

    handleResize() {
        // Handle resize
    }
}

// Solution: Clean up in destructor
class Component {
    constructor() {
        this.handleResize = this.handleResize.bind(this);
        window.addEventListener('resize', this.handleResize);
    }

    handleResize() {
        // Handle resize
    }

    destroy() {
        window.removeEventListener('resize', this.handleResize);
    }
}

// Problem: Detached DOM nodes
let cache = [];
function addToCache(element) {
    cache.push(element);  // Element never garbage collected
}

// Solution: Use WeakMap or WeakSet
const cache = new WeakMap();
function addToCache(element, data) {
    cache.set(element, data);  // Automatically garbage collected
}

Excessive Repaints

/* Problem: Animating layout properties */
.box {
    transition: width 0.3s;
}

.box:hover {
    width: 200px;  /* Triggers layout */
}

/* Solution: Animate transform instead */
.box {
    transition: transform 0.3s;
}

.box:hover {
    transform: scaleX(2);  /* GPU accelerated */
}

/* Use will-change for complex animations */
.animated {
    will-change: transform, opacity;
}

/* Promote to own layer */
.gpu-accelerated {
    transform: translateZ(0);
    /* or */
    will-change: transform;
}

Large DOM Trees

// Problem: Rendering large lists
function renderList(items) {
    return items.map(item => `<li>${item}</li>`).join('');
}

// Solution: Virtual scrolling
class VirtualList {
    constructor(container, items, rowHeight) {
        this.container = container;
        this.items = items;
        this.rowHeight = rowHeight;
        this.visibleRows = Math.ceil(container.clientHeight / rowHeight);
        this.init();
    }

    init() {
        this.container.style.height = this.items.length * this.rowHeight + 'px';
        this.container.addEventListener('scroll', () => this.render());
        this.render();
    }

    render() {
        const scrollTop = this.container.scrollTop;
        const startIndex = Math.floor(scrollTop / this.rowHeight);
        const endIndex = startIndex + this.visibleRows + 1;

        const visibleItems = this.items.slice(startIndex, endIndex);
        const offset = startIndex * this.rowHeight;

        this.container.innerHTML = `
            <div style="padding-top: ${offset}px">
                ${visibleItems.map(item => `<div style="height: ${this.rowHeight}px">${item}</div>`).join('')}
            </div>
        `;
    }
}

JavaScript Bundle Size

// Problem: Importing entire libraries
import _ from 'lodash';

// Solution: Import only what you need
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';

// Or use native alternatives
const debounce = (fn, delay) => {
    let timeoutId;
    return (...args) => {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => fn(...args), delay);
    };
};

// Dynamic imports for heavy dependencies
async function loadHeavyFeature() {
    const module = await import(/* webpackChunkName: "heavy" */ './heavy-module');
    module.init();
}

Quick Reference

Performance Checklist

Optimisation Impact Effort
Enable compression (gzip/Brotli) High Low
Optimise images (WebP, AVIF) High Medium
Implement lazy loading High Low
Add caching headers High Low
Minify CSS/JS Medium Low
Code splitting High Medium
Use CDN High Low
Defer non-critical JS High Low
Inline critical CSS Medium Medium
Remove unused CSS Medium Medium
Optimise web fonts Medium Low
Enable HTTP/2 Medium Low
Implement service worker High High
Reduce third-party scripts High Medium

Core Web Vitals Targets

INP (Interaction to Next Paint) replaced FID as a Core Web Vital on 12 March 2024.

Metric Good Needs Improvement Poor
LCP (Largest Contentful Paint) ≤ 2.5s 2.5s - 4s > 4s
INP (Interaction to Next Paint) ≤ 200ms 200ms - 500ms > 500ms
CLS (Cumulative Layout Shift) ≤ 0.1 0.1 - 0.25 > 0.25

HTTP Cache Headers

# Static assets (versioned)
Cache-Control: public, max-age=31536000, immutable

# Static assets (unversioned)
Cache-Control: public, max-age=86400, must-revalidate

# HTML pages
Cache-Control: public, max-age=0, must-revalidate

# API responses (cacheable)
Cache-Control: public, max-age=300, must-revalidate

# API responses (private)
Cache-Control: private, max-age=300, must-revalidate

# Never cache
Cache-Control: no-store, no-cache, must-revalidate

Resource Hints Priority

<!-- Highest priority -->
<link rel="preconnect" href="https://cdn.example.com">

<!-- High priority -->
<link rel="preload" href="critical.css" as="style">

<!-- Medium priority -->
<link rel="dns-prefetch" href="https://analytics.example.com">

<!-- Low priority -->
<link rel="prefetch" href="next-page.html">

<!-- Prerender: use the Speculation Rules API -->
<!-- (<link rel="prerender"> is deprecated and Chrome-only) -->
<script type="speculationrules">
{ "prerender": [{ "urls": ["likely-next.html"] }] }
</script>

Common Issues and Solutions

Issue Symptoms Solution
Slow initial load High FCP/LCP times Optimise critical rendering path, reduce bundle size
Layout shifts High CLS score Reserve space for dynamic content, use aspect ratios
Janky scrolling Low frame rate Use CSS transforms, avoid layout thrashing
Memory leaks Increasing memory usage Clean up event listeners, use WeakMap/WeakSet
Large bundle size Slow download times Code splitting, tree shaking, dynamic imports
Slow API responses High TTFB Implement server-side caching, use CDN
Render blocking Delayed FCP Defer/async scripts, load critical CSS inline
Too many requests Network waterfall Bundle resources, use HTTP/2, implement caching
Large images Slow LCP Optimise formats (WebP/AVIF), lazy load, responsive images
Third-party scripts Slow TTI Load asynchronously, delay until interaction

Debugging Performance Issues

// Measure function performance
console.time('myFunction');
myFunction();
console.timeEnd('myFunction');

// Profile with Performance API
performance.mark('start');
await heavyOperation();
performance.mark('end');
performance.measure('heavyOperation', 'start', 'end');

const measure = performance.getEntriesByName('heavyOperation')[0];
console.log(`Operation took ${measure.duration}ms`);

// Monitor FPS
let lastTime = performance.now();
let frames = 0;

function measureFPS() {
    const currentTime = performance.now();
    frames++;

    if (currentTime >= lastTime + 1000) {
        const fps = Math.round((frames * 1000) / (currentTime - lastTime));
        console.log(`FPS: ${fps}`);
        frames = 0;
        lastTime = currentTime;
    }

    requestAnimationFrame(measureFPS);
}

measureFPS();

// Detect long tasks
if ('PerformanceObserver' in window) {
    const observer = new PerformanceObserver(list => {
        for (const entry of list.getEntries()) {
            console.warn('Long task detected:', {
                name: entry.name,
                duration: entry.duration,
                startTime: entry.startTime
            });
        }
    });

    observer.observe({ entryTypes: ['longtask'] });
}

Testing Tools

# Lighthouse audit
npx lighthouse https://example.com --view

# WebPageTest
curl "https://www.webpagetest.org/runtest.php?url=https://example.com&k=API_KEY"

# Chrome DevTools Coverage
# Open DevTools > More Tools > Coverage > Record

# Bundle analysis
npx webpack-bundle-analyzer dist/stats.json