Shopify DevelopmentJune 13, 2026·Skaitymo trukmė – 5 min.·Miracle KaluMiracle Kalu

Why We Migrated Our Shopify Checkout From Liquid Hacks to Checkout Extensibility

A developer reviewing Shopify checkout customizations on multiple monitors.

The Checkout Time Bomb

For three years, our Shopify Plus checkout ran on a fragile patchwork of checkout.liquid overrides, third-party scripts, and Shopify Scripts that nobody wanted to own. The checkout looked bespoke, but the implementation was a liability. Every Shopify update meant a diff review that took half a day. Every new payment method required re-testing the entire funnel. And when Shopify announced the deprecation of checkout.liquid customizations for the Information, Shipping, and Payment pages, we had a hard deadline we could not negotiate.

We had two options: double down on the legacy approach and fight the platform, or treat checkout extensibility as a first-class architecture problem. We chose the latter. The migration took six weeks, removed 2,400 lines of Liquid and JavaScript, and gave us a checkout that is faster, more testable, and easier to extend.

What checkout.liquid Cost Us

The checkout is the highest-leverage page in any ecommerce store. A one-second slowdown can drop conversion by several percentage points. Our checkout.liquid file had grown into a monolith that controlled layout, tracking, fraud checks, upsells, and custom field validation. The problem was not that it was large; the problem was that it was ungovernable.

We had no clean separation between presentation and business logic. A/B tests were implemented by conditionally rendering script tags based on cart attributes. Trust badges were injected with DOM manipulation that broke whenever Shopify changed class names. And because the file was shared across markets, a change for one region risked regressions everywhere.

Worse, checkout.liquid runs synchronously on every checkout step. Adding a script that blocked rendering meant every shopper paid the price, not just the ones who needed the feature. We had optimized the frontend elsewhere, but the checkout was our blind spot.

What Checkout Extensibility Changes

Shopify Checkout Extensibility replaces the monolithic template with a composition model. Instead of one file that owns everything, you build checkout using three primitives:

  • Checkout UI extensions for rendering custom React components inside Shopify's checkout.
  • Web pixels and customer events for tracking and analytics.
  • Functions for server-side validation and pricing logic that runs inside Shopify's infrastructure.

The architecture is event-driven and scoped. A UI extension only loads on the step where it is needed. A function only runs when its specific condition is met. And everything is versioned, type-safe, and deployable through Shopify's infrastructure.

The Migration Strategy

We did not attempt a big-bang rewrite. The risk of breaking checkout for a live store is too high. Instead, we ran the old and new systems in parallel, migrating one concern at a time.

Step 1: Inventory the legacy surface

We audited every line in checkout.liquid and classified it into one of four buckets:

  1. UI changes that could become checkout UI extensions.
  2. Tracking and analytics that should move to web pixels.
  3. Pricing and validation rules that belonged in Shopify Functions.
  4. Dead code that could simply be deleted.

About thirty percent of the file fell into the last category. That alone made the audit worthwhile.

Step 2: Build the extension scaffold

We created a single Shopify app that owned all checkout extensions for the store. Each extension lived in its own directory with its own tests. The shared code, such as API clients and validation helpers, lived in a workspace package so that extensions could not accidentally depend on each other's internals.

// extensions/checkout-upsell/src/CheckoutUpsell.tsx
import { useExtensionApi, TextBlock, Button } from '@shopify/checkout-ui-extensions-react';

export default function CheckoutUpsell() {
  const { extension, lines, applyMetafieldsChange } = useExtensionApi();
  const { appMetafields } = extension;

  async function addRecommendedVariant(variantId: string) {
    await applyMetafieldsChange({
      type: 'updateMetafield',
      namespace: 'checkout_upsell',
      key: 'selected_variant',
      value: variantId,
      valueType: 'string',
    });
  }

  return (
    <TextBlock>Complete your kit with a matching item</TextBlock>
    // Render dynamic recommendation based on cart lines
  );
}

Step 3: Move business logic to Functions

Server-side validation was the scariest part to migrate because a bug could silently break pricing. We started with a simple metafield-based discount rule and added observability before scaling up.

// extensions/cart-validation/src/run.rs
use shopify_function::prelude::*;
use serde::Serialize;

#[derive(Serialize)]
struct Output {
  discount_application_strategy: String,
}

#[shopify_function_target
query = "src/run.graphql"]
fn run(input: ResponseData) -> Result<Output> {
  let eligible = input.cart.lines.iter().all(|line| {
    line.merchandise.as_ref().map_or(false, |m| {
      matches!(m.product.is_gift_card, Some(false))
    })
  });

  Ok(Output {
    discount_application_strategy: if eligible { "FIRST".to_string() } else { "ALL".to_string() },
  })
}

Step 4: Parallel validation and cutover

For two weeks, we logged the output of the new Functions and UI extensions alongside the legacy behavior. Any mismatch triggered an alert. Once the mismatch rate stayed below zero for seventy-two hours, we disabled the legacy checkout.liquid code for the migrated features.

What Broke and How We Fixed It

The first thing that broke was our A/B testing framework. It had relied on injecting different scripts based on checkout attributes. With checkout.liquid gone, we had to rebuild experiment assignment as a web pixel that fired at checkout start and pushed the variant into customer events. It was more work upfront, but the data quality improved because events were no longer lost when scripts failed to load.

The second surprise was performance. We expected checkout to get faster, but we underestimated how much. Removing blocking third-party scripts from the critical path cut our Largest Contentful Paint on the payment step by nearly forty percent. The React extensions are lazy-loaded and sandboxed, so they cannot block Shopify's core checkout rendering.

The third issue was organizational. Checkout extensibility forced us to split frontend and backend logic cleanly. Designers could no longer drop a script tag into a template and call it done. They had to think in terms of scoped extensions and event-driven tracking. That friction was intentional, and it improved the quality of new checkout features.

Takeaways

Treating checkout as a platform extension rather than a template override changes how you design, test, and deploy changes. The migration is not just a refactor; it is an architectural move.

Start with an audit and classify every legacy customization. Run old and new systems in parallel with real traffic before cutting over. Move business logic to Functions, presentation logic to UI extensions, and tracking to web pixels. Expect some A/B and analytics tooling to need rebuilding. The payoff is a checkout that is faster, safer, and aligned with Shopify's roadmap instead of fighting it.

Dalintis:

XLinkedIn
Miracle Kalu

Autorius

Miracle Kalu

Senior Full Stack Engineer

Patiko tai, ką perskaitėte?

Svarstau vyresniojo inžinieriaus pozicijas ir techninio konsultavimo projektus. Susisiekime.

Susisiekti →

Paskelbta 2026 m. birželio 13 d. · Skaitymo trukmė – 5 min.

Skaitykite toliau