Magento

Why Magento Checkout Customization Is So Painful

 ·   ·  ~14 min read

Four tiles labelled XML, PHP, JSON and KO feeding one after another into a checkout tile, above the words: Magento checkout. Four layers. One uncached page. Why every change takes longer than it should.
XML, PHP, JSON and Knockout all take part in rendering one Magento checkout page, and none of it can be cached.
TL;DR - Key Takeaways
  • 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.

4 technologies render one checkout page: XML, PHP, JSON and Knockout
12 array keys deep, by string path, to remove one field from the shipping form
0 errors, warnings or log entries when that path is wrong: the field just comes back
6 files touched to add one validated custom field to the checkout

The short version

Four root causes, in the order they bite:

  1. 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.
  2. Every extension reaches into the same array, so conflicts are structural rather than accidental.
  3. 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.
  4. 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.xml and 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.

Common Magento checkout symptoms, the layer that usually causes them, and the first thing to check
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.

An engineer behind your login

Our manual checkout audit places real orders on real devices across your markets, profiles the actual requests, and identifies which layer is failing before anyone proposes what to build. You get a prioritised list with the reasoning attached, not a redesign proposal.

Book a manual checkout audit →

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.


Written by Checkout Experts

Checkout Experts is a checkout-only optimisation agency with 20 years of experience and 500+ projects across Shopify, WooCommerce, Magento, BigCommerce, and custom builds. Part of ZestIQ, est. 2006.

Common Questions

Magento Checkout Customization FAQ

Quick answers on why the Magento checkout is hard to change, why it is slow, and what to do about it. Want an engineer to find the failing layer? Book a manual checkout audit.

The checkout is a Knockout.js application whose structure comes from a deeply nested PHP array built out of XML and serialised to JSON. Customising it means mutating that array by string path, usually via a plugin on LayoutProcessor::process(). Wrong paths fail silently, every extension mutates the same object, and a single field change can touch XML, PHP, a Knockout view model, a template and a RequireJS mixin.
Checkout is excluded from full-page cache by design, so every request runs the full application stack. On top of that it fires a sequence of blocking REST calls that each recollect the quote, loads a large JavaScript bundle, makes a live API call per active shipping carrier, and loads separate JavaScript for each enabled payment method. Personalised data is fetched separately and cached in localStorage, which brings its own main-thread cost.
Usually not as a first move. Magento's default checkout is already a single URL with two sequenced steps. A one-step extension replaces the component every other checkout extension plugs into, and the documented failure modes are extension conflicts, broken payment and shipping integrations, and sometimes worse performance. Configuration changes and field reduction generally deliver more, at far lower risk.
It is the strongest available answer if the frontend is genuinely your constraint, because it removes Knockout, RequireJS and jQuery from the flow rather than layering on top of them. The cost is integration work: it does not run your existing Knockout-based checkout extensions, so each needs a compatible version or a bridge. Audit your extension list before deciding.
Not in the Luma stack. Adobe's investment is in Edge Delivery Services, and for those projects Adobe ships Luma Bridge specifically so teams can reuse the existing PHP checkout rather than rebuild it. 2.4.9 is a maintenance release. Plan on the Knockout checkout continuing to exist.
Longer than the same change on most platforms, and the variance is the real problem. Adding a validated custom field can touch six files across four technologies. Any estimate that does not include regression testing across your payment methods, shipping carriers and markets is not an estimate.
Get in Touch

Find the Failing Layer Before Anyone Proposes a Rebuild

An engineer places real orders on your live Magento checkout, on real devices, in the markets you sell into, and profiles the requests that are actually slow.

Usually responds within 24 hours
I want to