# Performance

> Granularity is the product's main promise. This page shows what it is measured in and how to verify it yourself.

- Page: https://granularity.tech/docs/performance
- Last reviewed: 2026-10-16

The promise is short: **the bundle grows with what you use, not with what the
package contains**. Below is what backs it, what it actually costs and how to
measure it in your own project without taking anyone's word.

## Two sides, and they do not correlate

A component's weight is code plus styles, and the two live apart:

| Component | CSS added to the sheet | JS, the component itself |
|---|---:|---:|
| `GrCard` | 235 B | 2 779 B |
| `GrDialog` | 670 B | 9 989 B |
| `GrDataTable` | 2 911 B | 36 431 B |
| `GrSelect` | 3 037 B | 23 216 B |

A component heavy in code may add almost nothing to the sheet, and the other way
round. So "the weight of a component" is not a single number, and any table with
one column is lying. The numbers are the same as on the home page: a measuring
script takes them from the core, not a person.

## Code: a subpath instead of a barrel

```ts
// one component and its graph
import { GrButton } from '@feugene/granularity/components/GrButton'

// the whole package — granum 2 rejects this import: ComponentRootImportError
import { GrButton } from '@feugene/granularity'
```

The top line is granularity itself. A subpath's weight is the gzip of everything
it pulls from `dist`, nested components included, but without external packages
and without CSS. The lightest subpath is the `GrButtonGroup` wrapper: on its own
it weighs 1.5 kB, but it is never used without its buttons — `GrButton` arrives
in it through a slot and is imported from its own subpath. The heaviest is
`GrDataTable`, at 115.8 kB. The whole package from the root is 655.4 kB.

:::callout{type="warning"}
**Subpath weights must not be added up.** The shared chunk is counted afresh in
every row but paid for once: the sum of the five heaviest rows is 487.1 kB, while
together they weigh 251.9 kB. A set is measured as a union rather than a sum —
and that is an upper bound: the application's bundler shakes the tree further and
minifies again.
:::

The full table for every subpath is generated from the built `dist` on every
release — it ships inside the package and appears on each component's page in the
[catalogue](/components).

## Styles: you pay for what you selected

CSS is not imported as files — granum assembles it for exactly the components you
listed in `components` and for their dependencies. What each component needs — its
classes, its own `styles.css`, its tokens — is recorded in the package manifest,
and the sheet is built from the manifests rather than from `node_modules`. The
spending breaks down like this:

:::steps
1. **The first component brings the foundation** — the tokens, the base layer, the preflight, the theme. That is a one-off.
2. **Each one after it adds its own few hundred bytes** — from two hundred for a card to three kilobytes for a select.
3. **The second theme costs about one and a half kilobytes**, not double: themes differ in role values, not in rules.
:::

Not listing `components` at all is a working option: every component of the
provider then reaches the sheet. The difference between "everything" and "one" on
the shared sheet is roughly twofold rather than a hundredfold, because the
foundation is shared. Granular styles are worth having, but the effect is more
modest than it is for code, and saying so honestly is cheaper than explaining it
later.

## Runtime

- **Zero runtime dependencies in the core.** The only mandatory external library
  is `@floating-ui/dom`, and it is declared as a peer dependency of the
  application so that it never arrives as a second copy.
- **A component's CSS arrives through the manifest, not through the chunk.** The
  package's JS chunks import no styles: granum takes the component's `styles.css`
  from the manifest and places it in the `granum.components` layer — only for the
  selected components. There is no separate style import, and that is not a
  convenience but a condition: otherwise granularity would break on the first
  forgotten import.
- **The token reference and the test helpers are separate subpaths**
  (`/tokens`, `/testing`), and the core barrel does not import them. Code
  highlighting is not in the core at all: it lives in
  `@feugene/granularity-code/highlight`.

## How to measure it yourself

:::steps
1. Build the application with one component and measure the gzip of the emitted CSS and JS.
2. Add a second component and measure again. The difference is its real cost in your project.
3. Compare against the barrel: granum will not let a root import through, so switch the guard off for the measurement (`js: { guard: 'off' }`) and repeat. The difference is what the subpath spares you.
:::

The numbers on this site are taken by scripts rather than by building an
application. In the table at the top of the page, granum builds the sheet from
the core's manifest with no application sources, and esbuild builds the code from
the component's subpath with `vue`, `@floating-ui/dom` and `@feugene/fint-i18n`
left external. So treat them as an order of magnitude: your result depends on the
bundler version, the minifier settings and on what else is in the graph.

## What will speed nothing up

- **`importStyle: true` on the auto-import resolver.** The core does not need it:
  granum has already taken the components' styles from the manifests, and the
  core publishes no `components/<Name>/styles.css` subpaths — the option, once
  enabled, would break the build.
- **Manual `styles/*.css` imports** on top of the granum sheet. They add weight
  rather than removing it: granum has already placed the tokens, the base and the
  themes in its layers, and a file imported outside the layers will also override
  them in the cascade.
- **Narrowing `themes.names` to one theme** to save space. The second theme costs
  about one and a half kilobytes — this is not the place to give up theme
  switching.
