10KB raw.
Gzip does not count.
The AssetSizeAppBlockJavaScript check reads the file on disk, not the bytes on the wire. A 14KB bundle that compresses to 4KB fails — and it fails at deploy, after you have written the feature.
What the check actually measures
Theme app extensions carry asset-size budgets, and the JavaScript one is 10KB per app block, measured uncompressed. This surprises people because every other performance conversation is about transfer size, where gzip or brotli is doing most of the work.
Here it is not. The check reads the file. Your minifier's report and your browser's network tab will both tell you a smaller, friendlier number, and neither is the number being enforced.
It also fails at deploy — not at build, not in dev. So the discovery arrives at the end.
Keep your source outside the extension
A src/ directory fails the deploy outright
Theme app extensions permit only assets/, blocks/, locales/ and
snippets/. A build directory inside the extension folder is not "ignored" — it is rejected.
So keep readable source somewhere else in the repo and emit the minified artefact into
extensions/<name>/assets/:
npx esbuild extension-src/widget.src.js \
--bundle --minify --format=iife \
--outfile=extensions/myapp/assets/widget.js
Then assert the budget in the same step, so it fails on your machine rather than at deploy:
SIZE=$(wc -c < extensions/myapp/assets/widget.js)
test "$SIZE" -lt 10240 \
|| { echo "over 10KB raw: $SIZE"; exit 1; }
Getting under 10KB
- Ship no framework10KB raw does not fit a framework and your feature. Plain DOM APIs are entirely adequate for a storefront widget.
- Move logic into LiquidAnything derivable at render time should be. Liquid output does not count toward the JavaScript budget at all — this is the single biggest lever.
- Do not inline data as JSPut configuration in
data-attributes on the block's root element and read it. Data in markup is free; data in a script is not. - Skip the polyfillsOnline Store 2.0 themes run in modern browsers. Target them.
- One block, one scriptThe budget is per block, so a second genuinely separate surface gets its own allowance rather than sharing yours.
The better answer: ship no JavaScript
The budget stops being a constraint if the storefront surface renders in Liquid. Store your data in app-owned metaobjects, read it in the block's Liquid, and the browser receives markup instead of a script that fetches and builds markup.
That also settles the Built for Shopify storefront requirement — you cannot reduce a Lighthouse score by more than 10 points with code you did not ship. Architecting for it costs nothing at the start and is expensive to retrofit.
Two related traps while you are in there: put {{ block.shopify_attributes }} on the block's
root element or the theme editor cannot map it, and never pipe a merchant setting through | t —
that is what produces "Translation
missing".
We build theme app extensions to this bar by default
Pure-Liquid storefront surfaces, budgets asserted in the build, and the design gates in from the first commit — because retrofitting them is the expensive path.
Book a call →