PWA Performance — Optimizing Load, Caching, and Runtime
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
- Not measuring before optimizing. Optimizing without data is guesswork. Always run Lighthouse before and after changes to measure impact.
- 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.
- Caching too much dynamic data. Caching 1000 API responses fills storage and slows cache lookups. Set cache limits and use TTL-based eviction.
- Large initial JavaScript bundle. Every KB of JavaScript delays Time to Interactive. Aim for under 100KB of initial JS. Split the rest.
- 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
- What are the key performance metrics for PWAs and their target values?
- How do you measure cache hit rate and why does it matter?
- What techniques reduce JavaScript bundle size in PWAs?
- How do responsive images improve PWA performance?
- 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
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