Cutting checkout bundle sizes with web components
Checkout Blocks renders on a third of all customized checkouts, and its five UI extensions were built on React with Shopify's legacy Remote UI bridge, pinned to the 2025-07 API version. With Shopify moving the entire UI extension surface to remote-dom and Polaris web components, and deployments of older extension versions being blocked starting October 1, 2026, the team upgraded all five extensions to the 2026-01 API version running on remote-dom with Preact and Polaris web components.
The rewrite also moved the codebase to TypeScript. The results: transferred bundle sizes dropped 40% to 85% across the extension family, and every extension saw faster load times as measured by Extension Load Time (ELT).
Architecture before and after
All Checkout Blocks extensions consume from a single shared library called core, a folder alongside the extension code in /extensions. Core holds context providers for block config, product data, a GraphQL query client, analytics, and telemetry, plus shared hooks, helpers, and the rule engine that wraps every extension.
The headline change was swapping React for Preact. The legacy extensions rendered through @shopify/ui-extensions-react, which used react-reconciler to translate a React tree into remote-ui's serialized protocol. Remote-dom instead mirrors real DOM nodes between sandbox and host, so any DOM-rendering framework works — including none at all. Polaris web components are framework-agnostic custom elements, so no bespoke reconciler was needed. Preact provided React's hooks model at a fraction of the size, and the s-* components replaced the Polaris React tree.
Incremental migration, smallest first
The upgrade strategy was to move one extension at a time, starting with the smallest and lowest-risk, rather than a single large branch flipping everything at once. The team stood up a new core-next (Preact/TypeScript, remote-dom) alongside the legacy core. Each upgraded extension repointed to core-next; everything else kept running on core untouched. Once all extensions moved over, core-next folded back into a single shared core and the legacy code was deleted.
The first extension to ship, Static Content, set the pattern and established core-next. Each subsequent extension pulled in only the shared files it needed, leaving the fold-back and dead-code deletion as cleanup. This kept every change reviewable and battle-tested the patterns on simple extensions before the complex ones.
The conversion was heavily AI-assisted. An in-house agent skill handled the mechanical parts: React-to-Preact, swapping Polaris React components for s-* equivalents, and moving hooks to new APIs. Engineers focused on the judgment calls, which is where the hard problems turned out to be.
The 64KB budget
The 2026-01 remote-dom CLI enforces a hard 64KB gzip limit on each extension bundle. The extensions were more than double that — 300-356KB raw, around 100-112KB gzipped — so nothing could ship until each bundle fit. This single constraint shaped most of the engineering work.
Dropping react-reconciler (~89KB)
The single biggest win came for free by switching to Preact. This one change got the team most of the way to budget.
Replacing liquidjs (~73KB)
Checkout Blocks lets merchants write Liquid in block configurations, so a Liquid engine was needed in the bundle. liquidjs was too large for the budget. The team wrote a minimal Liquid parser internally nicknamed "droplet," built with AI assistance in a disciplined way:
- The agent was fed the official Liquid spec as the source of truth.
- A parity test suite was built from thousands of real-world merchant Liquid configurations pulled from the data warehouse, each paired with the exact output
liquidjsproduced. The fixtures file is over 42,000 lines. - The team iterated until droplet matched
liquidjson that corpus, documenting the handful of known, intentional differences.
The result is 13 KB gzipped vs. liquidjs's roughly 22 KB gzipped — about 9 KB, or ~40%, smaller — validated against production usage rather than synthetic cases.
Replacing dayjs (~12KB)
The custom-field extension used dayjs for date handling. It was replaced with a small in-house date utility scoped to exactly what was needed.
Keeping markdown-to-jsx (~15KB)
Several extensions render merchant-authored markdown via markdown-to-jsx, a React library. The tempting move was to swap in a 2KB markdown parser plus a custom HTML-to-Preact step, but that was high risk because it changed how markdown rendered for merchants.
Instead, the team kept markdown-to-jsx and ran it on Preact by aliasing React to Preact in the pnpm workspace catalog and in each extension's package.json. With the alias in place, markdown-to-jsx runs unmodified on Preact — no markdown behavior changed, and no new parser to maintain.
Note that the official Preact docs recommend aliasing to @preact/compat. The team reused its already-imported preact because markdown-to-jsx's reliance on React was minimal: no hooks, and it only imported createElement, Fragment, and cloneElement.
Bugs found and fixed upstream
Being among the first to push high-traffic extensions through new component and API paths exposed sharp edges, several of which would have hit other extension developers. The team drove fixes upstream.
ID collision (the one production rollback)
After the custom field extension shipped, overlays misbehaved wherever a checkout had more than one. A merchant could add a checkbox field with a label like "I agree to the Terms of Service" where the link opens a modal with the policy text. On checkouts with two such fields, clicking the second field's link opened the first field's modal.
The cause was ID collisions. Each trigger was paired with its modal through a shared ID (commandFor on the trigger, id on the modal), generated with Preact's useId(). That hook produces deterministic IDs scoped to a single render tree, so two instances of the same extension generated identical IDs. Polaris web components lean heavily on IDs for commandFor wiring. The issue was caught through monitoring, rolled back the same day, and fixed the next morning with a useStableId hook that generates a random, instance-unique ID.
Text alignment difficulties
The trickiest extension was dynamic-content, which shipped last. Its "benefits" block — a grid of cells with a centered icon, title, and description — came out of migration with text jammed to the left. The cause: textAlign had been removed from Paragraph and Heading in Polaris web components, leaving no way to center text in a multi-column grid.
The fix was a cross-functional call between engineers, designers, and product managers: re-expose styling control, or build a dedicated component? They aligned on re-exposing textAlignment on Paragraph in ui-extensions (PR #4455).
s-checkbox label slot refactor
The s-checkbox label only accepted a plain string, breaking the extremely common "Accept the [terms and conditions]" pattern with an inline link. The component was refactored to add a label slot accepting a string or an HTMLElement, sanitized to allow only s-text and s-link in ui-extensions (PR #4395).
Testing and rollout
Testing was done against real production configurations rather than synthetic ones. Each extension had a formal test plan with parity checks against real merchant stores spanning many block configurations, plus a suite of baseline e2e tests expected to pass pre- and post-upgrade.
Shared test infrastructure was built alongside the effort. A renderWithProviders helper wraps a component or hook in the full extension provider tree (telemetry, block context, query, analytics) with safe defaults, enabling fast, realistic remote-dom tests instead of hand-rolled mocks. Parity suites covered the risky swaps: a 42,000-line production-parity corpus for the Liquid parser and a 100-example suite from real warehouse data for the markdown migration.
Observability gaps were addressed pre-upgrade by adding metrics to establish success and failure rates, so regressions would be visible immediately. Releases went out incrementally, lowest-risk first, at most one extension per day to keep the blast radius small. The one rollback was caught fast by this monitoring and fixed forward the next day. Deprecated merchant-facing settings were removed from the checkout editor alongside go-live, and a forward-looking changelog was posted.
Hard-won lessons from a UI framework migration
The checkout migration produced a clear set of takeaways that apply beyond this specific project. Perhaps the most significant: enforcing a strict bundle limit, while painful, drove decisions the team had deferred. The 64KB ceiling made dependency replacements unavoidable, and that discipline translated directly into a faster buyer experience.
On the operational side, the team treated metrics as the primary rollback signal rather than relying on visual checks or intuition. Per-extension metrics, combined with cautious daily releases, provided a fast and reliable mechanism for detecting regressions and undoing them. This cadence meant problems were caught quickly, with a small blast radius.
Validation against production data proved critical. Shopify used a Liquid parity corpus and live-config validation on the benefits block to catch subtle breakage before it reached the billions of dollars in GMV flowing through checkout. The lesson: a "cosmetic" component gap can result in visibly broken checkouts at scale if you aren't testing against real-world inputs.
Where AI helped — and where it didn't
AI handled the mechanical workload well. An agent skill automated the repetitive conversion tasks, and pairing a detailed spec with a parity test suite made it possible to build a production-grade Liquid parser with AI assistance and high confidence. That approach left the engineering team free to focus on the judgment calls a migration of this scope demands.
The remaining friction points were the ones that deserve advance planning: bundle budgets, ID collisions, and component nuances. Those issues require human analysis and architectural foresight, not just automated transformation.
For teams undertaking a similar UI extension upgrade to remote-dom and Polaris web components, Shopify has published an AI toolkit and upgrade guides that automate much of the manual work and encode these lessons directly. The repetitive conversion work is effectively solved; the strategic planning around dependencies and validation is where teams should spend their effort.



