Quick Fix with Thunder
Thunder defers non-critical app JavaScript so recommendation runtimes stop racing the product media and add-to-cart controls. The widget can still appear when shoppers reach it. The first viewport just gets priority.
Start with the Rebuy speed impact guide and the Nosto speed impact guide if you use those tools. For the full pattern, read the product page speed guide and the Shopify JavaScript optimization guide.
Install ThunderWhy Recommendation Widgets Slow Product Pages
A recommendation widget is rarely just a row of products. Rebuy, Nosto, LimeSpot, Wiser, and Also Bought style apps often load a personalization runtime, identify the visitor, fetch rules or model output, request product data, render product cards in JavaScript, download several extra images, and send impression analytics. That work can be useful after the shopper sees the product. It is harmful before the product page is usable.
The usual PageSpeed symptoms are higher Total Blocking Time, worse Interaction to Next Paint on mobile, and an LCP image that starts late because the browser is busy parsing recommendation JavaScript. Google's long tasks guidance explains why these app tasks matter, and Shopify's web performance help recommends reducing app code that loads before it is needed.
The fix is sequencing. The hero image, price, variants, add-to-cart button, and product details come first. Recommendation rows can wait until the shopper scrolls.
Step 1: Audit the Recommendation Runtime
Test a product page with the app enabled, then disable the recommendation app embed in a duplicate theme and test the same page again. Compare JavaScript transfer, long tasks, LCP start time, image transfer, and INP. The snippet below catches the common recommendation runtimes and reports their size.
// Run in DevTools Console on a product page with recommendations enabled.
performance.getEntriesByType('resource')
.filter((entry) => /rebuy|nosto|limespot|wiser|recommend|also-bought|personalization/i.test(entry.name))
.map((entry) => ({
url: entry.name,
kb: Math.round((entry.transferSize || 0) / 1024),
ms: Math.round(entry.duration),
type: entry.initiatorType,
}))
.sort((a, b) => b.kb - a.kb)
.forEach((entry) => console.log(entry));
console.log('Recommendation cards:', document.querySelectorAll('[class*="recommend"] article, [data-recommendations] article').length); Cross-check the result with the free Shopify speed test and PageSpeed Insights. If the widget adds 200KB of JavaScript and eight product thumbnails before the main product image is settled, it belongs later in the load order.
Step 2: Move Recommendations Below the Buying Path
Recommendation rows should not appear above the product form. Above the fold, shoppers need product media, price, variants, availability, reviews summary, delivery promise, and add-to-cart. A "You may also like" row before those elements creates visual noise and makes the browser load more images than it needs for the buying decision.
- Product pages: place recommendations after the main product section and core details.
- Cart drawers: keep upsells small and load after the drawer opens, not during page load.
- Homepages: avoid personalized product rows in the first viewport unless the store is built around them.
- Collections: keep recommendation widgets out of the first product grid; use native product cards first.
For the LCP reason behind this, use the Shopify LCP optimization guide. The first product image and hero media should be the browser's priority, not a personalized carousel below it.
Step 3: Lazy Load Product Recommendation Apps
Once the widget is below the fold, load it when the row is close to the viewport. IntersectionObserver avoids loading the app for visitors who bounce from the first screen. requestIdleCallback keeps the script from running during a tap or scroll if the browser is busy.
// Load the recommendation app only when the widget is close to view.
// Use this for below-the-fold "You may also like" and cart upsell blocks.
const widget = document.querySelector('[data-recommendation-widget]');
if (widget && 'IntersectionObserver' in window) {
const loadWidget = () => {
const src = widget.dataset.recommendationSrc;
if (!src || widget.dataset.loaded === 'true') return;
const script = document.createElement('script');
script.src = src;
script.async = true;
document.head.appendChild(script);
widget.dataset.loaded = 'true';
};
const io = new IntersectionObserver((entries, observer) => {
if (!entries.some((entry) => entry.isIntersecting)) return;
if ('requestIdleCallback' in window) {
requestIdleCallback(loadWidget, { timeout: 1200 });
} else {
setTimeout(loadWidget, 250);
}
observer.disconnect();
}, { rootMargin: '500px 0px' });
io.observe(widget);
} If your app exposes lazy loading, use the app setting first. If the app only gives you a global script tag, move the tag behind a loader like this inside a duplicate theme. The Shopify lazy loading guide and lazy loading LCP guide cover the edge cases.
Step 4: Use Native Shopify Recommendations Where Possible
If the row is just "related products" or "you may also like," Shopify native recommendations are usually enough. Start with Shopify Search & Discovery to set manual complementary products on best sellers, then render those recommendations through the theme before adding a popup or personalization app. They avoid a personalization SDK and can keep product images on Shopify's CDN. Save app-powered recommendations for cases where the app earns its JavaScript: cart bundles, AI ranking, post-purchase logic, or merchandising rules you cannot express natively.
{%- comment -%}
Native Shopify recommendations fallback. It avoids a personalization SDK
for simple "related products" rows and keeps images on Shopify's CDN.
{%- endcomment -%}
<product-recommendations
data-url="{{ routes.product_recommendations_url }}?section_id={{ section.id }}&product_id={{ product.id }}&limit=4"
data-product-id="{{ product.id }}"
>
{%- if recommendations.performed and recommendations.products_count > 0 -%}
<ul class="recommendations-grid">
{%- for product in recommendations.products limit: 4 -%}
<li>
<a href="{{ product.url }}">
{{ product.featured_image | image_url: width: 480 | image_tag: loading: 'lazy', widths: '240,360,480', sizes: '(min-width: 990px) 25vw, 50vw' }}
<span>{{ product.title }}</span>
</a>
</li>
{%- endfor -%}
</ul>
{%- endif -%}
</product-recommendations> Shopify documents the product recommendations endpoint and Liquid image helpers. The same image sizing principles appear in our Shopify image optimization guide and properly size images guide.
Step 5: Cap Cards, Images, and Tracking
Recommendation rows get slow because they try to do too much. Eight cards, hover media, badges, compare-at prices, review stars, swatches, quick add, and tracking beacons turn a simple upsell into a second product grid. Cap the row and keep it stable.
/* Recommendation rows should be stable, small, and below the buying path. */
.recommendations-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
min-height: 320px;
contain: layout paint;
}
.recommendations-grid img {
aspect-ratio: 1 / 1;
width: 100%;
height: auto;
object-fit: cover;
}
@media (max-width: 768px) {
.recommendations-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
min-height: 520px;
}
.recommendations-grid li:nth-child(n + 5) {
display: none;
}
} - Use 4 cards on desktop and 2-4 on mobile.
- Disable hover videos and second-image previews inside recommendation rows.
- Remove duplicate review stars if your reviews app already loads elsewhere.
- Batch impression analytics after idle instead of firing one beacon per card at first paint.
- Use image widths around 360-480px for cards, not original product images.
Step 6: Scope Widgets by Template
Recommendation apps often load storewide because the app embed is global. That is rarely necessary. A product recommendation app normally belongs on product and cart surfaces. It does not need to load on blog posts, policy pages, password pages, or most landing pages.
{%- assign load_recommendations = false -%}
{%- if template.name == 'product' or template.name == 'cart' -%}
{%- assign load_recommendations = true -%}
{%- endif -%}
{%- if template.name == 'index' or template.name == 'collection' or template.name == 'page' -%}
{%- assign load_recommendations = false -%}
{%- endif -%}
{%- if load_recommendations -%}
<!-- Product recommendation app embed or native recommendations section goes here. -->
{%- endif -%} Shopify's theme app blocks help explains how app blocks attach to page types. Use app targeting first, then Liquid guards only when the app gives you no clean targeting control.
Step 7: Protect Mobile INP
Mobile INP gets worse when a recommendation carousel runs long JavaScript tasks while the shopper taps size, color, quantity, or add-to-cart. Keep the widget idle until the page has settled. Avoid carousel libraries that attach heavy touch handlers. Use passive listeners where possible and test on a low-end Android profile, not just a desktop laptop.
Read the Shopify INP fix guide, passive event listeners guide, and Total Blocking Time guide if PageSpeed still flags long tasks after lazy loading the widget.
Manual Fix vs Thunder Fix
| Problem | Manual fix | Thunder fix |
|---|---|---|
| Recommendation SDK loads at first paint | Move the widget below the fold and lazy load it with IntersectionObserver | Defers non-critical app JavaScript automatically |
| Simple related-products row uses heavy app runtime | Use native Shopify product recommendations | Improves the remaining app load order when native is not enough |
| Too many cards and images | Cap rows to 4-6 cards and use Shopify image widths | Reduces JavaScript competition but cannot choose card count |
| Widget loads storewide | Scope to product and cart templates using app settings or Liquid guards | Defers scripts on the pages where the widget must remain |
| Mobile taps feel delayed | Avoid heavy carousels, delay analytics, and test INP on mobile | Cuts main-thread pressure from non-critical scripts |
| Need broader speed work | Audit all app embeds and product page widgets | See Thunder features, pricing, or expert optimization |
Fix the Widget Without Killing AOV
Recommendation apps can earn their place. The mistake is letting them run before the shopper can evaluate the product. Keep the row below the buying path, lazy load the runtime, cap cards and image sizes, prefer native recommendations for simple rows, defer analytics, and test the duplicate theme before promoting it.
For the broader roadmap, use the complete Shopify speed optimization guide, the best Shopify speed apps guide, and the app speed review guide. That is the practical way to fix product recommendation app slowing down Shopify without giving up the AOV lift.
FAQ
How do I fix a product recommendation app slowing down Shopify?
Keep recommendation widgets below the first viewport, lazy load the recommendation runtime with IntersectionObserver, cap rows to 4-6 cards, use Shopify image widths and lazy loading, remove widgets from templates where they do not drive revenue, defer personalization and analytics JavaScript, and let Thunder handle the non-critical script deferral automatically.
Do product recommendation apps hurt Core Web Vitals?
They can. Rebuy, Nosto, LimeSpot, Wiser, Also Bought style widgets often load a personalization SDK, fetch product data, render cards in JavaScript, and download extra product images. If that happens before the product image and add-to-cart area are usable, LCP, TBT, and INP can all suffer.
Are Shopify native recommendations faster than recommendation apps?
For basic related-product rows, yes. Shopify native recommendations avoid a large personalization runtime and can render product images through Shopify's CDN. Dedicated apps are still useful for AI ranking, bundles, cart upsells, and merchandising logic, but simple product rows should use the native path first.
Where should product recommendations appear on a Shopify product page?
Below the main product media, variant selector, price, add-to-cart button, reviews summary, and core product details. A recommendation row above the fold competes with the LCP image and the buying controls. Put it where the shopper naturally looks after evaluating the product.
Can Thunder fix product recommendation apps slowing down Shopify?
Thunder defers non-critical JavaScript so recommendation apps stop competing with the initial product page load. It does not choose which products appear or change your merchandising rules. Pair Thunder with below-the-fold placement, lazy loading, smaller card counts, and native recommendations where possible.
Keep the Upsell. Remove the Drag.
Thunder defers non-critical recommendation JavaScript so Rebuy, Nosto, LimeSpot, Wiser, and similar widgets stop racing your product page's first viewport.
Install Thunder See Thunder features