What actually lands in your bundle when you import one component
A weight breakdown of one design system — what a component costs, what a second theme costs, and why CSS and JS do not correlate.
“It’s only a button” is how bundle budgets die. The honest question is not what a component weighs but what arrives with it — the framework glue, the styles of everything the sheet was told to emit, the second theme nobody asked for. This post is the answer for one design system, measured rather than asserted.
Every figure below was taken on 2026-08-29 against core 0.38.0 and preset 0.15.0. The method is stated with each one, because a number without a method is a claim, not a measurement.
Two costs, and they are unrelated
A component costs you twice: once in CSS, once in JavaScript. The instinct is that a heavy component is heavy in both. It is not.
| Component | Styles it adds to the sheet | Its own code |
|---|---|---|
GrCard | 223 B | 1 870 B |
GrDialog | 318 B | 9 789 B |
GrDataTable | 705 B | 16 243 B |
GrSelect | 2 335 B | 19 819 B |
Gzipped, framework external. GrDialog costs 30× more code than styles;
GrSelect only 8×. The reason is boring and worth knowing: styles scale with how
many visual states a component has, code scales with how much behaviour it
carries. A dialog is a plain box with a focus trap. A select is a plain box with
a listbox, filtering, keyboard navigation and a floating panel.
The practical consequence: you cannot estimate a component’s weight by looking at it. The lightest in this package is 1 455 B and the heaviest 19 819 B — a spread of 13.6×, and the visual difference between them is not 13.6× of anything.
What the first component actually costs
Here is where “it’s only a button” is genuinely wrong, and where it is genuinely right.
| Sheet | Gzipped |
|---|---|
| One component, one theme | 7 504 B |
| One component, two themes | 8 250 B |
| Every component of the core, two themes | 17 430 B |
The first component costs 7 504 B and the remaining 76 cost 9 180 B between them. That is not a paradox: the first one pays for the foundation — the token declarations, the base layer, the preflight — and everyone after it pays only for its own utilities.
So “it’s only a button” is wrong about the first button and roughly right about the second. If you are adding a design system for one component, you are paying for a design system. If you are adding the tenth, you are paying for a component.
The second theme costs 746 bytes
Not 746 kilobytes, and not a second sheet: 746 B, because a theme is a block of
custom-property values under a selector, not a copy of the styles. Every rule
that uses var(--gr-bg) is emitted once regardless of how many themes define it.
This is the part people mis-budget in both directions. Teams drop dark mode to save weight it never cost, and teams add six brand themes assuming they are free — they are, in CSS, and they are not free in review, contrast checking or design decisions. The byte count is the least of it.
Where granular imports actually help
The package declares 136 export paths and the core carries 77 components. Importing from a subpath rather than the root barrel is what keeps the other 76 out:
// The whole barrel is reachable from here — tree-shaking has to prove
// what is unused, and it cannot always prove it through re-exports.
import { GrButton } from '@feugene/granularity'
// One component and its own graph. Nothing to prove.
import { GrButton } from '@feugene/granularity/components/GrButton'Both lines work. The second one is smaller in every bundler and identical in the best one, which is the whole argument for it: the win is not that it is faster when everything goes right, it is that it does not depend on everything going right.
The CSS side is not tree-shaken at all — it is generated. The sheet contains
what the config was told to emit, which is why narrowing components in
uno.config.ts is the one place where granularity costs you attention:
const granular = {
providers: [granularityProvider],
components: [{ provider: '@feugene/granularity', names: ['GrButton', 'GrCard'] }],
themes: { names: ['light', 'dark'] },
}Leave components out and you get all of them — 17 430 B instead of 8 473 B.
That is a real difference and a bad default to inherit by accident.
What this does not tell you
Three honest limits.
These are gzipped bytes, not parse time. 19 819 B of GrSelect costs more to
execute than 19 819 B of markup, and neither number tells you about the third
render.
The framework is external in every JS figure. Vue is not in these numbers, because it is not in them for you either — you already have it. If you do not, add it, and the design system stops being the interesting part of your budget.
Zero runtime dependencies is a property of the core, not of the family. The core declares 0 runtime dependencies; companion packages that wrap a third-party editor or highlighter declare theirs, and they are listed on each package page.
The one number worth remembering
Not 7 504, and not 13.6×. It is this: the first component of a design system costs about as much as all the rest together. Budget the decision, not the import.