- Four layers, one page. The checkout is a Knockout app, configured by a PHP array, declared in XML, serialised to JSON, and it cannot be full-page cached.
- The main customisation mechanism fails silently. You change the checkout by reaching into a nested array by string path. A wrong path does nothing, with no error and no log entry.
- Every extension edits the same array. Conflicts are structural, not bad luck, which is why upgrades break several extensions at once.
- Every change recalculates everything. The quote reruns every totals collector, and each active shipping carrier adds a live API call.
- A one-step extension usually makes it worse, and Adobe's own next-generation storefront ships a bridge back to the old checkout rather than rebuilding it.
- What works, in order: measure the failing layer, fix configuration, fix the flow, and only then consider Hyvä Checkout or Edge Delivery Services with Luma Bridge.
Because the checkout is a JavaScript application, configured by PHP, declared in XML, rendered by a templating library most developers no longer use, and it is the one page on your store that cannot be cached. Almost every Magento checkout problem, from a two-week estimate on a one-line change to a Place Order button that silently does nothing, traces back to that sentence.
None of this is your team's fault, and it usually isn't your extensions' fault either. It's the compounding cost of four architectural decisions made when Magento 2 launched in 2015, at a moment when the industry believed client-side rendering was the future of everything. This article explains each of them, what they cost you in practice, and what actually works, in the order worth trying.
The short version
Four root causes, in the order they bite:
- The checkout is configured through a deeply nested array, assembled from XML by PHP, serialised to JSON, and handed to Knockout. Changing anything means reaching into that array by string path.
- Every extension reaches into the same array, so conflicts are structural rather than accidental.
- Checkout cannot be full-page cached, so every interaction hits the full application stack, and Magento's workaround for personalised data has its own cost.
- The quote model recalculates everything on every change, and each extension adds to what "everything" means.
Root cause 1: a JavaScript app configured by XML
Here's what happens when a shopper opens your checkout.
Magento reads checkout_index_index.xml, a layout file that declares every checkout element (authentication, shipping address, shipping methods, payment methods, summary, sidebar) as a UI component with a Knockout template and a config block. That XML is processed by Magento\Checkout\Block\Checkout\LayoutProcessor::process(), which walks the nodes and builds a large nested PHP associative array. The array is converted to JSON, echoed into onepage.phtml through getJsLayout(), and passed to Magento_Ui/js/core/app, which instantiates the component tree in the browser.
<script type="text/x-magento-init">
{
"#checkout": {
"Magento_Ui/js/core/app": <?= $block->getJsLayout() ?>
}
}
</script>
So the page you see is assembled client-side from a configuration object that was built server-side from XML. Four technologies, one page.
Now suppose you want to remove a field. The documented approach is a plugin on LayoutProcessor::process() that mutates the array:
unset($jsLayout['components']['checkout']['children']['steps']['children']
['shipping-step']['children']['shippingAddress']['children']
['shipping-address-fieldset']['children']['company']);
return $jsLayout;
Look at what that code is. A string path, twelve keys deep, into an untyped array, with no schema, no autocompletion, and no error if the path is wrong. If any segment of that path changes (because you upgraded, because a theme reorganised the tree, because another extension moved the node first), the line does nothing at all. No exception, no warning, no log entry. The field simply reappears, and someone finds out in production.
That single property explains an enormous share of Magento checkout debugging time. The failure mode of the primary customisation mechanism is silence.
The alternative is a custom layout processor registered through DI, which is cleaner and better practice. It doesn't change the fundamental issue: you are still manipulating a nested array by path.
And a single change frequently spans all four layers. Adding a validated custom field means: XML to declare the component, a PHP plugin or processor to inject it into jsLayout, a Knockout view model with observables, an HTML template, a RequireJS mixin to hook the existing validator, and usually a backend model to persist it onto the quote and then the order. Six files for one field. That is not a team moving slowly.
Root cause 2: everyone edits the same array
Because jsLayout is the only real extension point, every checkout extension you install manipulates it. Your shipping extension, your payment extension, your tax extension, your gift-message module and your own customisations are all plugins on the same method, mutating the same structure, in an order determined by plugin sortOrder and module load sequence.
The consequences are predictable once you see the mechanism:
- Two extensions target the same node; the second finds it already moved and silently no-ops.
- One extension removes a node another depends on; the dependent component throws during init.
- Behaviour differs between staging and production because module load order differs.
- An upgrade shifts a path by one level and three unrelated extensions stop working at once.
This is why Magento checkout projects carry a regression-testing burden that surprises people coming from other platforms. It isn't that Magento extensions are poor quality. It's that the architecture gives them one shared mutable object and no contract.
Root cause 3: checkout can't be cached, and the workaround costs too
Full-page cache (Varnish or the built-in FPC) serves identical HTML to everyone. Checkout is inherently personalised, so it is excluded by design. Every checkout request goes through the entire PHP application stack.
That alone would be manageable. Two things make it worse.
The AJAX waterfall. Opening checkout and interacting with it fires a sequence of REST calls, each of which does substantial work: estimate-shipping-methods, totals-information, customer data sections, payment method configuration, and whatever your extensions add. A widely circulated community profiling post from 2020 recorded estimate-shipping-methods running 166 SQL queries in 2.57 seconds and totals-information running about 150 queries in 2.89 seconds on a single real store. Those specific numbers are old and store-specific, but anyone who has profiled a Magento checkout since will recognise the shape. The requests are sequential, each blocks the UI, and the shopper watches a spinner.
Customer data sections. Because cached pages can't render personalised content server-side, Magento fetches it separately and caches it in the browser's localStorage under mage-cache-storage. The default configuration loads every section, and the cart section alone can reach 100KB+ with configurable products and tier pricing. Practitioners report multi-megabyte localStorage payloads on real stores. Parsing a payload that size blocks the main thread and shows up directly in Interaction to Next Paint. Mobile Safari evicts localStorage under memory pressure, forcing a full refetch at the worst possible moment: on a phone, mid-checkout.
On top of that, the checkout page loads a large JavaScript bundle. One agency analysis puts a default Magento 2 checkout at over 270 JS files and around 2.8 MB. Treat the exact figure as indicative rather than authoritative (it varies enormously with theme and extensions), but the order of magnitude is not in dispute, and every payment and shipping extension adds to it.
There is a less obvious link too: a broken cache on your catalogue pages can make checkout slow, because it uses up the PHP capacity checkout depends on. Our guide to full-page cache and a slow Magento payment page covers that mechanism and the fix list in order.
Root cause 4: the quote recalculates everything
Magento's quote model runs totals collectors in sequence: subtotal, shipping, tax, discount, grand total, plus whatever extensions register. They're interdependent by necessity: tax depends on shipping address, discounts depend on subtotal, shipping can depend on weight and destination.
Any change to the quote triggers the whole sequence. Change the country, recollect. Apply a coupon, recollect. Pick a different shipping method, recollect. Each recollection runs every registered collector, including the ones from extensions you installed for an unrelated reason.
Configuration compounds this. Every active shipping carrier means a live API call to that carrier when the address changes, and published latency estimates conflict sharply, with one source putting three carriers at 6 to 9 seconds and another putting four at 2 to 3 seconds. The honest read is that it depends heavily on carrier, region and network path, and that you should measure your own rather than trust either number. Every enabled payment method loads its own JavaScript and sometimes its own iframe. Every active cart price rule and catalog price rule is evaluated.
This is why "just add a small fee at checkout" is never a small job on Magento. The fee has to enter the totals sequence in the right position, survive recollection, appear in the sidebar, persist to the order, and not break tax.
What this looks like when it goes wrong
The architecture above produces a recognisable set of failures. These are the ones that come up repeatedly in audit work:
The checkout that renders but doesn't work. A JavaScript error during component initialisation aborts the tree partway. The page looks almost right. Place Order does nothing: no error message, no server-side log entry, because the request was never made. Nothing in your monitoring fires, because from the server's perspective nothing happened. The only signal is a conversion rate that dropped and a support inbox that's slightly busier. We've published a case where exactly this pattern turned out to have two separate causes, one of which was a payment skimmer.
The bug that only affects some customers. A specific combination (one country, one payment method, one product type) hits a code path nobody tested. Because checkout behaviour is assembled at runtime from configuration, the permutation space is larger than anyone's test matrix. These are invisible in aggregate metrics and obvious in segmented ones.
The upgrade that breaks three extensions at once. A path in jsLayout moved. Everything that referenced it silently stopped applying.
CSP violations blocking payment scripts. Magento's Content Security Policy implementation is strict, and payment SDKs pull from domains that need allowlisting. Adobe Commerce 2.4.8 shipped a fix allowing connections to region1.analytics.google.com, a reminder that even first-party integrations hit this.
Cache configuration that breaks sessions. Aggressive Varnish rules or a misconfigured CDN that caches something session-specific produces the worst class of bug there is: shoppers seeing each other's cart data.
Why the obvious fixes usually make it worse
"Install a one-step checkout extension." These replace the component everything else plugs into. You are not simplifying the architecture; you are adding another very large participant to the shared-mutable-array problem described above, and asking every payment and shipping extension you own to work with it. The recurring failure modes are well documented across the community: conflicts with other checkout extensions, payment and shipping integrations breaking, duplicated or vanishing fields, and often a slower checkout than the one you replaced.
It's also worth knowing what you're replacing. Magento's default checkout is already a single URL with two sequenced steps: shipping, then review and payment. It is an accordion, not a multi-page flow. The gap between it and a "one-step" extension is smaller than the marketing suggests, and the research on page count is a lot less decisive than vendors claim.
"Go headless: build a new frontend." This is where Adobe's own documentation is more revealing than any third-party opinion.
Adobe's strategic investment has moved to Edge Delivery Services as the next-generation Commerce storefront. PWA Studio isn't deprecated (its last release was v14.5.0 in February 2025, and Adobe has never dropped support for a storefront), but major new feature work is unlikely there.
And for EDS projects, Adobe ships something called Luma Bridge: a session-management mechanism that lets a new Edge Delivery storefront hand the shopper back to the existing Luma PHP checkout, rather than rebuild it. Adobe's guidance tells implementers to assess the architecture, complexity and customisation requirements of cart, checkout and account pages before deciding between Luma Bridge and a headless implementation.
Read that again. Adobe's own next-generation storefront architecture ships an official escape hatch whose purpose is to let you avoid rebuilding checkout. That is the clearest possible confirmation that checkout is the hardest part of a Magento frontend, from the party with the most incentive to claim otherwise.
What actually works, in order
1. Measure before you change anything. A slow checkout is not one problem. A delayed mini-cart refresh looks like slow add-to-cart. A carrier timeout looks like a frozen shipping step. A payment provider's script looks like broken Magento. Identify the failing layer (server request, customer-data refresh, third-party script, browser main thread) before touching code. In practice: open the network panel on a real device, place a real order, and find which request or task is actually slow. If your analytics say shoppers drop at a particular step, first confirm the funnel events are really firing; a missing event looks exactly like a lost shopper.
2. Configuration before code. A large share of Magento checkout slowness is configuration, and costs nothing to fix:
- Disable shipping carriers you don't use. Each active one is a live API call on address change.
- Disable payment methods below a couple of percent of transactions. Each loads its own JS and sometimes an iframe on every checkout.
- Restrict the country list to where you actually ship.
- Disable expired and unused cart and catalog price rules.
- Audit
sections.xmland trim customer-data sections to what the page actually needs.
This layer routinely produces the largest improvement per hour spent, and it needs no developer.
3. Fix the flow before the framework. Field count, cost transparency, guest checkout and error handling move conversion more reliably than architecture does, and they apply on any platform. Baymard's benchmark attributes an average 35.26% potential conversion improvement to better checkout design across large ecommerce sites generally; that's a whole-flow UX figure across all platforms, not a Magento number, but it's a useful reminder that the expensive rebuild is rarely the first thing worth doing. Our Magento abandonment guide sets out the order of work for this layer.
4. Then, if the frontend really is the constraint, consider replacing it. Two credible paths in 2026:
Hyvä Checkout replaces the default checkout with an Alpine.js and Magewire implementation on the Tailwind-based Hyvä stack, removing Knockout, RequireJS and jQuery from the flow. Hyvä-ecosystem sources report page weight dropping from roughly 1 MB to 0.2 MB and JavaScript from 300 KB+ to around 30 KB. Those are vendor-adjacent figures, so treat them as directional, but the architectural change underneath is real and the ecosystem has matured considerably. Hyvä Default Theme reached 1.4.6 in May 2026, with a CSP-compliant variant that matters for payment-handling stores.
The honest caveat: Hyvä Checkout doesn't run Knockout, doesn't expose checkoutConfig to the frontend, and therefore doesn't run your existing checkout-side extension UI. Every shipping picker, payment widget and custom field needs a Hyvä-compatible version or a bridge module. The compatibility library now covers a large share of popular extensions, but "large share" is not "all," and the gaps are your project scope. Audit your extension list before committing, not after.
Edge Delivery Services with Luma Bridge, if you're following Adobe's roadmap: a modern storefront for everything except checkout, with checkout staying where it is. Pragmatic, and officially supported.
Is this getting better?
Partly, and not in the direction most people expect.
Adobe Commerce 2.4.9 is a maintenance release: real fixes, including several B2B checkout bugs and guest checkout validation, but not an architectural change. The Knockout checkout is not being rewritten. Adobe's investment is in Edge Delivery Services, and its official answer for checkout there is a bridge back to the Luma one.
The realistic outlook: the default Magento checkout will keep working and keep being maintained, and it will keep being what it is. Improvement is coming from the ecosystem (Hyvä and Magewire) rather than from core. Plan on that basis rather than on a rewrite that isn't scheduled.
Symptom to first action
Where to look first for the failures that come up most often.
| Symptom | Likely layer | First thing to check |
|---|---|---|
| Checkout renders, Place Order does nothing | Client-side JS error aborting component init | Browser console on a real device, not desktop Chrome |
| Long spinner after entering address | Carrier API calls, quote recollection | Number of active carriers; time estimate-shipping-methods |
| Slow to open checkout at all | JS bundle size, RequireJS, customer-data sections | Network panel; localStorage size |
| Only some customers affected | Configuration permutation | Segment by country, payment method, product type |
| Broke after an upgrade | jsLayout path moved |
Extensions plugging into LayoutProcessor |
| Payment step blank or frozen | CSP blocking the payment SDK | Console for CSP violations |
| Customers seeing wrong cart data | Cache configuration | Varnish/CDN rules for session-specific content |
| Field reappears after you removed it | Silent path mismatch in a plugin | The unset path against the actual runtime array |
When you want someone who has done this before
Most Magento checkout problems are diagnosable in days by someone who knows where to look, and expensive to guess at by someone who doesn't. The difference between "the payment step is slow" and "the payment step is slow because two extensions both plug into LayoutProcessor and one of them re-registers a totals collector" is the difference between a targeted fix and a rebuild.
Want to start on your own? The checkout audit checklist is the 77 checks we run, published in full, including the configuration items above that need no developer at all. For how we work on the platform more broadly, see Magento checkout optimization and Adobe Commerce checkout optimization.
Sources
Numbers in this space are unusually unreliable, so each figure above is attributed and dated in the text. Where sources conflict, the article says so rather than picking one.
- Adobe Commerce: Luma Bridge (Edge Delivery Services storefront documentation)
- Adobe Commerce: 2.4.9 release notes
- Inviqa: A technical guide to the Magento 2 checkout
- M.academy: Custom Magento checkout layout processors
- Community profiling: Magento 2 one-step checkout architecture pathology (2020, single store)
- Magevanta: Magento 2 customer data sections and localStorage performance
- Baymard Institute: Cart abandonment rate statistics