Performance
Motif resolves tokens to CSS variables, hashes generated rules into shared classes, and switches themes through the cascade. Most apps are fast by default. The shortlist below is the bit you control.
What motif does for you
A few things take work in other styling models that motif handles automatically.
- Atomic deduplication. Every unique style bag becomes one CSS rule. A hundred buttons across
ten pages share one class for the surface, one for the hover state, one per pseudo. Re-renders
do not re-emit rules; the runtime checks a
Setand skips. - Theme switching is an attribute write. All themes' variables ship in one
<style>block at mount time. A switch setsdata-themeon the wrapping element. The browser repaints; React does nothing. - Token references compile to
var(--…). No runtime lookup, no theme-context read on every prop. The cascade does the work. - Pseudo-state and responsive rules inject once per page. The first
<Box _hover={{...}} />on a page emits a rule; every subsequent one with the same bag reuses the class.
Avoid runtime allocation in render
The pattern that costs you on motif is the pattern that costs you elsewhere: allocating a fresh object literal in every render and passing it as a prop.
Motif still hashes the bag's content, so this works correctly — but the parent component re-renders the child every time because the prop reference is new. Hoist style bags out of render when you can:
oxlint's react-perf/jsx-no-new-object-as-prop and jsx-no-new-array-as-prop rules catch the
pattern at lint time. If a rule fires, the fix is almost always to lift the value to module
scope or memoise it.
The same applies to functions. onClick={() => doThing(id)} allocates per render; hoist with
useCallback, or factor the indirection out (onClick={selectFor(id)} where selectFor returns
a memoised handler).
Reach for styled over inline props
A <Box> with five style props is fine. A <Box> with twenty style props on every render of a
list is not — every consumer pays the resolution cost.
When a component has a stable shape used in more than one place, promote it with styled():
The merge happens once at module scope; each render only resolves variant differences and caller overrides.
Profile the bundle
Three things to keep an eye on:
usemotifitself. ~12 kB gzip on web; lower on native (no SSR collector). Both builds tree-shake cleanly — the runtime only includes the props you actually use.- Your tokens module. If your theme is large (deep semantic tree, many animation presets),
the JSON-shaped object lands in the JS bundle. Consider exporting
lightThemeanddarkThemefrom a single module so bundlers can deduplicate. - Generated CSS. Motif's runtime keeps a small style block per page. If you see the block
growing past a few KB, look for unintended explosion: every unique inline
_hoveror responsive bag becomes a class. The fix is the previous section — promote withstyled().
Profile the runtime
For a real-world app, the two metrics worth tracking:
- Time to first paint. Verify your SSR pipeline injects the style block into
<head>before the body. See Server-side rendering. - Theme switch latency. Should be a few milliseconds for any tree size — if it is not, you have
expensive children re-rendering on
useTheme()reads. Most components do not need the hook; prefervar(--…)strings via style props.
The library's CI runs perf benchmarks on every release; the public regressions feed at the changelog calls them out when relevant.