Performance

  • Concepts
  • 5 min
  • 1 snippet

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

Reviewed 2026-10-16

On this page

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:

ComponentCSS added to the sheetJS, the component itself
GrCard235 B2 779 B
GrDialog670 B9 989 B
GrDataTable2 911 B36 431 B
GrSelect3 037 B23 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.

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.

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:

  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

  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.