Skip to main content

Audit and optimize checkout UI extensions

Reduce checkout LCP and INP by identifying slow extensions, removing unused ones, and working with extension developers when a specific extension is the primary bottleneck.


Shopify checkout UI extensions load as JavaScript bundles on every checkout pageview, including extensions that are installed but not assigned to any active surface. Each extension adds network requests, JavaScript execution time, and potentially DOM nodes. An unused extension still initializes on load even if it renders nothing, so it costs download, parse, and execution time on every checkout. That cost is largest on mobile CPUs, and it varies with the extension, so measure it on your own checkout rather than assuming a figure.

The LCP element on checkout isn't available from RUM data. Instead, identify which extension is slowest to become visible to the buyer. Checkout instrumentation records two timings for each extension:

  • extension_visible: the time until the extension's content is visible on the page.
  • extension_buyer_usable: the time until the extension can accept buyer input.

extension_visible is the relevant signal for LCP impact, not extension_buyer_usable. An extension that renders visible content late is a candidate for being the LCP element. extension_buyer_usable measures interactivity separately, so an extension can be visible and trigger LCP without being usable.

Note

This applies to Shopify Plus merchants who use checkout UI extensions.


Anchor to Identify your slowest checkout extensionIdentify your slowest checkout extension

Work on a real checkout, not the checkout editor. The editor loads extra tooling and doesn't represent buyer conditions.

  1. Add a product to the cart and open the checkout in a Chrome incognito window, so extensions aren't served from a warm cache.
  2. Open DevTools, and in the Network panel select Disable cache, then set throttling to Slow 4G. Extension cost is hidden on a fast desktop connection.
  3. In the Network panel, filter on extensions and sort by Size, then by Time. Enable the Priority column. This gives you the per-extension bundle size and download time.
  4. Reload the checkout with the Performance panel recording. Stop the recording after the checkout is fully rendered.
  5. In the recording, select the LCP marker to see which element is the LCP candidate. If it belongs to an extension block, then that extension is your LCP bottleneck.
  6. Look at the Main track for long tasks that start after the checkout skeleton paints. Expand each one, and note the script URL at the top of the flame chart. Extension work appears as its own script, separate from checkout's own bundles.
  7. Repeat on mobile emulation and on a real phone. Extension cost scales with CPU, so the ranking can differ between devices.

Rank extensions by the time they take to render visible content, not by bundle size alone. A small bundle that waits on a network request can render later than a large one that renders immediately.

Anchor to Triage with visibility versus usabilityTriage with visibility versus usability

Use the two signals to decide who fixes what and in which order:

  • Visibility is the moment the extension's content appears on the page. A late-appearing extension is a candidate for the LCP element, and it's also what makes the checkout look unfinished. Triage these first.
  • Usability is the moment the extension is wired up and can accept buyer input. An extension can be visible long before it's usable. Late usability shows up as INP and as buyers tapping a control that does nothing yet.

In practice:

  • Content appears late, and the LCP element belongs to the extension: fix or remove the extension, or ask the developer to render a lightweight placeholder before fetching data.
  • Content appears quickly, but interaction is unresponsive: the problem is main-thread work after render, not the bundle download. This is an INP problem, so measure it with the Performance panel while interacting, not on load.
  • Content never appears: the extension failed to mount. It's still costing download and execution time for no buyer benefit, so remove it.
Note

Blocks that render outside the initial viewport, such as an order status upsell, can't be the LCP element. Deprioritize them even when their bundle is large.

Anchor to Remove unused extensionsRemove unused extensions

In the Shopify admin, navigate to Settings > Checkout, and then click Customize. In the checkout editor, identify extensions with no active surface assignments. An extension that initializes but renders nothing still consumes main-thread time on every checkout load. Remove it if it's not necessary.

Anchor to Consolidate overlapping extensionsConsolidate overlapping extensions

Multiple extensions from different apps serving overlapping functions, such as two trust badge apps or multiple upsell blocks, add cumulative load cost. Evaluate whether you can achieve the same outcome with fewer installed extensions.

Anchor to Engage extension developers for slow extensionsEngage extension developers for slow extensions

If an extension is slow to become visible, then share timing data with the developer. The phase breakdown identifies where the time is spent: slow JavaScript download, slow parse and compile, or slow render. The developer controls the bundle size and render logic. These aren't changes the merchant can make directly.

Shopify doesn't publish a bundle-size limit for checkout UI extensions, so rank the bundles against each other on your own checkout. The largest bundle, and any bundle that's an outlier next to the rest, is the one to raise with its developer first.

Anchor to Identify which extension placement is slowestIdentify which extension placement is slowest

When one app has multiple checkout block instances, the performance impact varies by placement. Check which specific block placement has the slowest visibility time and review the content complexity of that block. It might be fetching external data or rendering more markup than others.


Anchor to List extension bundles by size and load timeList extension bundles by size and load time

Run this in the DevTools Console on a loaded checkout to get a sortable table of every extension asset the page fetched, with its transfer size and when it started:

Rank extension assets

DevTools Console

console.table(
performance
.getEntriesByType('resource')
.filter((entry) => entry.name.includes('/extensions/'))
.map((entry) => ({
asset: entry.name.split('?')[0].split('/').slice(-2).join('/'),
transferKB: Math.round(entry.transferSize / 1024),
decodedKB: Math.round(entry.decodedBodySize / 1024),
startedAtMs: Math.round(entry.startTime),
durationMs: Math.round(entry.duration),
}))
.sort((first, second) => second.decodedKB - first.decodedKB),
);

Two caveats. transferKB reports 0 for cross-origin assets served without timing headers, so compare decodedKB instead. Assets fetched inside an extension's sandbox don't appear in the top-level document's resource timeline, so if the table is empty or short, fall back to the Network panel filtered on extensions, which shows every request regardless of frame.

Record the table before and after each change. It's the cheapest before-and-after evidence you can hand to an app developer.

Anchor to Confirm where an extension is allowed to renderConfirm where an extension is allowed to render

Extension placement comes from the targets that the developer declares in the app's shopify.extension.toml. When you ask a developer why their extension loads on a step where you don't need it, this file is the answer:

Target assignment

shopify.extension.toml

api_version = "2026-04"

[[extensions]]
name = "delivery-note"
handle = "delivery-note"
type = "ui_extension"

[[extensions.targeting]]
target = "purchase.checkout.delivery-address.render-after"
module = "./src/Checkout.tsx"

[[extensions.targeting]]
target = "purchase.thank-you.block.render"
module = "./src/ThankYou.tsx"

Each target is a separate render surface with its own module. An extension that declares a checkout target and a Thank you target runs on both, so if you need only the post-purchase behavior, ask the developer to drop the checkout target rather than accepting the load cost on the highest-value page.


  • Open the checkout in Chrome DevTools and check which element is the LCP candidate in the Performance panel. If it belongs to an extension block, then that extension is the first thing to fix or remove.
  • Compare checkout LCP and INP before and after removing unused extensions. Test on a throttled mobile profile, not on desktop, and compare the same checkout step each time.
  • Check extension bundle sizes in the Network panel, filtered on extensions, and record them so that you have a baseline for the next audit.
  • Interact with each extension as soon as it appears. If it's visible but doesn't respond, then the gap between visibility and usability is the problem to report.
  • Confirm that every extension mounts. An extension that downloads but never renders is pure cost.
  • Rank blocks that render in the initial viewport ahead of blocks that render outside it in your remediation list.

Repeat the audit quarterly, and after installing or removing any checkout app. Each time, confirm that:

  • Every installed checkout extension is assigned to an active surface. Uninstall the rest.
  • No two extensions serve the same purpose, such as two trust badge blocks or two upsell blocks.
  • No extension declares a checkout target when its value is only on the Thank you or Order status page.
  • No extension declares the network_access capability unless it demonstrably needs external data at checkout time. External calls add a serial dependency before content can render.


Was this page helpful?