Skip to main content
ModPageSpeed 2.0 and mod_pagespeed 1.1 — Now available

INP on WooCommerce: where the cost lives

By Otto van der Schaaf

core-web-vitals inp woocommerce

INP on a WooCommerce product page is almost always the click on a variation swatch, a hamburger menu, or “Add to cart”: somewhere between 250 ms and 700 ms on mid-range Android, and Search Console is now flagging it. The plan: diagnose the responsible interaction, push parse-blocking JS past it with defer_javascript and friends via ModPageSpeed (an nginx or Apache module), then fix the cart and variation handlers on the WooCommerce side, because that is where the real cost lives. The cart is interactive JS by design, so server-layer leverage on those interactions is genuinely small.

What INP measures

Interaction to Next Paint (INP) is the worst-case latency between a user input (click, tap, keypress) and the next frame the browser paints in response. Chrome records every interaction over the lifetime of the page and reports the 98th-percentile slowest one as that page’s INP. Good is ≤ 200 ms, “needs improvement” is up to 500 ms, anything above 500 ms is failing. Real-user numbers come from CrUX; lab approximations come from Lighthouse via PSI, but lab INP is unreliable because the lab runs no human input. Field data is the only number that matters. See web.dev/inp.

The most common INP failures on WooCommerce

  1. “Add to cart” triggers a full wc-cart-fragments.js refresh. Click “Add to cart”, JS posts to ?wc-ajax=add_to_cart, the response triggers a fragment refresh, every <div data-fragment-name> block re-renders. On a busy product page with a mini-cart plus cross-sells, that re-renders 20–40 KB of HTML and takes 250–500 ms on a mid-range phone. INP fires on the click, so that latency is what gets recorded.
  2. Variation swatches re-render the entire <form.variations_form> on every change. WooCommerce’s add-to-cart-variation.js does a synchronous JSON parse of all variations on page load (the data-product_variations attribute can be 100 KB+ for products with 50+ variants), then on every swatch click runs a full re-render including price and availability lookup. Swatch-click INP is consistently the worst metric on WC PDPs.
  3. Coupon-apply on the cart page blocks the form. cart.js’s apply_coupon flow does a synchronous AJAX call inside the form submit handler. The UI freezes for the duration of the request; INP is bounded by network latency, which on poor connections is 600–1000 ms.
  4. Cart-fragments AJAX runs on every page, not just cart pages. The mini-cart count is refreshed via wc-cart-fragments.js on the homepage, blog, and every other page. The AJAX call itself does not record an INP (it is not user-triggered), but its completion handler often runs while the user is trying to interact, and now it is competing for the main thread.

Catch the swatch-click

INP is a worst-case metric; you cannot fix it without knowing which click. WooCommerce makes this easier than vanilla WordPress because the worst interactions are predictable.

  • Open the product page in Chrome, open DevTools → Performance panel, click record, then perform the user flow: load the page, click a variation swatch, click “Add to cart”, open the cart page, apply a coupon. Stop. The Interactions track lists each input with INP duration. Click the worst one; the flame chart shows the handler.
  • Install web-vitals with attribution mode in footer.php. Log onINP(console.log, {reportAllChanges: true}). The attribution.eventEntry field names the script and handler that took the longest.
  • Run PSI: https://pagespeed.web.dev/analysis?url=<URL>. Run it once on the homepage, once on a PDP, once on the cart page; the numbers will differ because the worst interaction differs.
  • In WP admin: WooCommerce → Status → Tools → System Status lists active extensions. Each one is a candidate for the worst-interaction handler.

Output you want: the specific interaction (e.g., “click on .swatch-color”) and the specific script (e.g., “add-to-cart-variation.js line 421”). That dictates which step below actually moves the metric.

Step 2: Defer cart-fragments off non-shop pages with defer_javascript

Where this actually helps: on non-shop pages (homepage, blog, content pages), defer_javascript is a real win because the cart-fragments script is doing useless work. On the cart and checkout pages themselves, the JS cost is the logic, not the bytes, so server-layer filters cut a measurable but small slice off it.

Enable, in order:

  • defer_javascript — pushes script execution past the initial render, so the first interaction is not competing with cart-fragments init. Marked Test first in the filter reference because order-sensitive WC behavior can break. Stage it; verify the cart still updates after add-to-cart.
  • combine_javascript — concatenates the dozen separate JS files WC pulls in. Reduces per-file parser setup; first-load parse drops noticeably.
  • rewrite_javascript — minifies inline and external JS. WC’s bundles are already minified in production, but theme bundles and third-party plugin JS often are not.

Minimal nginx config:

pagespeed on;
pagespeed FileCachePath /var/cache/ngx_pagespeed;
pagespeed RewriteLevel CoreFilters;
pagespeed EnableFilters defer_javascript;
pagespeed EnableFilters combine_javascript;
pagespeed EnableFilters rewrite_javascript;

After enabling, re-run PSI on each page type. The homepage and blog INP should improve clearly; the PDP swatch-click and cart-page INP will improve modestly. That asymmetry is the signal that the remaining work is at the WC layer.

Tuning WooCommerce itself

This is where the WooCommerce-specific INP wins live. They matter more than the server-layer filters for the cart/checkout path.

  • Switch to WooCommerce Blocks Checkout (Cart and Checkout blocks, available since WC 7.8). React-based, with a different INP profile: faster for subsequent changes, sometimes slower for the first interaction (hydration cost). Test field data after switching; do not switch on lab data alone.
  • Disable wc-cart-fragments.js on non-shop pages. The snippet is six lines in functions.php; it dequeues the script when ! is_woocommerce() && ! is_cart() && ! is_checkout(). Homepage and blog INP often drop 100–200 ms from this alone.
  • Reduce variation JSON size for variable products with many options. The 100 KB+ data-product_variations blob is parsed synchronously on every swatch click. Limit attributes per variation in single-product/add-to-cart/variable.php, or generate variation data lazily via the WC REST API on swatch click instead of pre-embedding all of it.
  • Replace the cross-sells fragment with a deferred load. Wrap the cross-sells render in requestIdleCallback(...); the user reaches the add-to-cart button before the cross-sells contend for the main thread.
  • Audit theme-injected wp_footer scripts. Premium themes (Flatsome, Astra Pro) often add per-product analytics that run on every variation change. They double the swatch-click handler cost. Disable them.

How to confirm the swatch click landed

  1. Re-run PSI on the same set of URLs. The field “Interaction to Next Paint” should drop on all of them; the homepage drop should be large (often 30–50 %), the PDP drop modest (10–20 %).
  2. Open DevTools → Performance, replay the user flow, confirm the worst-interaction INP is under 200 ms in the Interactions track. If swatch-click is still above 200 ms, the WooCommerce-side tuning work remains.
  3. Wait 28 days, then check Search Console → Core Web Vitals report. The URL group should move from “Needs improvement” to “Good”. Track PDP and cart URL groups separately — they regress for different reasons.
  4. Field-data attribution log (web-vitals script in footer.php) is the definitive view. PSI summarizes; field logs name the click.

Configuration cheat sheet

# nginx — minimal config to address INP on WooCommerce
pagespeed on;
pagespeed FileCachePath /var/cache/ngx_pagespeed;
pagespeed RewriteLevel CoreFilters;
pagespeed EnableFilters defer_javascript;
pagespeed EnableFilters combine_javascript;
pagespeed EnableFilters rewrite_javascript;
# On non-shop URLs only — useful where the first paint is text-heavy:
pagespeed EnableFilters prioritize_critical_css;

For Apache:

ModPagespeed on
ModPagespeedFileCachePath /var/cache/mod_pagespeed
ModPagespeedRewriteLevel CoreFilters
ModPagespeedEnableFilters defer_javascript
ModPagespeedEnableFilters combine_javascript
ModPagespeedEnableFilters rewrite_javascript

Where the server layer runs out of leverage

WooCommerce’s interactive surface (cart, variations, checkout) is JS by design. Server-layer optimization minifies and reorders, but cannot remove the work the cart logic actually does.

  • Cart and checkout INP is largely architectural. If your worst interaction is “Add to cart” or “Update cart”, you are looking at WooCommerce’s AJAX-fragment refresh pattern. ModPageSpeed cuts parse time but the fragment refresh itself (JSON parse, DOM rewrite, JS event re-binding) is what eats the INP budget. The fix is upgrading to WC Blocks Checkout, or accepting the architectural floor.
  • Third-party scripts you do not control. Cart abandonment trackers, exit-intent popups, and chat widgets all attach click listeners. INP attribution on a popup-trigger click stays with the popup script: MPS minifies the bytes but the handler still runs.
  • Variation JS scales with variation count. If your product catalog has products with 100+ variations, the swatch-click handler is doing work proportional to the variation list size. The fix is product data, not page optimization: reduce variations, or move variation data to a lazy REST fetch on swatch click instead of embedding all of it in data-product_variations.
  • Plugin-injected per-page JS. WooCommerce’s plugin ecosystem ships some genuinely heavy frontend JS (wishlist plugins, ajax filters, instant-search overlays). MPS minifies them but cannot remove a 150 ms keyup handler.

If your INP problem is on the homepage/blog, the server layer is the high-leverage fix. If it is on the cart/checkout, the high-leverage fix is at the WooCommerce layer. Knowing which one applies to you is what the Diagnose step is for.

ModPageSpeed runs as an nginx or Apache module; a 14-day trial starts from /pagespeed_global_admin (card-at-start via FastSpring). See license terms.

Read next