구성 요소는 실제로 얼마나 큽니까? 저는 세 가지 방법으로 측정했고 세 가지 답을 얻었습니다.

작성자

카테고리:

← 피드로
DEV Community · Simon · 2026-08-18 개발(SW)

I wanted honest bundle sizes for my UI library and found out most size numbers are measured wrong. Here’s what actually happens.

I have been building ShipUI for about 3 years now, a zoneless signal based Angular UI library with zero runtime dependencies. I wanted to put honest per component sizes on the site, like “this is what a button actually costs you”.

Turns out that is way harder than i thought, because “how big is this component” has like 3 different answers depending on how you measure. And the numbers are not close, we are talking 3x apart. So i went down the rabbit hole and figured i’d share what i found.

3 ways to measure, 3 different numbers

Take one component, lets say a sortable directive. Depending on how i measured it i got:

  • 6.0 kB gzipped
  • 9.8 kB gzipped
  • something smaller again once you already use the library

None of these are wrong. They just answer different questions. The trap is you pick the smallest one, slap it on your landing page and call it “the bundle size” 🤣

Way 1: minify the published bundle on its own

First thing i did was measure each components own bundle, minified, with all its imports left external:

const result = await Bun.build({
  entrypoints: [componentBundlePath],
  minify: true,
  target: 'browser',
  external: ['*'], // only this components code, not its imports
});
const gzip = gzipSync(Buffer.from(await result.outputs[0].arrayBuffer())).length;

Enter fullscreen mode Exit fullscreen mode

The external: ['*'] is the important bit. It means shared Angular code, rxjs, my own internal utils, none of that gets counted. You get the size of the code i actually wrote for that component and nothing else.

Its a real number but its the floor. It tells you how much code i wrote, not how much your app grows when you use it. Any component that leans on shared stuff is gonna cost more than this in real life, because that shared stuff has to come from somewhere.

Way 2: measure the real bundle delta through the Angular CLI

Ok so the question you actually care about is: if i add this component, how many bytes does my bundle grow? So just measure that.

Build an empty app, note the size, add one component, build again, take the difference:

// empty zoneless app
const baseline = await build('');

// same app + one component
const withComponent = await build(
  `import * as mod from '@ship-ui/core/ship-sortable';
   (globalThis as any).__keep = mod;`
);

const delta = withComponent.gzip - baseline.gzip;

Enter fullscreen mode Exit fullscreen mode

Every build runs the real production pipeline, AOT, esbuild, tree shaking, same thing your app uses. Subtracting the baseline throws away Angulars fixed runtime (bootstrap, change detection, all that) so what is left is the actual cost of that one component.

The namespace import + the __keep reference is there on purpose, it keeps every export of the component alive so tree shaking cant quietly drop half of it and hand me a nicer number.

This is the number i trust for “what does adding this cost”. Its bigger than way 1, 9.8 kB instead of 6.0 kB, and that gap is the whole point. The difference is the shared code the component drags in that the empty app didnt have.

Now the fun part, the thing nobody shows you

Ok so check this out. Here are 2 components measured on their own, each against an empty app:

Component gzip delta (measured alone) button-group ~11.5 kB tabs ~11.2 kB

A button group is not 11 kB of code. So what is in there??

Both of them extend the same base class, a selection group thing that does roving focus, aria wiring and keyboard nav. That base class pulls in a shared keybindings service and some internal utils. So when i measure button-group ALONE, that whole thing gets charged to button-group. Then i measure tabs alone and the exact same thing gets charged AGAIN to tabs.

So the naive thing, looking at a size table and adding up the rows, is just wrong. It counts the shared code once per component instead of once per bundle.

In a real build that base class is in there once. First component that needs it pays for it, every component after that only adds its own little bit on top.

You can actually measure this. Build both together and compare to the sum:

  • button-group alone: ~11.5 kB
  • tabs alone: ~11.2 kB
  • naive sum: ~22.7 kB
  • both together in one build: ~12.0 kB

Together they come out to about ~12.0 kB, not 22.7, because the base class + the keybindings service + the shared utils are in the bundle once instead of twice. So the second components real cost is under 1kb.

And this is the bit i find cool, its kind of backwards from what you’d expect. The MORE of the library you use, the cheaper each new component gets. The shared stuff amortizes. A size table measured component by component completely hides this because every row re-pays the shared cost.

So how should you report size

If you wanna put per component numbers out there, few things keep you honest:

Measure the real delta not the isolated code. Way 2 is what people actually experience. And just say up front that the numbers dont include Angulars base runtime, because thats what someone is gonna assume anyway, might as well say it first.

Dont sum the rows. The per component numbers are fine on their own but you cant add them up, the sum double counts the shared code. So say that, and point people at a real combined number instead.

Report it as a floor + a small cost per component. Thats the honest shape. Nobody imports exactly 1 component into an otherwise empty app anyway.

The method if you wanna steal it

None of this is ShipUI specific. If you maintain an Angular lib and want numbers you can actually defend:

  1. A tiny harness app that boots an empty zoneless component
  2. A script that rewrites 1 import, runs ng build in prod mode, diffs against the empty baseline
  3. For the headline number, measure a realistic COMBO of components as one build, not a sum of isolated ones

The isolated numbers tell you what you wrote. The combined build tells you what people ship. Publish the second one and give people the script so they can check you. A bundle size claim you cant reproduce is just marketing, one with the script attached is proof.

The actual numbers

Ok so here is the whole thing, every component in ShipUI. Im showing both numbers side by side so you can see the gap yourself:

  • isolated gzip is way 1, just the components own code (external: ['*'], minified + gzipped)
  • real gzip delta is way 2, the real ng build growth over an empty zoneless baseline

Remember what i said, dont add the delta column up, each row re-pays the shared code so the sum is way more than a real app ships. This is “what does this cost if its the only ShipUI thing in your app”, the worst case per component. And notice the gap between the 2 columns, thats the shared code that way 1 doesnt count and way 2 does.

The big ones at the top are exactly what youd expect, a code editor and a video engine are supposed to be chunky. The bottom is where it gets silly, an icon for under 2 kB of its own code.

Component isolated gzip (way 1) real gzip delta (way 2) ship-code – 98.6 kB ship-editor 47.2 kB 75.0 kB ship-video 17.3 kB 59.0 kB ship-datepicker 7.4 kB 40.4 kB ship-select 7.0 kB 31.4 kB ship-color-picker 7.6 kB 27.8 kB ship-table 8.8 kB 25.1 kB ship-spotlight 4.9 kB 24.7 kB ship-menu 4.8 kB 24.4 kB ship-tree 6.2 kB 24.1 kB ship-spreadsheet 6.8 kB 23.8 kB ship-blueprint 8.0 kB 23.3 kB ship-form-field 4.6 kB 20.2 kB ship-alert 3.6 kB 16.6 kB ship-file-upload 1.8 kB 16.4 kB ship-dialog 4.3 kB 15.9 kB ship-popover 3.5 kB 15.1 kB ship-checkbox 2.1 kB 14.3 kB ship-toggle 2.4 kB 13.7 kB ship-radio 2.1 kB 13.5 kB ship-virtual-scroll 3.4 kB 12.6 kB ship-sidenav 2.8 kB 12.2 kB ship-stepper 2.4 kB 11.8 kB ship-range-slider 3.7 kB 11.5 kB ship-button-group 1.8 kB 11.5 kB ship-tooltip 3.3 kB 11.2 kB ship-toggle-card 1.5 kB 11.2 kB ship-tabs 1.5 kB 11.2 kB ship-theme-toggle 1.4 kB 10.0 kB ship-sortable 6.0 kB 9.8 kB ship-list-item-swipe 2.8 kB 9.3 kB ship-accordion 2.0 kB 8.0 kB ship-icon 1.4 kB 6.9 kB ship-chip 1.4 kB 6.7 kB ship-button 1.5 kB 6.5 kB ship-event-card 0.9 kB 6.1 kB ship-kbd 1.4 kB 6.1 kB ship-card 0.9 kB 5.8 kB ship-progress-bar 1.5 kB 5.1 kB ship-a11y-keybindings 2.9 kB 4.5 kB ship-spinner 0.9 kB 3.8 kB ship-file-drag-drop 0.6 kB 3.8 kB ship-list 2.0 kB 3.6 kB ship-divider 0.5 kB 2.1 kB ship-table-filter-bar 0.4 kB 2.0 kB ship-input-mask 0.8 kB 1.8 kB ship-prevent-wheel 0.4 kB 0.9 kB ship-a11y-announcer 0.7 kB 0.4 kB

Look at button-group, 1.8 kB of its own code but 11.5 kB delta. Thats the selection group base class + keybindings service showing up. Tabs is the same story, 1.5 kB on its own, 11.2 kB delta, because its dragging in the exact same shared stuff. Put them in the same app and that shared part is paid once, not twice.

Again, in a real app a bunch of these share code so the numbers you actually ship are lower than reading the delta column straight. But i’d rather show you the honest worst case than a cherry picked best case ✌️

I build ShipUI in the open, zoneless, signal based, zero runtime deps. The size scripts are in the repo if you wanna poke holes in them, and honestly if you find a hole in my method let me know in the comments ✌️

원문에서 계속 ↗