[Feature] report file size for ui extensions on build and dev#7205
[Feature] report file size for ui extensions on build and dev#7205robin-drexler wants to merge 1 commit intomainfrom
Conversation
abf63b1 to
b75d5eb
Compare
47bd3d6 to
675a667
Compare
675a667 to
25e7d56
Compare
There was a problem hiding this comment.
Pull request overview
Adds reporting of UI extension bundle sizes (original + compressed) to build and dev workflows to help developers understand proximity to Shopify’s 64kb compressed limit.
Changes:
- Append original and deflated-compressed bundle size information to UI extension build output (
app build). - Append bundle size information after successful rebuilds during
app devfor esbuild-managed extensions. - Introduce
bundle-sizeutility + unit tests, and add a changeset for@shopify/app.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/app/src/cli/services/dev/app-events/app-event-watcher.ts | Adds bundle size suffix to “Build successful” messages on rebuild during dev. |
| packages/app/src/cli/services/build/extension.ts | Adds bundle size suffix to UI extension “successfully built” output. |
| packages/app/src/cli/services/build/bundle-size.ts | New utility to compute raw + deflate-compressed sizes and format as a suffix. |
| packages/app/src/cli/services/build/bundle-size.test.ts | Adds unit tests for raw/compressed sizing and formatting behavior. |
| .changeset/tricky-results-roll.md | Declares a minor release for @shopify/app to ship the feature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const [rawBytes, content] = await Promise.all([fileSize(filePath), readFile(filePath)]) | ||
| const compressed = await deflateAsync(Buffer.from(content)) |
There was a problem hiding this comment.
getBundleSize calls readFile(filePath) with the default {encoding: 'utf8'} and then wraps it in Buffer.from(content). For accurate byte-level compression (and to keep this utility safe for non-text/binary bundles), read the file as a Buffer explicitly (e.g., pass an options object that yields a Buffer) and deflate that buffer directly.
| const [rawBytes, content] = await Promise.all([fileSize(filePath), readFile(filePath)]) | |
| const compressed = await deflateAsync(Buffer.from(content)) | |
| const [rawBytes, content] = await Promise.all([fileSize(filePath), readFile(filePath, {encoding: null})]) | |
| const compressed = await deflateAsync(content) |
d8e7d01 to
dac58c5
Compare
| * Uses the same compression algorithm as the Shopify backend (Zlib::Deflate.deflate). | ||
| */ | ||
| export async function getBundleSize(filePath: string) { | ||
| const [rawBytes, content] = await Promise.all([fileSize(filePath), readFile(filePath)]) |
There was a problem hiding this comment.
does this need to wrap in a try/catch in case we exhaust the buffer? i'm unfamiliar with how we run the build and compression here
There was a problem hiding this comment.
the only actual usage of this already wraps it in a try / catch. should be good for now? https://github.com/Shopify/cli/pull/7205/changes/BASE..dac58c54bb75c3790b3fcdd8d7f63424aa4d135e
ryancbahan
left a comment
There was a problem hiding this comment.
seems reasonable to me. left a q about the read from buffer on the file, but i'm low context there
craigmichaelmartin
left a comment
There was a problem hiding this comment.
Quick review but looks great!
| * Uses the same compression algorithm as the Shopify backend (Zlib::Deflate.deflate). | ||
| */ | ||
| export async function getBundleSize(filePath: string) { | ||
| const [rawBytes, content] = await Promise.all([fileSize(filePath), readFile(filePath)]) |
There was a problem hiding this comment.
💡 Improvement: fileSize performs a separate stat syscall, but the file content is already being read by readFile in the same Promise.all. Since the content is in memory, the raw byte count can be derived from the buffer directly, eliminating one filesystem call and a minor TOCTOU window (file could theoretically change between stat and read). For UTF-8 JS bundles produced by esbuild, Buffer.byteLength(content, 'utf8') equals stat.size.
Suggestion: Replace the parallel fileSize + readFile with a single read and derive rawBytes from the buffer:
| const [rawBytes, content] = await Promise.all([fileSize(filePath), readFile(filePath)]) | |
| const content = await readFile(filePath) | |
| const rawBytes = Buffer.byteLength(content, 'utf8') | |
| const compressed = await deflateAsync(Buffer.from(content)) |
This also removes the fileSize import if no longer needed elsewhere.
There was a problem hiding this comment.
good idea. will adjust
| await extension.buildValidation() | ||
|
|
||
| options.stdout.write(`${extension.localIdentifier} successfully built`) | ||
| const sizeInfo = await formatBundleSize(extension.outputPath) |
There was a problem hiding this comment.
💬 Suggestion: When an extension has assets, each is bundled to a separate output file via bundleExtension, but formatBundleSize only reports the size of extension.outputPath (the main entry point). If the 64 KB compressed limit applies to the total extension payload (main + assets), developers could be misled about how close they are to the limit. Worth verifying whether the backend counts total extension size or just the main bundle.
Suggestion: Consider also reporting asset sizes or a total size so developers get a complete picture. If the 64 KB limit only applies to the main bundle, a brief comment clarifying that would help future maintainers.
There was a problem hiding this comment.
Good question. The main bundle JS size is checked individually, but some extensions have more files like should_render or tool files that are also validated by the backend. I think I'd like to start with reporting just the main entry point since this is the file most people seem to have issues with. We can add reporting for other files later on?
dac58c5 to
dbcdf9f
Compare

WHY are these changes introduced?
With 2025-10 version, we introduced strict 64kb limits for ui extensions. It's been difficult for developers to analyze their file sizes. We've already shipped a change that emits esbuild metafiles and allows developers to find out what's in their bundles.
The issue is that those sizes are being reported uncompressed. And while this is useful to sport large libraries etc, it doesn't give you the full picture and is confusing to some folks.
WHAT is this pull request doing?
This PR now shows extension file uncompressed and compressed sizes when running
app buildorapp devmaking it easier to spot when an extension is getting too big.We are not (yet) adding any size limit warnings. That information is currently afaik only available in the backend when you try to upload an extension. We can look into this at a later point. Just adding the information itself imo is already pretty useful.
build
dev
rebuild

How to test your changes?
pnpm shopify app build --path=/path/to/your/appinside the cli repoPost-release steps
Measuring impact
How do we know this change was effective? Please choose one:
Checklist