Shopify Third-Party Script Defer: 7 Apps That Tank INP (And the Fix Per App)

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.

  1. 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.
  2. 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.
  3. 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_allocations Liquid 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.

Frequently Asked Questions

Which Shopify apps tank INP the most in 2026?

The 7 most common INP killers I see in 2026 audits are Klaviyo (synchronous onsite tracking ~80KB), Yotpo (review widget hydration ~120KB), Rebuy (upsell engine ~140KB), Loop Subscriptions (subscription widget ~38-72KB depending on plan), Postscript (SMS tracking ~52KB), Hotjar (session recording ~95KB), and Tidio chat (lazy-loaded but heavy ~180KB once hydrated). Each registers click handlers, mutation observers, or resize listeners on the main thread. Stacking 3+ on the same template pushes p75 INP from green (under 200ms) to needs-improvement (200-500ms) on Android.

How do I defer a Shopify app's JavaScript without breaking it?

Three layers in order. First, wrap the script tag with `defer` or `async` attributes to delay execution until after first paint. Second, gate the script behind an IntersectionObserver so it only loads when the relevant UI element scrolls into view (a reviews widget below the fold does not need to hydrate at first paint). Third, use `requestIdleCallback` to schedule the actual initialization for idle main thread time. The pattern works across all major Shopify app types but breaks if the app expects synchronous DOM-ready execution; verify by testing add-to-cart, variant change, and search interactions after the defer.

Will deferring app scripts hurt my conversion rate?

Usually no, often the opposite. Apps that fire before first paint compete with hero image rendering and variant picker hydration, both of which directly impact bounce. Deferring the apps frees the main thread for the interactions that actually drive add-to-cart. The exception is apps that need to run before the customer sees the page (consent banner, geo-redirect, A/B test variant selection). For those, lazy loading breaks the UX. For tracking, reviews, chat, and upsells, deferring is a CRO win not a CRO risk.

How do I measure INP improvement after deferring app scripts?

Field data via CrUX is the only metric that matters; lab Lighthouse INP scores do not reflect what real users experience. Pull the 28-day CrUX p75 INP from PageSpeed Insights before and after the defer, allowing a full 28 days of post-fix field data to accumulate (the metric is a rolling 28-day window, so day 1 reflects mostly pre-fix data). My Shopify INP fix case study walks through the same measurement window on a real store that went from 612ms p75 to 178ms p75 in 5 working days. The lab improvement showed immediately; the field improvement showed on day 28.

Should I just uninstall the slow apps instead of deferring them?

Sometimes yes. If an app contributes nothing measurable to revenue or operations, uninstall it; one fewer script tag is always faster than the most aggressive defer pattern. Run a 30-day uninstall test on apps you suspect are unused (the merchant's first reaction is usually 'no, we use that' but the analytics rarely show meaningful interaction with the feature). For apps that do drive measurable revenue (Klaviyo flows, Yotpo reviews on PDPs, Rebuy AI recommendations), defer instead of uninstalling. The defer fix preserves the revenue while removing the performance tax.

What is the difference between INP and the older First Input Delay metric?

First Input Delay (FID) measured only the first interaction on the page (often the first click after page load), and only the delay before the browser started processing it. INP (Interaction to Next Paint), which replaced FID in March 2024, measures the worst interaction across the entire session and the full latency including event processing and the next visual update. INP is harder to game because it captures slow handlers that fire on the 5th cart drawer open, not just the first click. Apps that pass FID often fail INP because their slow handlers fire mid-session, not at page load.

Book Strategy Call