# 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.

- Page: https://granularity.tech/blog/what-lands-in-your-bundle
- Last reviewed: 2026-09-03

"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 the
`@feugene/unocss-preset-granular` preset 0.15.0. The method is stated with each
one, because a number without a method is a claim, not a measurement.

Since then the CSS pipeline has been replaced: the preset gave way to
[`@feugene/granum`](/docs/installation), and the figures moved with it. The
argument of this piece did not — the two prices are still unrelated — but do not
carry the numbers over. The current ones are on the [home page](/) and are
regenerated on every build rather than typed.

## 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 149 export paths and the core carries 84 components. Importing
from a subpath rather than the root barrel is what keeps the other 76 out:

```ts
// 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` is the one
place where granularity costs you attention:

```js
export default defineGranumConfig({
  engine: windEngine(),
  providers: ['@feugene/granularity'],
  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.
