What Is INP and Why Did It Replace FID?
Interaction to Next Paint (INP) is one of Google's three Core Web Vitals — the metrics that directly influence your search rankings and measure real user experience. INP measures how fast your page responds to user interactions — every click, tap, and key press throughout the entire page visit.
In March 2024, INP officially replaced First Input Delay (FID) as a Core Web Vital. Why? FID only measured the delay before the browser started processing the very first interaction. It missed two critical problems: slow processing time (the actual work the browser does) and presentation delay (time to paint the result). It also ignored every interaction after the first one.
INP fixes all of that. It tracks every interaction and measures the full round trip — from the moment a user clicks to the moment the screen updates. Each interaction has three phases:
Phase 1
Input Delay
Time between the user's click/tap and when the browser starts running event handlers. Caused by other JavaScript blocking the main thread.
Phase 2
Processing Time
Time spent running your event handler code. Heavy DOM manipulation, synchronous operations, and complex calculations slow this phase.
Phase 3
Presentation Delay
Time for the browser to recalculate styles, perform layout, and paint the visual update. Large DOMs and layout thrashing slow this phase.
INP reports the worst interaction (technically the 98th percentile) across the entire page visit. This means even one slow interaction — like a sluggish "Add to Cart" button or a laggy filter dropdown — can fail your INP score.
< 200ms
Good
200–500ms
Needs Improvement
> 500ms
Poor
Why INP matters for your Shopify store: A slow-responding store feels broken. When a customer taps "Add to Cart" and nothing happens for 400ms, they tap again — and sometimes end up with two items or navigate away entirely. Poor INP hurts both your SEO rankings and your conversion rate. Want to see where your store stands? Run a free speed test.
The Easy Fix: Automatic INP Optimization
Before diving into manual JavaScript optimization — there's a faster approach. Thunder Page Speed Optimizer automatically addresses several of the most common causes of high INP on Shopify stores.
How Thunder reduces INP:
Smart Script Deferral
Defers non-critical JavaScript so it doesn't block the main thread during user interactions
Dependency-Aware Loading
Loads scripts in the correct order without creating main-thread contention that delays interactions
Third-Party Script Management
Prevents app scripts from running heavy code during critical interaction windows
Main Thread Protection
Reduces total blocking time (TBT), which directly correlates with better INP scores
Average improvement: +27 PageSpeed points
Thunder addresses INP alongside LCP and CLS — most stores see all three Core Web Vitals improve within minutes of enabling optimizations.
Free plan available · No credit card required · 30-second setup · Works with all themes
How to Measure INP on Your Shopify Store
Before fixing INP, you need to identify which interactions are slow and why. Here are the best tools:
PageSpeed Insights (Start Here)
Go to pagespeed.web.dev or use our free Shopify speed test. Check both sections:
- Field data — Real user INP from Chrome users over 28 days. This is what Google uses for rankings.
- Lab data — Shows Total Blocking Time (TBT), which correlates with INP. Lab tests can't measure INP directly because they don't simulate real user interactions.
Look for diagnostics like "Minimize main-thread work" and "Reduce JavaScript execution time" — these directly relate to INP.
Chrome DevTools Performance Panel (Best for Debugging)
Open DevTools (F12), go to the Performance tab, check "Web Vitals," and record while you interact with the page. The Interactions track shows every interaction with color coding:
- Green — Good (under 200ms)
- Yellow — Needs improvement (200–500ms)
- Red — Poor (over 500ms)
Click any interaction to see the three phases (input delay, processing time, presentation delay) broken down individually. This tells you exactly where the bottleneck is.
Google Search Console
Under Experience → Core Web Vitals, you'll see INP performance across your entire site. Since INP replaced FID in March 2024, this report now shows INP data from real users. Pages are grouped by status (Good / Needs Improvement / Poor). Note: Search Console uses a 28-day rolling average, so improvements take about a month to fully reflect.
Web Vitals Extension (Quick Check)
Install the Web Vitals Chrome extension for a real-time INP overlay as you browse your store. It shows your INP score updating with every interaction — great for quickly identifying which buttons, links, or form elements are slow.
JavaScript Performance Observer (Advanced)
Paste this into your browser console while interacting with your store to log every interaction's duration:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const duration = entry.duration;
const status = duration < 200 ? '🟢' : duration < 500 ? '🟡' : '🔴';
console.log(
`${status} INP candidate: ${duration.toFixed(0)}ms`,
`| Input delay: ${entry.inputDelay?.toFixed(0) ?? '?'}ms`,
`| Processing: ${entry.processingDuration?.toFixed(0) ?? '?'}ms`,
`| Presentation: ${(duration - (entry.inputDelay ?? 0) - (entry.processingDuration ?? 0)).toFixed(0)}ms`,
`| Target: ${entry.target?.tagName ?? 'unknown'}`
);
}
}).observe({ type: 'event', buffered: true, durationThreshold: 16 }); This logs the three phases of each interaction so you can pinpoint whether the problem is input delay (main thread blocked), processing time (slow event handler), or presentation delay (expensive rendering).
Common Causes of High INP on Shopify
Before jumping into fixes, understand what's causing high INP on your store. Here are the most common culprits, ranked by how frequently they affect Shopify stores:
🔴 Heavy JavaScript Event Handlers
Click handlers that perform DOM manipulation, API calls, or complex calculations synchronously. Common in cart drawers, quick-view modals, and product variant selectors.
🔴 Third-Party App Scripts
Apps that add event listeners to common interactions — analytics tracking on every click, review widgets that process on scroll, chat widgets that intercept clicks. These run in addition to your theme's handlers.
🟠 Large DOM Size
Shopify stores with 1,500+ DOM elements (common with mega menus, product grids, and footer link lists) make every layout recalculation slower. The presentation delay phase grows with DOM complexity.
🟠 Layout Thrashing
JavaScript that reads layout properties (offsetHeight, getBoundingClientRect) then writes to the DOM in a loop. Each read forces the browser to recalculate layout synchronously, blocking the main thread.
🟡 Synchronous DOM Operations
Event handlers that update many DOM elements at once (updating prices across a page, re-rendering a product grid after filtering). Without yielding, these block the main thread until complete.
Most Shopify stores have a combination of these issues. The good news: even fixing one or two of the top causes can dramatically improve your INP score. Let's walk through the fixes. For more background on how these issues relate to overall store speed, see our complete Shopify speed optimization guide.
Fix #1: Break Up Long JavaScript Tasks
The browser's main thread is single-threaded — it can only do one thing at a time. When JavaScript runs for more than 50ms without yielding, it becomes a "long task" that blocks user interactions. If a user clicks while a long task is running, the browser can't respond until the task finishes — directly increasing input delay.
Use scheduler.yield() (Recommended)
The modern way to break up long tasks is scheduler.yield(). It pauses your code, lets the browser handle pending interactions and rendering, then resumes:
// Before: One long task blocking the main thread
async function processProducts(products) {
for (const product of products) {
updateProductCard(product); // DOM updates
calculatePricing(product); // Heavy computation
renderReviews(product); // More DOM work
}
}
// After: Yields between iterations so browser stays responsive
async function processProducts(products) {
for (const product of products) {
updateProductCard(product);
calculatePricing(product);
renderReviews(product);
// Yield to the browser between each product
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
}
}
} Fallback: setTimeout Pattern
For browsers that don't support scheduler.yield() yet, use the setTimeout(0) pattern:
function yieldToMain() {
return new Promise(resolve => setTimeout(resolve, 0));
}
async function processProducts(products) {
for (let i = 0; i < products.length; i++) {
updateProductCard(products[i]);
// Yield every 5 iterations to balance throughput and responsiveness
if (i % 5 === 0) {
await yieldToMain();
}
}
} Use requestAnimationFrame for Visual Updates
When your event handler needs to update the UI, wrap visual changes in requestAnimationFrame to batch them with the browser's rendering cycle:
// Bad: DOM updates interleaved with logic
button.addEventListener('click', () => {
const data = computeExpensiveData();
element.style.height = data.height + 'px'; // Forces layout
element.textContent = data.label;
element.classList.add('active');
});
// Good: Separate computation from rendering
button.addEventListener('click', () => {
const data = computeExpensiveData();
requestAnimationFrame(() => {
element.style.height = data.height + 'px';
element.textContent = data.label;
element.classList.add('active');
});
}); Fix #2: Optimize Event Handlers
Every event handler that runs during a user interaction adds to the processing time phase of INP. The goal: make your handlers as fast as possible, and defer anything that isn't immediately visible.
Debounce Rapid-Fire Events
Events like scroll, resize, and input fire many times per second. Without debouncing, each event runs your handler — creating a backlog of long tasks:
// Bad: Fires on every keystroke in search
searchInput.addEventListener('input', (e) => {
fetchSearchResults(e.target.value); // API call on every keypress!
renderSuggestions();
});
// Good: Debounce to max once per 300ms
let debounceTimer;
searchInput.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
fetchSearchResults(e.target.value);
renderSuggestions();
}, 300);
}); Defer Non-Visual Work
When a user clicks "Add to Cart," they need to see feedback immediately — but analytics tracking, inventory checks, and recommendation updates can happen after the visual update:
addToCartButton.addEventListener('click', async () => {
// 1. Visual feedback FIRST (what the user needs to see)
showCartAnimation();
updateCartCount();
// 2. Defer non-visual work
requestIdleCallback(() => {
trackAnalyticsEvent('add_to_cart');
updateRecommendations();
syncInventory();
});
}); Use Event Delegation
Instead of attaching individual click handlers to dozens of product cards, use event delegation with a single handler on the parent container:
// Bad: 50 individual handlers on a collection page
document.querySelectorAll('.product-card').forEach(card => {
card.addEventListener('click', handleProductClick);
});
// Good: One handler via event delegation
document.querySelector('.product-grid').addEventListener('click', (e) => {
const card = e.target.closest('.product-card');
if (card) handleProductClick(card);
}); Fix #3: Reduce DOM Size
Every element in your DOM makes layout recalculations slower. When the browser needs to update the screen after an interaction (the presentation delay phase), it must recalculate styles and layout for potentially thousands of elements. Google recommends keeping your DOM under 1,400 elements — many Shopify stores have 3,000+.
Audit Your DOM Size
Check your current DOM size in Chrome DevTools console:
console.log('DOM elements:', document.querySelectorAll('*').length);
console.log('Max depth:', document.querySelector('[data-max-depth]')?.dataset.maxDepth);
// Find the heaviest sections
document.querySelectorAll('section, div[class]').forEach(el => {
const count = el.querySelectorAll('*').length;
if (count > 100) console.log(count, el.className || el.tagName);
}); Common DOM Bloat Sources on Shopify
- Mega menus — Often render all dropdown content even when hidden. Use lazy rendering or
content-visibility: autofor off-screen sections. - Product variant selectors — Stores with 50+ variants may render hidden option elements for each one. Load variants on demand.
- Footer link lists — Footer mega-links with dozens of columns add hundreds of DOM elements below the fold.
- Hidden mobile/desktop duplicates — Themes that render both mobile and desktop navigation (hiding one with CSS) double the DOM elements.
Use content-visibility for Off-Screen Content
/* Skip rendering for sections below the fold */
.product-recommendations,
.footer-mega-links,
.recently-viewed {
content-visibility: auto;
contain-intrinsic-size: 0 500px; /* Estimated height */
} This CSS property tells the browser to skip rendering work for off-screen elements. When the user scrolls near them, the browser renders them just in time. This directly improves the presentation delay phase of INP because there are fewer elements to recalculate during interactions. For more on reducing layout-related issues, see our CLS guide.
Fix #4: Eliminate Layout Thrashing
Layout thrashing happens when JavaScript alternates between reading and writing DOM layout properties in a loop. Each read forces the browser to synchronously recalculate layout — and if you write immediately after, the next read forces another recalculation.
// Bad: Layout thrashing — forces layout recalc on EVERY iteration
function resizeCards() {
const cards = document.querySelectorAll('.product-card');
cards.forEach(card => {
const height = card.offsetHeight; // READ (forces layout)
card.style.minHeight = height + 'px'; // WRITE (invalidates layout)
// Next iteration's read forces ANOTHER layout recalculation
});
}
// Good: Batch reads, then batch writes
function resizeCards() {
const cards = document.querySelectorAll('.product-card');
// Phase 1: Read all values
const heights = Array.from(cards).map(card => card.offsetHeight);
// Phase 2: Write all values (only one layout recalc)
cards.forEach((card, i) => {
card.style.minHeight = heights[i] + 'px';
});
} Properties that trigger forced layout: offsetHeight, offsetWidth, getBoundingClientRect(), scrollTop, clientHeight, and getComputedStyle(). When any of these are read after a DOM write, the browser must recalculate layout synchronously.
Audit your theme's JavaScript for these patterns — they're especially common in product grid layouts, sticky headers, and infinite scroll implementations.
Fix #5: Tame Third-Party Scripts
Third-party scripts are the silent INP killers on Shopify stores. Each app you install can add JavaScript that runs on every page load and — more critically — on every user interaction. Analytics scripts that track clicks, review widgets that process on hover, and chat widgets that intercept events all compete for the main thread.
Audit Your Third-Party Script Impact
In Chrome DevTools, record a Performance trace while interacting with your page. In the Main thread flame chart, look for long tasks from third-party domains. You can also use the Network panel → filter by "JS" → sort by size to see which scripts are loading:
- Analytics — Google Analytics, Meta Pixel, TikTok Pixel often add event listeners
- Reviews — Yotpo, Judge.me, Loox can process on scroll and click events
- Chat — Tidio, Zendesk, Gorgias intercept click events page-wide
- Upsell/Cross-sell — ReConvert, Bold apps often add handlers to cart interactions
Manual Approach: Defer with Interaction Trigger
Load non-critical third-party scripts only after the first user interaction, when the page is already responsive:
<script>
// Load non-critical scripts after first user interaction
const loadDeferredScripts = () => {
// Analytics
const ga = document.createElement('script');
ga.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXX';
ga.async = true;
document.head.appendChild(ga);
// Chat widget
const chat = document.createElement('script');
chat.src = 'https://cdn.chatwidget.com/widget.js';
chat.async = true;
document.head.appendChild(chat);
// Remove listeners after loading
['click', 'scroll', 'keydown', 'touchstart'].forEach(event =>
document.removeEventListener(event, loadDeferredScripts)
);
};
['click', 'scroll', 'keydown', 'touchstart'].forEach(event =>
document.addEventListener(event, loadDeferredScripts, { once: false, passive: true })
);
</script> This pattern works but is fragile — you need to maintain the script list manually, handle dependencies between scripts, and ensure nothing breaks when scripts load out of order. This is exactly what Thunder automates: it identifies which scripts are safe to defer, manages their dependencies, and loads them in the optimal order. Read more about managing third-party scripts on Shopify.
Advanced: Web Workers and Code Splitting
For stores with complex custom functionality — advanced product configurators, real-time pricing calculations, or client-side search — these advanced techniques can dramatically reduce processing time.
Move Heavy Computation to Web Workers
Web Workers run JavaScript on a separate thread, completely independent of the main thread. This means heavy computation won't block user interactions:
// pricing-worker.js — runs on a separate thread
self.addEventListener('message', (e) => {
const { products, discountRules } = e.data;
const calculated = products.map(p => ({
...p,
finalPrice: applyDiscountRules(p, discountRules),
savings: calculateSavings(p, discountRules),
}));
self.postMessage(calculated);
});
// main.js — keeps the main thread free
const pricingWorker = new Worker('/pricing-worker.js');
variantSelector.addEventListener('change', () => {
// Show loading state immediately (fast — on main thread)
showPriceLoading();
// Offload heavy calculation to worker (won't block interactions)
pricingWorker.postMessage({
products: getSelectedProducts(),
discountRules: window.discountRules,
});
});
pricingWorker.addEventListener('message', (e) => {
// Update UI with results (fast — just DOM updates)
updatePriceDisplay(e.data);
}); Code Splitting for Theme JavaScript
Don't load all JavaScript upfront. Split code so each page only loads what it needs:
// Instead of loading everything in theme.js:
// import './product-zoom.js';
// import './cart-drawer.js';
// import './mega-menu.js';
// import './search-autocomplete.js';
// Load only when needed:
if (document.querySelector('.product-media')) {
import('./product-zoom.js');
}
if (document.querySelector('.cart-drawer')) {
import('./cart-drawer.js');
}
// Or load on first interaction:
document.querySelector('.search-input')?.addEventListener('focus', () => {
import('./search-autocomplete.js').then(mod => mod.init());
}, { once: true }); Dynamic imports reduce the initial JavaScript payload, which means less main thread blocking during the critical early moments when users are most likely to interact. This also reduces the impact of render-blocking resources on your store.
Manual INP Fixes vs Thunder: Side-by-Side Comparison
Here's how the manual approach compares to letting Thunder handle INP optimization automatically:
| INP Fix | Manual Approach | Thunder Approach |
|---|---|---|
| Script deferral | Write custom defer logic, manage script dependencies manually | Automatic dependency-aware deferral with one click |
| Third-party scripts | Audit each app's JS, build custom loading triggers | Identifies and defers non-critical app scripts automatically |
| Main thread blocking | Refactor event handlers, add yield points, use Web Workers | Reduces blocking by deferring heavy scripts away from interaction windows |
| Long tasks | Profile, identify, and break up each long task individually | Prevents most long tasks by controlling script execution timing |
| Skill level | Advanced JavaScript profiling and optimization skills required | No coding required — one-click install |
| Time to implement | 4–16 hours depending on store complexity | 30 seconds |
| Maintenance | Must re-audit after adding apps or updating themes | Adapts automatically to store changes |
Manual fixes give you granular control but demand significant JavaScript expertise and ongoing maintenance. Thunder handles the most impactful optimizations — particularly script deferral and third-party management — automatically. Many developers combine both: install Thunder for the heavy lifting, then apply manual fixes for custom code bottlenecks. See Thunder pricing plans.
Frequently Asked Questions
What is a good INP score for Shopify stores?
Google considers INP under 200ms as 'good,' 200–500ms as 'needs improvement,' and over 500ms as 'poor.' Most well-optimized Shopify stores can achieve INP under 200ms on desktop and under 300ms on mobile. INP replaced First Input Delay (FID) as a Core Web Vital in March 2024, and it's a more comprehensive measure of interactivity because it tracks all interactions throughout the page lifecycle, not just the first one.
What replaced FID as a Core Web Vital?
Interaction to Next Paint (INP) officially replaced First Input Delay (FID) as a Core Web Vital in March 2024. While FID only measured the delay before the browser started processing the very first interaction, INP measures the full responsiveness of every interaction (clicks, taps, key presses) throughout the entire page visit. This makes INP a much more accurate measure of how responsive your Shopify store actually feels to real users.
Why is my Shopify store's INP high on mobile but fine on desktop?
Mobile devices have significantly less processing power than desktops. Google's lab testing throttles CPU by 4x to simulate mid-range mobile devices, and real mobile users often have even less capable hardware. Heavy JavaScript that runs fine on a desktop can block the main thread for hundreds of milliseconds on mobile. Third-party app scripts, complex event handlers, and large DOM sizes all hit harder on mobile. Focus on reducing JavaScript execution time and breaking up long tasks.
Can third-party Shopify apps cause high INP?
Yes — third-party apps are one of the biggest causes of high INP on Shopify stores. Apps that add click handlers (quick-view modals, cart drawers, wishlist buttons), inject analytics tracking on every interaction, or run heavy JavaScript on user events can significantly delay the browser's ability to paint the next frame. Thunder helps by deferring non-critical app scripts and managing their loading sequence so they don't block the main thread during interactions.
How do I measure INP on my Shopify store?
Use Google PageSpeed Insights (pagespeed.web.dev) for both lab and field INP data. For real-time debugging, open Chrome DevTools, go to the Performance panel, enable 'Web Vitals,' and interact with your page — the Interactions track shows each interaction's processing time. Google Search Console's Core Web Vitals report shows INP across your entire site using real user data. You can also use our free speed test at thunderpagespeed.com/tools/speed-test/ for a quick check.
What is scheduler.yield() and how does it help INP?
scheduler.yield() is a newer browser API that lets you break up long JavaScript tasks by yielding control back to the browser's main thread. When you call await scheduler.yield() inside a function, the browser can process pending user interactions and paint updates before your code continues. This prevents long tasks from blocking responsiveness. It's supported in Chrome 129+ and can be polyfilled for older browsers. It's the recommended replacement for older patterns like setTimeout(0).
Does fixing INP improve Shopify SEO rankings?
Yes. INP is one of Google's three Core Web Vitals (alongside LCP and CLS) that serve as ranking signals. Pages with 'good' INP scores (under 200ms) receive a ranking advantage in Google Search. Beyond SEO, responsive interactions directly impact conversions — studies show that every 100ms of interaction delay can reduce conversion rates by up to 7%. A fast-responding store feels more professional and trustworthy to shoppers.