Skip to content
← ALL WRITING

2026-04-23 / 10 MIN READ

Setting a performance budget for a DTC Shopify theme

The shopify performance budget I set on DTC theme builds: LCP, INP, JS, CSS, images, and fonts with the numbers and why each ceiling matters.

The checklist first. Six numbers I set as a Shopify performance budget on a DTC theme build, what I enforce them against, and what happens to the theme if you treat them as suggestions.

The shopify performance budget checklist

  • LCP on the PDP under 2.5s on a 4G throttled connection in Lighthouse, measured on a real product page with a hero image or video block.
  • INP under 200ms measured on real user data (CrUX or RUM), not on a synthetic emulator. The 75th percentile is the target.
  • Total JavaScript payload under 250KB gzipped on the PDP and homepage. App scripts count. Shopify's own tracking scripts count.
  • Total CSS payload under 60KB gzipped for the critical page. Theme-wide CSS is fine to exceed; the per-page critical slice has the ceiling.
  • Hero image under 150KB on mobile, served as WebP or AVIF, dimensions matching the rendered size within 20%.
  • Web fonts: two families maximum, subset, preloaded, with font-display: swap. Each family capped at 40KB WOFF2 per weight shipped.
the DTC Shopify performance budget
  • 01
    LCP (PDP)
    4G throttled, hero-dominant
    < 2.5s
  • 02
    INP
    p75 real user data
    < 200ms
  • 03
    JS total
    gzipped, apps included
    < 250 KB
  • 04
    CSS critical
    gzipped, first-paint slice
    < 60 KB
  • 05
    Hero image
    WebP/AVIF, sized to render
    < 150 KB
  • 06
    Web fonts
    subset, preloaded, swap
    2 families
0 of 6 constraints live
Six budget ceilings revealed in order. Treat as design constraints, not post-launch fixes.

These are the numbers I write into the engagement doc before any Liquid gets touched. They're tight enough that the theme has to be designed to hit them, not adjusted at the end to squeak under a Lighthouse score.

Why each number matters

LCP on the PDP under 2.5s

Largest Contentful Paint is the single metric that correlates most strongly with conversion on a product page. Past 2.5s, bounce rate starts climbing fast. Past 4s, it's a different kind of store.

On a DTC Shopify theme, LCP is almost always the hero image or video. Everything else is implementation detail: the image format, the size, the preload, the layout shift protection. If the hero takes two screens worth of work to get right, the hero takes two screens worth of work. Budget defended.

The 2.5s target is measured on 4G throttled, not on a fiber connection. Shopify's CDN is fast enough that fiber makes the theme look better than it is. 4G is what most buyers actually experience, especially in categories where the click-through comes from a paid social ad on mobile.

INP under 200ms

Interaction to Next Paint replaced FID in 2024 as a Core Web Vitals metric. It measures how long the main thread is blocked after a tap, click, or keypress before the next frame renders. Below 200ms feels instant. Above 500ms feels broken. In between, the buyer doesn't articulate it, but they abandon.

The two main things that kill INP on Shopify: app scripts that run on every interaction (quick-add to cart, variant picker, image zoom), and heavy theme JavaScript that hasn't been code-split. The budget forces you to audit both. Not every app needs to run on every page. Not every interaction needs 50KB of JS behind it.

Total JavaScript payload under 250KB gzipped

Shopify themes accumulate JavaScript invisibly. Every app adds a script. The theme adds its own. Meta Pixel, GA4, and whatever third-party analytics are configured each add their own. By the time you look, a PDP is loading 600KB of JS without doing anything specifically unusual.

250KB gzipped is tight. On a theme that uses no apps, it's generous. On a theme with 8 installed apps all injecting scripts, it means some of those apps need to go or need to be loaded on interaction instead of on page load. Both are fine answers; the budget forces the conversation.

Total CSS payload under 60KB gzipped

CSS doesn't block interactivity the way JS does, but it blocks rendering. Every additional stylesheet extends the critical rendering path. The budget is for the critical CSS (what the browser needs to render the first viewport), not the theme's total CSS, which can be larger and loaded asynchronously.

Getting under 60KB usually means inlining the critical slice and deferring the rest. Tailwind-in-Liquid setups trip on this because the default build ships a large stylesheet; PurgeCSS-equivalent tree-shaking at build time is what keeps the budget reachable.

Hero image under 150KB

The PDP hero is the single largest asset on the page. If it's 500KB, the page is slow before the browser reads a byte of JS. 150KB is achievable on a well-compressed WebP at the dimensions most mobile devices render (roughly 750x1000 at typical PDP crops).

Shopify's image transformation URLs (?width=750&format=webp&quality=80) will get you most of the way. The remaining discipline is sizing: the hero image should be requested at close to the dimensions it's rendered at, not at 2000px wide on a screen that shows it at 750. Using srcset with breakpoint-appropriate sizes is the way.

Two web font families, subset, preloaded

Web fonts are the sneaky LCP killer. If the PDP's hero title is in a custom font and the font hasn't loaded, the browser either shows invisible text (which delays LCP) or a fallback (which causes a layout shift when the real font swaps in). Both are bad.

Two families is the cap because past two, the cumulative font payload grows faster than the visual return. Subsetting removes characters you don't need (most stores don't need Cyrillic or Greek). Preloading tells the browser to fetch the font as early as possible. font-display: swap tells the browser to render the fallback immediately and swap when the real font arrives, which is the right tradeoff for almost every DTC context.

The 40KB per-weight cap is for WOFF2, the format every modern browser supports. If the designer's chosen font family is 120KB per weight, that's a font selection problem, not a performance problem; push back at the design brief stage, not at the build stage.

What happens if you skip any of these

Every skipped budget item has a predictable failure mode.

Skip the LCP budget and the PDP gets slow enough that paid traffic bounces. Meta and Google both derank slow landing pages for paid traffic. The LCP budget is the one that costs you money directly.

Skip the INP budget and the quick-add to cart feels sluggish. This one is harder to measure because it doesn't show up in Lighthouse the same way; you have to watch real user metrics. The symptom is buyers tapping the add-to-cart button twice because the first tap felt unresponsive.

Skip the JS budget and you accumulate apps you can't remove. Every app you add makes the JS budget harder to hit, which makes the performance audit harder, which makes the case for removing apps stronger. Without a budget, the app count just grows.

Skip the CSS budget and the time-to-first-paint degrades. The page looks blank for longer before anything renders, especially on slower devices. This one is silent; buyers don't know why the page felt slow, they just know it did.

The budget isn't a Lighthouse score to chase. It's a design constraint that shapes what the theme is allowed to do.

Skip the image budget and the hero becomes the bottleneck. Common failure: the designer picks a beautiful high-resolution hero, nobody runs it through the image transformation pipeline, the theme ships a 1.2MB hero as a PNG. LCP doubles. The designer is surprised; they shouldn't have to think about this, and the budget is what ensures someone else does.

Skip the font budget and the title area flashes. Fonts that arrive after the rest of the page cause a visible reflow. Buyers don't report this, but they notice it; the page feels "janky" without them being able to say why.

The budget isn't a Lighthouse score to chase. It's a design constraint that shapes what the theme is allowed to do. A theme designed with the budget in mind ends up leaner than a theme designed without one and then trimmed at the end; the constraint leads to different decisions at the brief stage. Which sections exist, how much JS each of them needs, whether a given effect is worth the payload it costs.

The budget was baked into the cross-browser video theme rebuild and has carried over to every theme build since. It pairs directly with the rubric in Shopify sections vs blocks because every section you add is a line item in the CSS and JS budget, and the rubric gives you the discipline to decide whether the section is earning its weight. It also ties back to the PDP conversion framework, where the anchor block's LCP is usually the specific number that forces the hardest tradeoff on a build. And if the theme uses video, the cross-browser video section walkthrough documents the IntersectionObserver pattern that keeps video off the critical path.

All of this fits under the DTC Shopify theme architecture for brands past 2M, which is where the three-layer model formalizes why each budget ceiling exists. When a brand is considering whether to stay on Liquid or go headless, the Hydrogen vs Liquid decision for mid-market DTC covers how the budget ceilings change shape on the other side of the decision.

The budget is baked into the Shopify Theme Starter defaults; the starter ships inside the six ceilings and includes the Lighthouse CI config that fails a PR if any of them are breached.

FAQ

How do I enforce the budget during development, not just at launch?

Add a Lighthouse CI run to the theme's pull request pipeline. It runs Lighthouse against a preview theme URL, compares the result to the budget, and fails the PR if any of the six numbers exceed their ceiling. The starter I ship includes a lighthouserc.js that encodes all six. Without this, budget drift is a matter of time.

What if an app I need exceeds the JS budget by itself?

That's a conversation, not a technical constraint. Either the app is critical and the budget moves (document why), or the app gets replaced by something smaller or by theme-native code, or the app gets loaded on interaction instead of on page load. I've done all three on different engagements.

Does the budget apply to the checkout page too?

Checkout extensibility has its own performance constraints that Shopify enforces. You don't control the page the way you control a theme template. The budget in this article is for theme-owned pages (home, collection, PDP, cart, custom landing), not for Shopify-owned pages.

What about Shopify's own vault scripts, like consent mode?

Count them in the budget. They're small individually but not zero. When you audit the JS payload, include all first-party Shopify scripts, all third-party app scripts, and your theme's scripts. The 250KB ceiling is the total.

Should the budget be tighter on Shopify Plus?

Plus stores often have more traffic and more paid media spend, so the cost of breaching the LCP budget is higher, not lower. I don't tighten the numerical ceilings on Plus, but I enforce them more aggressively (CI required, not optional) and audit more frequently.

Sources and specifics

  • LCP, INP, and CLS definitions are current as of web.dev's 2026 Core Web Vitals documentation. INP replaced FID in March 2024.
  • The 250KB JS ceiling is based on DTC Shopify themes I've shipped since 2023; tight but achievable on most themes without a re-platform.
  • The 150KB hero image ceiling assumes WebP or AVIF delivery; a comparable JPEG would be roughly 2x the size for similar perceived quality.
  • All numbers are measured against 4G throttled Lighthouse runs. Real user metrics (CrUX, RUM) are the ground truth for INP specifically.
  • The rubric deliberately ignores TTFB; on Shopify, the merchant doesn't control server response time enough to make TTFB a useful budget item.

// related

Product catalog

If you want to take this further, the products page has everything from self-serve audits to working sessions. Priced for where you are right now.

>See the products