INP is Core Web Vitals' most important metric for PWA install trust
Users don't install apps that feel slow. INP measures the real responsiveness that matters for install decisions.
Why INP matters more than LCP for installed PWAs
Largest Contentful Paint (LCP) measures loading speed. Interaction to Next Paint (INP) measures interaction speed. For an installed PWA, users have already "paid" the cost of loading—they expect fast responses from there. A high INP score feels like the app is "broken" or "low-quality," directly impacting install retention and word-of-mouth recommendations.
According to web.dev's Core Web Vitals guidance, INP replaced First Input Delay (FID) as the responsiveness metric because FID only measured the first interaction—now it's consistent responsiveness.
What INP actually measures
INP captures the longest interaction delay from:
- User tap/button press
- Main thread starts processing
- Visual feedback renders on screen
This includes all three phases:
- Input delay: Main thread blocked when user acts
- Processing time: Event handlers, JavaScript execution
- Presentation delay: Browser rendering the visual change
Good INP: < 100ms Needs improvement: 100ms - 200ms Poor: > 200ms
Why high INP breaks PWA trust
Think about user expectations:
- Native app: Button tap → visual feedback in ~16ms (1 frame)
- Slow web: Button tap → spinner, delay, render (~800ms)
- Good PWA: Button tap → feedback within 100ms (close enough to native)
When users install "native-like" web apps, they subconsciously benchmark against actual native apps. High INP makes the PWA feel "cheap" or "broken," leading to:
- Immediate uninstall: "This isn't really a native app"
- Negative reviews: "Slow," "unresponsive," "glitchy"
- Refusal to reinstall: "This brand's web app is low quality"
The PWA-specific INP traps
PWAs have higher INP risk than regular web pages:
| Risk Factor | Why PWAs are vulnerable | Fix | |-------------|-------------------------|-----| | Offline-first architectures | IndexedDB reads can block main thread | Use IDB Keyval pattern or background sync | | Service worker lifecycle | Activation race conditions delay events | Pre-warm SW, fallback to main thread | | Manifest screenshot generation | Time-consuming render before install | Pre-generate, use simpler formats | | Large JavaScript bundles | Installed apps don't have perfect caching | Code-split, lazy-load routes | | Platform detection code | Feature checks run on every interaction | Cache detection results in module scope |
Practical INP optimization for PWAs
1. Pre-fuel JavaScript execution
// Before install button shows
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
// Cache user capabilities
const supportsFileAPI = 'showOpenFilePicker' in window;
const supportsClipboard = 'clipboard' in navigator;
}2. Schedule expensive work after interaction
// Install button handler (fast!)
button.onclick = () => {
// Immediate feedback: show loading spinner
showLoadingSpinner();
// Expensive work: schedule after frame
requestAnimationFrame(() => {
setTimeout(async () => {
await prewarmServiceWorker();
await cacheOfflineAssets();
await verifyInstallCriteria();
}, 0);
});
};3. Avoid layout thrashing
// Bad: Read-write-read-write pattern
const height = el.clientHeight; // Force layout
el.style.height = height + 10 + 'px';
const width = el.clientWidth; // Force layout again
// Good: Batch reads, then batch writes
const heights = elements.map(el => el.clientHeight);
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px';
});4. Use Web Workers for heavy lifting
// Don't block main thread
const worker = new Worker('/offline-validator.js');
worker.postMessage({ action: 'validate-manifest' });
worker.onmessage = (event) => {
if (event.data.valid) showInstallButton();
};Platform-specific INP behaviors
iOS Safari PWA
- WebView has higher scheduling latency (~20-30ms baseline)
- IndexedDB reads serially, can block main thread
- Fix: Use IndexedDB Keyval pattern for small reads
Android Chrome PWA
- Lower baseline latency (~10-15ms)
- V8 optimizes hot paths better
- Fix: Keep interaction code in main bundle, avoid dynamic imports for hot paths
Desktop PWAs
- Mouse vs touch: Mouse has ~8ms latency vs touch ~30ms
- Window resize events can spike INP
- Fix: Throttle resize, use ResizeObserver instead
Measurement workflow
Chrome DevTools Performance panel:
- Load your PWA
- Open Performance panel
- Start recording
- Tap a button/interaction 5-10 times
- Stop recording
- Look for "INP" metric or long tasks (>50ms)
Automated testing:
// Lighthouse CI + INP check
await lighthouse('https://pwa.example.com', {
onlyCategories: ['performance'],
thresholds: {
'interaction-to-next-paint': 100 // ms
}
});INP vs. other metrics: what to report
| Metric | For PWAs, focus on this if... | |--------|-------------------------------| | INP | User clicks, taps, form submits | | LCP | First load, deep links | | CLS | Dynamic content, animations, carousels | | FID | Older browsers (Chrome 95 and below) |
When good enough is actually good enough
INP targeting depends on PWA category:
- E-commerce PWA: Aim for <100ms (users abandon slow carts)
- Media PWA: 150ms acceptable (less per-interaction demand)
- Utility PWA: <200ms tolerable (fewer interactions, more task-based)
- Social PWA: <80ms ideal (continuous interaction loops)
Next step
Measure your PWA's current INP with DevTools, then schedule expensive work (service worker prewarming, cache warming) away from user interactions using requestAnimationFrame + setTimeout batching.