I traced 38 Shopify PDPs in 2025 looking for the apps that tank Interaction to Next Paint (INP). The same 7 apps show up in 24 of 38 audits, contributing 60-220ms each to long-task time on a mid-tier Android. Stack 3+ on one template and p75 INP slips from green into the needs-improvement band. The fix is the same per app: defer the script bundle, gate hydration behind IntersectionObserver, schedule init in requestIdleCallback. Below is the per-app pattern.
TL;DR: 7 apps account for most Shopify INP failures I see: Klaviyo (~80KB), Yotpo (~120KB), Rebuy (~140KB), Loop (~38-72KB), Postscript (~52KB), Hotjar (~95KB), Tidio (~180KB). Each registers handlers on the main thread. The fix per app is the same pattern: defer the bundle, wrap hydration in IntersectionObserver + requestIdleCallback. Field p75 INP drops 200-400ms within 28 days of shipping.
Why this matters for your store
- INP replaced First Input Delay as a Core Web Vital in March 2024. Google ranks on the 28-day CrUX p75 field value, not the lab Lighthouse score.
- 52% of Shopify stores fail INP on mobile at the 75th percentile (Web Almanac 2024 analysis). The structural cause is theme + apps stacking handlers on the same main thread.
- Each blocked interaction costs conversion. A 612ms INP on the variant swatch means a customer taps, sees nothing for 2/3 of a second, taps again. The double-tap fires twice in your analytics; the customer leaves.
How third-party app JavaScript actually breaks INP
A Shopify theme renders its own JS on every PDP: variant picker, cart drawer, predictive search, image gallery. That alone is manageable. Each installed app then injects more behavior: click handlers for chat icons, mutation observers for review widgets, resize listeners for sticky CTAs, polling intervals for cart updates.
By the time a customer taps the variant swatch on an iPhone 12 over 4G, three or four scripts compete for the same main thread tick. The slowest handler wins; everyone else waits. INP measures that wait, end to end, from input to the next paint that reflects the result.
The fix is not “remove all apps.” Most apps drive measurable revenue. The fix is making sure no single app’s hydration competes with the customer’s first interaction. For the full INP triage playbook, see my Shopify INP fix case study where a real Plus store went from p75 INP 612ms to 178ms in 5 working days.
The 7 apps that show up in 24 of 38 audits
Klaviyo onsite (~80KB synchronous)
Klaviyo’s onsite tracker injects via klaviyo.js and fires on DOMContentLoaded. The script registers an IntersectionObserver on every form for signup events, polls the cart for abandoned-cart triggers, and writes 3-4 cookies. Long-task contribution: 40-80ms on Android mid-tier.
Fix:
{%- comment -%} Defer Klaviyo until after first paint {%- endcomment -%}
<script>
if (document.readyState === 'complete') {
loadKlaviyo();
} else {
window.addEventListener('load', loadKlaviyo);
}
function loadKlaviyo() {
requestIdleCallback(function () {
var s = document.createElement('script');
s.src = 'https://static.klaviyo.com/onsite/js/klaviyo.js?company_id={{ settings.klaviyo_id }}';
s.async = true;
document.head.appendChild(s);
}, { timeout: 2000 });
}
</script>
Cost: signup forms hydrate ~500ms later than default. Customers below the fold never notice. Above-the-fold signup forms should preload only the form widget, not the full tracker.
Yotpo reviews (~120KB widget hydration)
Yotpo’s review widget hydrates on visibility but ships the full reviews bundle on first paint. Even the default ?async=true install registers handlers eagerly. Long-task contribution: 60-110ms per PDP load.
Fix: lazy-mount the widget container only when scrolled into view:
// theme/assets/yotpo-deferred.js
const reviewSlot = document.querySelector('.yotpo-reviews-slot');
if (reviewSlot) {
const io = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
const script = document.createElement('script');
script.src = 'https://staticw2.yotpo.com/{{ settings.yotpo_app_key }}/widget.js';
script.async = true;
document.head.appendChild(script);
io.disconnect();
}
}, { rootMargin: '300px' });
io.observe(reviewSlot);
}
The 300px rootMargin starts the load before the widget is visible, so it appears hydrated by the time the customer scrolls down.
Rebuy AI upsells (~140KB)
Rebuy’s recommendation engine fires personalization queries on every cart open and PDP load. The hydration step parses recommendations and builds DOM nodes synchronously. Long-task contribution: 90-150ms on cart drawer open.
Fix: delay recommendation fetch until after the cart drawer opens, not on page load:
// Listen for cart drawer open instead of pre-fetching
document.addEventListener('cart:drawer-opened', function () {
if (!window.rebuyLoaded) {
requestIdleCallback(function () {
const script = document.createElement('script');
script.src = 'https://rebuyengine.com/api/v1/widget.js?shop={{ shop.permanent_domain }}';
script.async = true;
document.head.appendChild(script);
window.rebuyLoaded = true;
});
}
});
Cost: first cart drawer open is ~300ms slower than default. Every subsequent open is faster because Rebuy stays loaded.
Loop Subscriptions widget (~38-72KB)
Loop’s subscription widget weighs ~38KB on Growth tier, ~72KB on Enterprise. Hydrates on DOMContentLoaded and registers a MutationObserver on the PDP form. Long-task contribution: 30-60ms.
Fix: Loop, Skio, and Recharge all expose selling_plan_allocations cleanly through Shopify’s native Liquid API, so you can render the selector server-side and skip the app’s widget entirely on first paint:
{%- assign current_variant = product.selected_or_first_available_variant -%}
{%- if current_variant.selling_plan_allocations.size > 0 -%}
<fieldset class="subscription-options">
{%- for allocation in current_variant.selling_plan_allocations -%}
<label>
<input type="radio" name="selling_plan" value="{{ allocation.selling_plan.id }}">
{{ allocation.selling_plan.name }} - {{ allocation.price | money }}
</label>
{%- endfor -%}
</fieldset>
{%- endif -%}
The widget then hydrates only for the upsell logic (frequency picker, savings copy), which can defer behind IntersectionObserver. For the full Recharge vs Skio vs Loop comparison and integration patterns, see my Shopify subscription apps comparison.
Postscript SMS tracking (~52KB)
Postscript’s tracking SDK registers a MutationObserver on the cart and fires on every variant change. Long-task contribution: 25-50ms.
Fix: Postscript supports a defer flag in the official install snippet. Most merchants miss it. Update the install:
<script defer src="https://sdk.postscript.io/sdk.bundle.js?shopId={{ settings.postscript_shop_id }}"></script>
If you also use Postscript’s keyword opt-in widget, gate it behind a click handler on the trigger button rather than pre-rendering it.
Hotjar session recording (~95KB)
Hotjar’s recorder polls the DOM, captures mouse events, and uploads sessions. Even with sampling at 10%, the recorder script ships in full. Long-task contribution: 50-100ms.
Fix: Hotjar lets you set a sampling rate and a delay. Set both aggressively:
<script>
window.hjSettings = { hjid: HJID, hjsv: 6 };
window._hjSettings = { ...window.hjSettings, hjssr: 0.1 }; // 10% session rate
setTimeout(function () {
var s = document.createElement('script');
s.async = true;
s.src = 'https://static.hotjar.com/c/hotjar-' + HJID + '.js?sv=6';
document.head.appendChild(s);
}, 3000); // 3 second delay after page load
</script>
3 seconds is enough that the customer’s first interactions complete before Hotjar starts recording. The lost first 3 seconds rarely contain anything useful in session replay anyway.
Tidio chat (~180KB on hydration)
Tidio loads a lightweight bootstrap on initial paint but hydrates a 180KB chat widget the moment a user is detected. Long-task contribution: 80-220ms once the chat icon appears.
Fix: delay the chat icon itself behind interaction. Tidio supports a JavaScript API to defer the full widget:
<script>
window.tidioChatApi = { delay: 5000 }; // 5s before icon appears
setTimeout(function () {
var s = document.createElement('script');
s.async = true;
s.src = '//code.tidio.co/{{ settings.tidio_chat_id }}.js';
document.head.appendChild(s);
}, 2500);
</script>
If chat is not a measurable conversion driver, uninstall instead. 30-day analysis of chat engagement separates “we need this” from “we think we need this.”
How to verify the fix in 5 minutes
Three checks per deferred app.
- Chrome DevTools Performance trace. Mobile emulation, 4x CPU throttle, slow 4G. Record an interaction trace tapping the variant swatch, opening cart drawer, typing in search. Long-task purple bars should drop from 80-200ms to under 50ms per handler.
- Lighthouse lab INP. Run PageSpeed Insights on a top PDP. The lab INP score (TBT proxy) should drop within minutes of the deploy. This is the leading indicator.
- CrUX field p75 INP. Pull from Google PageSpeed Insights field data section, or from CrUX BigQuery export. Wait 28 days for the rolling window to refresh fully. Day 5 looks like the fix did nothing because the metric still reflects pre-fix data; the trend appears around day 10 and confirms by day 28.
For the broader Core Web Vitals optimization playbook including LCP and CLS patterns, see my Shopify Core Web Vitals optimization 2026 and the Shopify CrUX Grader tool which pulls your store’s real CrUX data directly.
When deferring is not enough
Some apps cannot be deferred without breaking the customer experience. The 3 categories:
- Consent banners (Cookiebot, Klaviyo consent, etc.) must fire before tracking. Lazy-loading them breaks GDPR compliance.
- A/B test variant selectors (Intelligems, GrowthBook) must execute synchronously to assign the variant before render, or the customer sees a flash of the wrong variant.
- Geo-redirect logic must run synchronously to send EU customers to the right currency before paint.
For these, the fix is removing the app entirely if it is not earning its cost, or routing the logic server-side via Liquid where possible. Klaviyo consent specifically can move to Shopify Customer Privacy API instead of Klaviyo’s client-side handler.
The takeaway
- Audit which apps fire on first paint. Open DevTools Network tab, filter by JS, count third-party scripts. 6+ is a red flag.
- Defer Klaviyo, Postscript, Hotjar, Tidio with the script-tag patterns above. Each is a 5-minute Liquid edit.
- Lazy-mount Yotpo and Rebuy via IntersectionObserver. Widgets below the fold should never hydrate at first paint.
- Server-render subscription selectors (Loop, Skio, Recharge) using Shopify’s native
selling_plan_allocationsLiquid object instead of the app widget. - Validate the fix in lab Lighthouse INP within an hour of deploy; confirm in CrUX p75 28 days later. The lab moves immediately; the field metric is a rolling window.