CHECKOUT · PERFORMANCE · FUNCTIONS · 16 AUGUST 2026 · 7 MIN READ
Checkout extensions that are worth the milliseconds
The platform caps most of the damage an extension can do. What is left is network calls, stacked extensions and rules built in the wrong layer.
One well-built checkout UI extension is not what slows a checkout down, and that is deliberate: Shopify runs extensions in a Web Worker sandbox with no DOM access and caps a compiled bundle at 64 KB, so the two classic ways a checkout script got heavy are closed off. What is left is real, though, and it is where every slow checkout we have looked at was losing its time — an extension that has to call your API before it can render anything, an extension that writes changes one at a time until it gets rate limited, three extensions sharing one block target, and rules built in the UI layer that should have been a server-side Function. The budget is not the bundle. It is the round trips.
IN SHORT
- Shopify documents a hard ceiling of 64 KB for a compiled checkout UI extension bundle, and extensions run inside a Web Worker with no access to `window` or the DOM.
- An extension that fetches from your own endpoint before it can render is the single biggest cost in checkout, because the buyer waits on your infrastructure rather than Shopify’s.
- Shopify documents rate limiting for extensions that make too many changes during a checkout, and recommends batching changes with `Promise.all()` — after an extension is rate limited it cannot make further changes for the rest of that session.
- A merchant can place up to three extensions in one block target location, so your extension’s cost is rarely the only cost in that spot.
- Functions run server-side and have published budgets: 11 million instructions, 128 kB of input and 20 kB of output for carts up to 200 line items, and a 256 kB compiled binary.
- Measure checkout completion per step before and after. An extension that cannot show it paid for its own friction should come back out.
What the platform already stops you doing
Before worrying about the milliseconds an extension costs, it is worth being clear about which costs the platform has already removed — because they are the ones that made the old checkout.liquid era slow, and people still budget for them out of habit.
Shopify documents a hard ceiling: a compiled UI extension bundle "can’t exceed 64 KB, or 128 KB for full-page customer account extensions", and it is enforced at deployment. You cannot ship a charting library, a date-picker dependency tree or an analytics SDK into checkout, because none of them fit. That single number rules out most of what historically made a checkout heavy.
Extensions also run inside a Web Worker environment with no access to window or the DOM. That is usually discussed as a security property — it is why an extension cannot read payment data or restyle the page — but it is a performance property too. Worker code is not competing with the host page for the main thread while it parses and executes.
So the naive worry, "our extension’s JavaScript will make checkout janky", is largely handled. The interesting question is what your extension does *after* it starts running.
Where the milliseconds actually come from
Three mechanisms, in the order we usually find them.
Your extension waits on your API. Any call to a third-party service requires the network_access capability, declared in shopify.extension.toml. Declaring it is trivial; living with it is not. The moment an extension cannot render until your endpoint answers, the buyer’s checkout is gated on your infrastructure, your region, your cold starts and your worst day — none of which Shopify’s uptime covers. A delivery-date extension that calls a carrier API synchronously is the common version of this, and it fails in exactly the way you would predict: fine in testing, then a support ticket on the first morning the carrier is slow.
The fix is almost always to move the computation earlier. Precompute the answer and store it where the extension can read it without a round trip — a metafield on the variant or the shop, a cached shape written by a scheduled job. If the value genuinely cannot exist before checkout, render the rest of the extension immediately and fill that one field in when it arrives, with a state that is honest about not knowing yet. Never block the whole component on a fetch.
Your extension writes changes one at a time. Extensions mutate the checkout through calls such as applyAttributeChange and applyMetafieldChange, and Shopify documents that "rate limits may apply to extensions that make too many changes during a checkout" — and that once rate limited, "it can’t make further changes during the buyer’s session". The recommendation in the documentation is to batch them with Promise.all(). This is worth internalising, because the failure is not a slow checkout, it is a silently broken one: the first four writes land, the fifth does not, and the order arrives missing the attribute your warehouse sorts on. Awaiting each change in sequence inside a loop is the pattern that gets you there.
You are not the only extension in that spot. A merchant can add up to three extensions to the same block target location. Your budget is not the whole page. If the store already runs a loyalty widget and a gifting field in the same slot, your addition is the third thing rendering, and the aggregate is what the buyer feels. Ask what is already installed before designing as if the space is empty.
Functions are not free, but the budget is published
Rules belong in Functions rather than in the UI layer — a client-side check does not survive an express checkout, so a rule built as an extension evaporates the moment someone taps Shop Pay. That argument is about correctness, but the performance argument points the same way, and Functions are unusually well documented about what they will and will not let you spend.
Shopify publishes the resource limits directly. For carts up to 200 line items: 11 million instructions of execution, 128 kB of function input, 20 kB of function output — scaling proportionally above 200 lines. Fixed limits apply regardless of cart size: a 256 kB compiled binary, 10,000 kB of runtime linear memory, 512 kB of stack. The input query has its own ceilings: a maximum query size of 3000 bytes excluding comments, a maximum query cost of 30, list arguments capped at 100 elements, and metafield values over 10,000 bytes simply not returned.
Two things follow from that list. First, the input query is where a Function goes wrong at scale — the instruction limit is generous and the query cost limit is not, so the discipline is to fetch the minimum the rule needs rather than everything that might be useful. Second, Shopify recommends Rust and says so plainly, describing it as "the most performant language choice to avoid your function failing with large carts". A JavaScript Function that passes on a three-line test cart is not evidence about a fifty-line trade order.
Functions also cannot use randomisation or clock functionality. That is not a performance limit but it catches people out when they try to build anything time-dependent, and it is another reason a delivery-date calculation wants to be precomputed data rather than logic evaluated at checkout time.
The ones that earn it, and the ones that do not
Our test is unchanged whatever the mechanism: does the buyer need this to complete *this* order, and is checkout the first place they could possibly have seen or provided it? Everything that passes tends to be cheap, and everything that fails tends to be expensive — which is not a coincidence, because things the buyer does not need are usually the ones reaching out to a third party.
Worth the milliseconds:
- A field the order cannot be fulfilled without — gift message, delivery instructions, PO number — which is local state and a single batched write.
- A delivery date or window, provided the underlying data is precomputed and read from a metafield rather than fetched live from a carrier.
- An age, licence or compliance confirmation, paired with a validation Function that actually enforces it server-side.
- One line of factual reassurance — the returns window, duties included, who to ring — which costs nothing because it is static text.
And the ones that do not
Checkout upsells are the most requested and the least defensible. They add a network call and a render to 100% of sessions in order to convert a small fraction, at the precise moment the buyer has finished deciding. Post-purchase is the right home: a decline there costs nothing, and the page is already past payment.
Live chat and analytics injection are worse, because both want to load a third-party script and neither is what UI extensions are for. Tracking has its own sandbox in web pixel extensions, which is why that is the supported route rather than a workaround.
The honest framing for all of these: you are not being asked whether the feature is good. You are being asked whether it is worth a network round trip on the one page where abandonment is most expensive. Most of the time the same feature, one page earlier, costs nothing and works better. That judgement is most of what we do when we scope [a Shopify build](/services/build) — the checkout you ship should be smaller than the one you were talked into.
How to know, rather than argue
The measurement people reach for is site-wide page speed, and it will tell you nothing, because checkout is not your theme and a Lighthouse run on a product page cannot see it.
Measure checkout completion rate per step, before and after the extension ships. That is the number the extension can actually move, in either direction, and it is the only one worth putting in front of whoever asked for the feature. Pair it with a deliberate test of the failure mode: block or throttle your own endpoint in staging and check that the extension still renders, still lets the buyer continue, and does not silently swallow the write it needed to make.
Then set the rule in advance, before anyone is attached to the thing: if it has not paid for its own friction by a date you agree now, it comes out. Extensions accumulate the same way apps do, and a checkout nobody has audited in two years is carrying at least one thing that no longer has an owner.
Questions this raises
Do checkout UI extensions slow down checkout?
Not inherently. Shopify caps a compiled bundle at 64 KB and runs extensions in a Web Worker sandbox with no DOM access, which closes off the two ways checkout scripts used to get heavy. The cost that remains is network calls: an extension that must fetch from your API before it can render puts the buyer on your infrastructure’s worst day, and that is where slow checkouts actually lose their time.
How big can a checkout UI extension be?
Shopify documents a limit of 64 KB for a compiled UI extension bundle, or 128 KB for full-page customer account extensions, enforced at deployment. Treat it as a design constraint rather than an obstacle — anything needing a large dependency to function is almost certainly not something that should be running inside checkout.
Can a checkout extension call an external API?
Yes, with the `network_access` capability declared in `shopify.extension.toml`. The question is whether it should. Precompute the answer into a metafield where you can, and where you genuinely cannot, render everything else immediately and fill the one unknown field in when it arrives. An extension that blocks on a fetch inherits your API’s latency and its outages.
Why did my extension stop applying changes partway through checkout?
Almost certainly rate limiting. Shopify documents that rate limits may apply to extensions making too many changes during a checkout, and that once rate limited an extension cannot make further changes for the rest of that buyer’s session. Awaiting each change in a loop is the usual cause; batching them with `Promise.all()` is the documented recommendation.
Are Shopify Functions faster than a UI extension for checkout logic?
They are a different thing rather than a faster one — Functions run server-side and cover accelerated checkouts, so a rule built as a Function holds when a buyer taps Shop Pay and a rule built in the UI does not. Their budgets are published: 11 million instructions, 128 kB input and 20 kB output for carts up to 200 line items, a 256 kB compiled binary, and an input query capped at a cost of 30.
How do I measure whether a checkout extension is worth keeping?
Checkout completion rate per step, before and after, not a site-wide speed score — a Lighthouse run on your theme cannot see checkout at all. Agree the threshold and the review date before the extension ships, and test the failure path in staging by throttling your own endpoint to confirm the buyer can still complete the order.
NEXT STEP
Free store audit
A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritised list of fixes.
