Skip to main content

Checkout UI extension performance

When a buyer navigates to checkout, Shopify downloads and runs your extension's code asynchronously and independently from the page. Your extension shows a loading state until it becomes interactive. The shorter that window, the better the buyer experience. This page lists best practices for optimizing the performance of UI extensions.


Anchor to Remove the need for external network callsRemove the need for external network calls

Every network call your extension makes at load time adds latency before the buyer sees your UI.

Anchor to Use Shopify data APIsUse Shopify data APIs

Shopify already provides the data most extensions need. Store your app's own data in metafields or metaobjects instead of fetching it from your server. Read buyer and checkout context through the checkout APIs, such as extension settings and localization APIs. Use the Storefront API only as a last resort, for catalog data the checkout APIs don't expose.

The following example reads an app-owned product metafield instead of fetching it from an external server:

import '@shopify/ui-extensions/preact';
import {render} from 'preact';

export default function extension() {
render(<Extension />, document.body);
}

function Extension() {
const metafields = shopify.appMetafields.value;
const badge = metafields.find(
(m) => m.target.type === 'product' && m.metafield.key === 'badge',
);

if (!badge) {
return null;
}

const {message} = JSON.parse(badge.metafield.value);
return <s-banner>{message}</s-banner>;
}

Anchor to If you need external network calls, make them fastIf you need external network calls, make them fast

If your extension needs data that you can't store in Shopify metafields or metaobjects, then keep your requests fast and audit your backend response times regularly. Run independent requests in parallel with Promise.all, and set a timeout so a slow upstream doesn't keep the extension hidden indefinitely. Treat shopify.query() calls the same way: they're network calls too. Fetch the data before first paint, so checkout's skeleton stays in place during the wait and the extension appears once, with no loading flash and minimal layout shift:

import '@shopify/ui-extensions/preact';
import {render} from 'preact';

export default async function extension() {
// Only load initial data from the network as a last resort.
const {data} = await shopify.query(
`{ products(first: 3) { nodes { id title } } }`,
);

render(<Extension products={data.products.nodes} />, document.body);
}

function Extension({products}) {
return (
<s-stack direction="block">
{products.map((product) => (
<s-text key={product.id}>{product.title}</s-text>
))}
</s-stack>
);
}

Anchor to Keep your bundle smallKeep your bundle small

Your extension's bundle must download, parse, and execute before it can render. Smaller bundles mean faster load times. Audit your bundle using the esbuild metafile CLI generation to see what's included and where the weight is.

Replace third-party utility libraries with the specific functions you need. For example, swap a date library like Day.js or Moment.js for the built-in Intl.DateTimeFormat. Move large static data to metafields or metaobjects instead of embedding it in your bundle.

If you use error reporting or analytics SDKs, then review your import configuration to minimize bundle impact. Use Shopify's localization APIs for translations instead of bundling i18n libraries.


Anchor to Avoid complex work at module scopeAvoid complex work at module scope

Code at module scope runs before your extension's render callback fires. Anything expensive here delays when the page can start rendering your extension. Don't run network calls, expensive parsing, schema validation, or third-party SDK initialization at module scope.

If you need a value that's expensive to compute, then initialize it lazily so it runs only when your extension actually uses it:

let _config;
const getConfig = () =>
(_config ??= JSON.parse(CONFIG_BLOB));

let _phoneRe;
const getPhoneRe = () =>
(_phoneRe ??= new RegExp('^\\+?[0-9]{7,15}$'));

Anchor to Read data where you render itRead data where you render it

The shopify global exposes checkout data as Preact Signals. When a component reads signal.value, it subscribes to updates. Read signals in the smallest, most specific component possible so that updates don't rerender parent components unnecessarily. If a value doesn't need to update the UI, then read it in an effect or event handler instead.

The following example reads the total cost in a leaf component so that only Total rerenders when the value changes:

import '@shopify/ui-extensions/preact';
import {render} from 'preact';

export default function extension() {
render(<Extension />, document.body);
}

function Extension() {
return (
<s-stack direction="block">
<s-heading>Order summary</s-heading>
<Total />
</s-stack>
);
}

function Total() {
const total = shopify.cost.totalAmount.value;

return (
<s-text>
{total?.amount} {total?.currencyCode}
</s-text>
);
}


Was this page helpful?