Knit UI
GitHub

Performance

Bundle size

Three things dominate, in order:

1. Generated barrels. @knitui/icons re-exports ~6,100 modules and @knitui/emoji ~3,800. Because the kit src-ships, your bundler compiles what it reaches. On Next, withKnitui sets optimizePackageImports; elsewhere import the glyph directly:

import { IconCheck } from "@knitui/icons/IconCheck";

2. Satellite kits you do not need. Skia (@knitui/graphics), MapLibre (@knitui/map) and the media players are heavy. Load them behind an interaction or a route:

const Map = dynamic(() => import("./Map"), { ssr: false });

3. .styleable() frames. Most kit components keep a runtime wrapper, so the web output is not pure static CSS. That is the composability trade-off — budget for it rather than hoping extraction removes it.

Long lists

Past a few hundred rows, use VirtualList rather than mapping into a ScrollArea. It windows variable-height rows and keeps a mounted pool (an LRU) so scrolling back does not remount, with caches keyed by item identity rather than index.

<VirtualList data={items} keyExtractor={(item) => item.id} renderItem={renderRow} />

Two rules that matter more than the component choice:

  • Stable keys. Index keys defeat the pool and remount everything on insert.
  • Memoised rows. A row that re-renders on every parent render undoes the windowing.

Carousels

@knitui/carousel mounts a live window plus a warm pool rather than every slide. If slides come back blank while flinging, the window is too small for the velocity — raise the overscan rather than mounting everything.

Animation

Animate on the UI thread. In practice that means Reanimated shared values on native and CSS/rAF on the web, which is what the kit's motion layer already does — so prefer its animation props and usePressScale over hand-rolling with React state. A setState per frame is a dropped frame per frame.

Resource limits

LimitConsequence
~16 live WebGL contexts per pageStack more Skia canvases and they fail to attach a surface
One shared <audio>/<video> per mediumTwo players on screen contend; only the active one works
Android list scrollingMount cost per row is far higher than on desktop web

Mount one canvas or player at a time, and unmount when it scrolls away — the docs site does both with an IntersectionObserver.

Measuring, not guessing

  • Native: a real device fling plus adb shell dumpsys gfxinfo for the jank percentage. Simulator numbers do not transfer.
  • Web: Lighthouse per route, and a bundle budget in CI. This site enforces a per-route JS budget so a regression fails the build instead of being noticed later.

Startup

Native startup is dominated by module evaluation. optimizePackageImports-style rewriting helps on the web; on native, the equivalent is not importing a barrel in the first place. Import from the specific path in library code and in any module that loads at startup.