Skip to content

PWA Performance — Optimizing Load, Caching, and Runtime

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about PWA Performance. We cover key concepts, practical examples, and best practices to help you master this topic.

PWA performance optimization focuses on instant loading via caching, minimizing JavaScript execution, optimizing images, and measuring with Lighthouse audits to deliver app-like speed.

What You'll Learn

By the end of this tutorial, you will understand key PWA performance metrics, how to optimize caching strategies, how to reduce JavaScript payload, how to optimize images, and how to measure performance with Lighthouse.

Why It Matters

PWA users expect instant loads. A 1-second delay in load time reduces conversions by 20%. PWAs compete with native apps that load in under 500ms. Performance optimization is not optional — it is the defining feature that makes users choose your PWA over competitors.

Real-World Use

Twitter Lite optimized their PWA by reducing JavaScript from 1MB to 400KB, implementing Code Splitting, and using aggressive caching. Result: Time to Interactive dropped from 23 seconds to 5 seconds on slow 3G. User engagement increased by 65%.

Performance Metrics

Critical PWA Performance Metrics
    ┌──────────────────────────────────────────────────────────────┐
    │                   Core Web Vitals & PWA Metrics             │
    ├──────────────────────────────────────────────────────────────┤
    │  LCP (Largest Contentful Paint)    Target: < 2.5s           │
    │  FID (First Input Delay)           Target: < 100ms           │
    │  CLS (Cumulative Layout Shift)     Target: < 0.1             │
    │  TTI (Time to Interactive)         Target: < 5s              │
    │  FCP (First Contentful Paint)      Target: < 1.8s            │
    │  TBT (Total Blocking Time)         Target: < 200ms           │
    │  Offline load time                 Target: < 1s              │
    └──────────────────────────────────────────────────────────────┘

Think of performance metrics like a car's dashboard. LCP is the speedometer (how fast the car accelerates). FID is the steering response (how quickly the car turns). CLS is the suspension (how stable the ride is). A good dashboard helps you drive better.

Measuring Performance

// Using Performance API in your PWA
function measureLoadTime() {
    const perfEntries = performance.getEntriesByType('navigation');

    if (perfEntries.length > 0) {
        const nav = perfEntries[0];
        console.log('Performance Metrics:');
        console.log('  DOM Content Loaded:', nav.domContentLoadedEventEnd.toFixed(0), 'ms');
        console.log('  Load Event:', nav.loadEventEnd.toFixed(0), 'ms');
        console.log('  DOM Interactive:', nav.domInteractive.toFixed(0), 'ms');
        console.log('  Total Page Load:', nav.loadEventEnd.toFixed(0), 'ms');
    }
}

// Measure from service worker
self.addEventListener('activate', event => {
    // Time service worker activation
    const startTime = performance.now();

    event.waitUntil(
        caches.keys().then(() => {
            const activationTime = performance.now() - startTime;
            console.log('SW activation time:', activationTime.toFixed(0), 'ms');
        })
    );
});

Output:

Performance Metrics:
  DOM Content Loaded: 342 ms
  Load Event: 891 ms
  DOM Interactive: 310 ms
  Total Page Load: 891 ms

Optimizing Cache Hit Rate

Maximize cache hits to reduce network dependency:

// sw.js — Measure cache hit rate
let totalRequests = 0;
let cacheHits = 0;

self.addEventListener('fetch', event => {
    totalRequests++;

    event.respondWith(
        caches.match(event.request).then(cached => {
            if (cached) {
                cacheHits++;
                return cached;
            }
            return fetch(event.request).then(response => {
                if (response.ok) {
                    caches.open('dynamic-cache').then(cache => {
                        cache.put(event.request, response.clone());
                    });
                }
                return response;
            });
        })
    );
});

// Log cache hit rate periodically
self.addEventListener('message', event => {
    if (event.data.type === 'GET_CACHE_STATS') {
        const hitRate = totalRequests > 0
            ? ((cacheHits / totalRequests) * 100).toFixed(1)
            : 0;

        console.log(`Cache hit rate: ${hitRate}% (${cacheHits}/${totalRequests})`);

        event.ports[0].postMessage({
            totalRequests,
            cacheHits,
            hitRate
        });
    }
});

JavaScript Bundle Optimization

// Before optimization: single large bundle
// import everything at once

// After optimization: code splitting
// main.js
async function loadFeature(featureName) {
    switch (featureName) {
        case 'dashboard':
            const dashboard = await import('./features/dashboard.js');
            dashboard.init();
            break;
        case 'charts':
            const { Chart } = await import('./features/chart.js');
            new Chart(document.getElementById('chart'));
            break;
        case 'editor':
            const editor = await import('./features/editor.js');
            editor.init(document.getElementById('editor'));
            break;
    }
}

// Load features lazily based on user interaction
document.getElementById('show-chart').addEventListener('click', () => {
    loadFeature('charts');
});

Image Optimization for PWAs

// Generate responsive image sources
const IMAGE_CONFIG = {
    quality: 80,
    formats: ['webp', 'avif'],
    sizes: [
        { width: 320, label: 'small' },
        { width: 640, label: 'medium' },
        { width: 960, label: 'large' },
        { width: 1280, label: 'xlarge' }
    ]
};

// Lazy load images with Intersection Observer
function initLazyImages() {
    const images = document.querySelectorAll('img[data-src]');

    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                const src = img.dataset.src;
                const srcset = generateSrcset(src);

                img.src = src;
                img.srcset = srcset;
                img.removeAttribute('data-src');
                observer.unobserve(img);
            }
        });
    }, { rootMargin: '200px' });

    images.forEach(img => observer.observe(img));
}

function generateSrcset(baseUrl) {
    return IMAGE_CONFIG.sizes
        .map(size => `${baseUrl}?w=${size.width} ${size.width}w`)
        .join(', ');
}

App Shell Loading Optimization

// Optimize app shell delivery
// 1. Inline critical CSS in the HTML head
// 2. Defer non-critical JavaScript
// 3. Preload critical fonts
// 4. Use resource hints

// In index.html:
// <link rel="preload" href="/fonts/inter.woff2" as="font" crossorigin>
// <link rel="preload" href="/scripts/main.js" as="script">
// <link rel="preconnect" href="https://api.example.com">

Common Mistakes

  1. Not measuring before optimizing. Optimizing without data is guesswork. Always run Lighthouse before and after changes to measure impact.
  2. Ignoring the service worker startup cost. Service worker startup adds ~50-100ms to the first request. Measure and minimize this by keeping the service worker lean.
  3. Caching too much dynamic data. Caching 1000 API responses fills storage and slows cache lookups. Set cache limits and use TTL-based eviction.
  4. Large initial JavaScript bundle. Every KB of JavaScript delays Time to Interactive. Aim for under 100KB of initial JS. Split the rest.
  5. Not optimizing for the critical rendering path. CSS blocks rendering. Inline critical CSS and defer the rest. JavaScript blocks Parsing. Defer non-critical scripts.

Practice Questions

  1. What are the key performance metrics for PWAs and their target values?
  2. How do you measure cache hit rate and why does it matter?
  3. What techniques reduce JavaScript bundle size in PWAs?
  4. How do responsive images improve PWA performance?
  5. What is the critical rendering path and how do you optimize it?

Challenge: Run a Lighthouse audit on a PWA you have built or a public PWA. Identify the three lowest-scoring performance metrics. Implement optimizations for each and re-audit. Document before/after scores.

FAQ

What is a good Lighthouse PWA score?

Aim for 90+ on all categories. Google uses Lighthouse scores as a ranking signal. A score below 50 indicates serious issues that affect user experience.

How much JavaScript is too much?

Keep the initial JavaScript bundle under 100KB (compressed). Total page JavaScript under 300KB. Every 100KB of JavaScript adds ~1 second to Time to Interactive on mobile.

Does PWA performance differ from regular web performance?

PWA performance adds the dimension of offline and cached performance. Regular web performance focuses on first load. PWA performance includes subsequent loads, cache hit rates, and service worker overhead.

How often should I run performance audits?

Run a full Lighthouse audit every week and after every significant deployment. Set up Lighthouse CI to catch regressions automatically.

What is the single highest-impact performance optimization for PWAs?

Pre-caching the app shell. This ensures the UI loads instantly on every subsequent visit, reducing perceived load time from seconds to milliseconds.

Mini Project

Optimize a PWA for performance: run an initial Lighthouse audit and record scores. Implement: app shell pre-caching, code splitting (split JS into 3+ lazy-loaded chunks), responsive images with WebP format, inline critical CSS, and deferred JavaScript. Re-audit and compare before/after scores. Target: 90+ on all Lighthouse categories.

What's Next

Performance is optimized. Now learn about PWA testing with Lighthouse — auditing your PWA against Google's quality checklist.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro