CRO · PERFORMANCE · APPS · 23 SEPTEMBER 2025 · 6 MIN READ
Upsell and cross-sell without slowing the page
Most upsell apps add a script to every template to serve a widget on one. Shopify already ships the endpoints to do it without one.
Build the upsell out of Shopify’s own endpoints and render it in the theme, rather than installing an app that loads JavaScript on every page to decide what to show on one. The product recommendations Ajax API returns between one and ten products for a product id, with related recommendations auto-generated by Shopify and complementary ones set up manually in the Search & Discovery app, and it will return a rendered theme section rather than JSON if you pass a section_id. The Cart Ajax API can re-render up to five theme sections in the response to an add or change, so a cart upsell needs no client-side templating at all. The upsells that cost real money are the ones that block interaction after the page looks ready — so measure Interaction to Next Paint, which Google puts in the good band at 200 milliseconds or less at the 75th percentile, not a synthetic score.
IN SHORT
- Shopify’s recommendations endpoint returns 1 to 10 products per product id, and can return rendered section HTML directly via `section_id` instead of JSON you have to template.
- Only `related` recommendations are auto-generated. `complementary` ones have to be set up by hand in the Search & Discovery app, so "complementary" is a merchandising commitment, not a switch.
- The Cart Ajax API accepts a `sections` parameter on add, change, update and clear, returning the HTML for up to five sections — which removes the usual reason a cart upsell ships a client-side rendering library.
- The cost of an upsell is rarely bytes on the wire. It is main-thread work after paint, which shows up in INP and not in a Lighthouse score.
- Post-purchase offers add nothing to the storefront page, but Shopify documents them as requiring a credit card payment — wallets such as Apple Pay, Google Pay, Klarna and Affirm are excluded, along with orders that have no shipping address.
- An upsell app loading on every template to serve a widget on one template is the single most common avoidable cost on a Shopify store.
The weight is not where people look for it
Ask why an upsell slowed the store and you will get an answer about file size. That is the least of it. A typical cross-sell app adds a script to the layout, so it loads on the home page, on collections, on blog articles and on the 404 page; then on each of those it parses, reads the cart, calls its own API, decides there is nothing to show, and stops. The bytes are modest. The main-thread time is not, and it lands after first paint — exactly when the customer is trying to tap something.
That is why INP is the metric to watch. It measures the latency of click, tap and keyboard interactions across the whole visit, and Google’s good threshold is 200ms or below at the 75th percentile, split by device. A page can score respectably on a synthetic audit and still feel broken to a customer on a mid-range Android who taps "Add to basket" while three widgets are hydrating.
The second cost is layout. An upsell block that appears after its data arrives pushes everything below it down, and the customer who was reaching for a button has the page move under their finger. Reserve the space or render it server-side; those are the only two honest options.
Use the endpoints that already exist
Before evaluating an app, it is worth knowing what the platform gives you, because a surprising share of upsell apps is a paid wrapper around these two endpoints.
Product recommendations. GET /{locale}/recommendations/products.json takes a product_id, an optional limit between 1 and 10 (default 10) and an optional intent of related or complementary. There is a second form — GET /{locale}/recommendations/products — which takes a section_id and returns your theme section already rendered. That second form is the one to reach for: the markup comes from the theme, so it matches the rest of the site, it uses your existing card component, and there is no client-side template to ship.
Two documented caveats matter operationally. Products must be published to the Online Store channel or the request 404s, which is the usual explanation for recommendations that work on staging and not in production. And only related recommendations are auto-generated by Shopify — complementary ones need setting up manually, through the Search & Discovery app. So if the plan is "show the accessory that goes with this", somebody has to sit down and pair products. That work is where the conversion lift lives; no app skips it, they just charge for the interface you do it in.
Cart section rendering. The Cart Ajax API accepts a sections parameter on /cart/add, /cart/change, /cart/update and /cart/clear, and returns the HTML for the requested sections under a sections key, keyed by section id. Up to five sections per request, and a section that fails to render comes back as null, so handle that rather than assuming markup. This is the feature that makes a cart drawer upsell cheap: add the item, get back freshly rendered drawer markup including the recommendations block, swap it in. No framework, no duplicated Liquid logic in JavaScript, no drift between the drawer and the cart page.
Three placements, three different costs
Where you put the offer decides what it costs you, and the cheapest placement is the one most stores use last.
On the product page. The lowest-risk and lowest-yield spot. Render it in Liquid at page load or with the section-rendering endpoint on demand, below the fold, in reserved space. If it is below the fold, load it when it approaches the viewport — an intersection observer and one fetch, which is a dozen lines rather than a dependency.
In the cart or drawer. The highest-yield storefront placement, because intent is already established and the basket is in front of the customer. Use the sections parameter so the server renders it. The failure mode here is the one to design against: an upsell that makes the add-to-cart interaction feel slow costs more in abandoned baskets than it earns in attachment, and you will not see the trade in an attachment-rate report.
After the order. Post-purchase offers cost the storefront nothing at all — they render after checkout completes, so no storefront page carries a byte for them. The limits are real and Shopify documents them: the customer must have paid by credit card, which the platform vaults before showing the offer, and wallets including Apple Pay, Google Pay, Klarna and Affirm are excluded, as are gift cards, orders without a shipping address and local delivery orders. A maximum of three offers per checkout, one app per merchant, and third-party pixels only report the original purchase. If a large share of your customers pay by wallet, the reachable audience is a fraction of your orders — check your own payment-method mix before budgeting a return on it. Checkout-page extensions, as opposed to post-purchase ones, need Shopify Plus.
If you do install an app, ask these four things
Sometimes an app is the right call — you want merchandiser-facing rules, reporting and A/B testing, and you do not want to build them. Fine. The questions that predict whether it will hurt are all answerable before install.
- Does it ship as a theme app extension with an app block, so its code only loads on templates where a merchandiser placed the block? Or does it inject a script into the layout on every page?
- Does it render server-side or fetch and template in the browser? The second means cumulative layout shift and main-thread work unless it reserves space.
- What does it do on a page with no offer to show — nothing, or the whole decision pipeline before concluding nothing?
- When you uninstall it, does anything remain in the theme? Script-tag apps and apps that edited theme files routinely leave code behind.
Measure the trade, not the win
Every upsell report shows attachment rate and incremental revenue, because that is the number the feature exists to produce. Nobody instruments the other side.
So instrument it. Take field INP and the add-to-cart completion rate for mobile sessions before you launch, and read them again after. If attachment is up 3% and mobile add-to-cart interactions got 300ms slower, you have very likely lost money, and you will not find that out from the app’s dashboard.
The version of this we would remove on almost any store we inherit: a bundle or cross-sell app loading sitewide to serve a block on two templates, a recommendations carousel below the fold that nobody has ever scrolled to, and a cart upsell that re-renders the whole drawer in JavaScript because nobody knew the sections parameter existed. None of those three is a strategy problem. They are the cost of features nobody re-examined after launch.
And the honest version of the whole argument: the biggest attachment gains we see come from pairing products properly and writing the offer copy well, not from the mechanism. A hand-curated complementary set with a plain, server-rendered block will out-earn an algorithmic widget that takes 400ms of main thread on every page of the site.
Questions this raises
How do you add upsells without hurting page speed?
Render them from the theme using Shopify’s own endpoints — the recommendations Ajax API with a `section_id` for server-rendered markup, and the Cart Ajax API’s `sections` parameter to re-render the drawer after an add — and load anything below the fold on approach. The cost that matters is main-thread work after paint, so the goal is no client-side templating and no script on templates that show no offer.
How many products does Shopify’s recommendations API return?
Between 1 and 10 for a given product id, with a default of 10. The `intent` parameter takes `related` or `complementary`, and the products must be published to the Online Store channel or the request returns a 404 — which is the usual reason recommendations appear on a development store and not in production.
Are Shopify’s complementary product recommendations automatic?
No. Shopify documents that only `related` recommendations are auto-generated; complementary ones must be set up manually, through the Search & Discovery app. That is the real work in a cross-sell programme — someone deciding which products go together — and it is the same work whether you build the block yourself or buy an app.
Are post-purchase upsells better than on-page ones?
They are free of storefront page weight, which is a genuine advantage, but their reach is limited by documented rules: the customer must have paid by credit card, and wallets such as Apple Pay, Google Pay, Klarna and Affirm are excluded, along with gift cards, orders with no shipping address and local delivery. Look at your payment-method mix first — on a store where most orders are wallet payments, most orders will never see the offer.
Do you need an upsell app on Shopify?
Not for the mechanism. You need one if you want merchandiser-managed rules, reporting and testing without building them, which is a reasonable thing to buy. If you do buy, insist on a theme app extension with an app block so the code loads only where the block is placed, rather than a script injected into every page of the site.
Which metric shows whether an upsell is slowing the store?
Field Interaction to Next Paint, segmented by device, alongside add-to-cart completion rate. INP captures the latency of taps and clicks across the visit and Google’s good threshold is 200ms or below at the 75th percentile — which is where a widget hydrating over your add-to-cart button becomes visible, and where a synthetic performance score will not show it.
NEXT STEP
Free store audit
A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritised list of fixes.
