CSS-in-JS benchmarks

One set of React components, built 17 different ways and measured head-to-head on identical workloads — so the numbers compare by construction, not by claim.

17 styling techniques11 workloadsproduction React · median of repeated runs
Technologies 17 / 17 shown
·
Baseline
next-yak
Panda
Runtime CSS-in-JS
Atomic / className
Key findings

next-yak 9.7 ships static folding and the composition-flattening perf runtime on by default: the styled API holds a flat SSR cost from one wrapper to six where 9.6.0 rose 7.8×, both next-yak paths land at or below hand-written vanilla across SSR, payload and scaling, and the css-prop fold is the fastest shipped path.

The 9.7 perf runtime holds composition flat where 9.6.0 multiplied it

The styled API's library slice used to scale straight-line with nesting; the perf runtime now collapses the chain at construction time and holds it flat. On the composition ladder (the Examples compose-1, compose-3 and compose-6 shown below) next-yak-9.7 keeps a ~0.138 ms SSR library slice at every depth, where the 9.6.0 baseline climbs 0.460 to 1.38 to 3.65 ms. In throughput that is 0.671 ms/1k at six levels against the baseline's 5.26, a 7.8× gap, and the interaction follows: the 9.6.0 baseline re-renders a six-level subtree in 24.2 ms against the perf runtime's 7.40 ms. On flat JSX (the Example realistic-button shown below) the perf runtime barely moves the needle, because there is no chain to collapse.

Folding is the flat-JSX lever, the perf runtime the composition lever

Disabling static folding (foldStatic:false) isolates what each mechanism buys. On the flat button (the Example realistic-button shown below) folding rewrites <DenseButton> to a plain <button className> and deletes the styling component, so turning it off costs about 2× SSR (0.962 vs 0.479 ms/1k) as the Yak wrapper returns with a 0.786 ms library slice. On the Example compose-6 shown below folding off costs only ~1.3×, because there the perf runtime, not the fold, does the work. Folding applies when the host is a known intrinsic tag or a class-toggle variant; it stops at styled(Component) seams, which is why the Example tabs shown below keeps a residual wrapper on its styled(Tabs) composition.

The css-prop fold is next-yak's fastest shipped path and ties vanilla

Where the styled fold hands each composition level its own class down a chain, the css-prop fold concatenates the whole chain into a single className on the plain element. On the Examples tabs, compose-3 and compose-6 shown below its SSR CPU matches or beats hand-written vanilla (0.507 vs 0.508 ms/1k at three levels), and it ships the smallest payload in the run, 2.39 kB gzipped on tabs, one class regardless of depth. For hot composed trees it is the path to recommend today; the styled API reaches the same shape only through the perf runtime, which leaves one residual wrapper on composed seams.

Runtime hashing dominates every library that still does it, and it repeats on the client

The libraries that name a class from the styles at render time pay for it every render. On the Example realistic-button shown below, emotion's murmur2 hash is 4.27 ms of its 7.53 ms render, the single most expensive styling function in the run, and generateAndInjectStyles is most of styled-components' slice. The cost repeats during hydration, when these libraries inject their sheet and force reflow: on the naive dynamic case (the Example dyn-translate shown below) goober injects 1000 distinct classes and its client mount blows out to 1270 ms. Every extracted lane (next-yak, StyleX, vanilla) ships zero styling runtime and hydrates as pure JS. That cost lands at render; the codegen tools pay theirs at build instead, where StyleX and Panda cold-build in 343-548 ms against 69-121 ms for everything else, with next-yak's transform adding only ~40 ms over the runtime libraries.

Dynamic values are a feature in next-yak, a discipline everywhere else

next-yak compiles a dynamic value into a static class plus a CSS custom property automatically. On the Example dyn-translate shown below, where each element carries a distinct value and nobody has hand-optimized, the hash libraries regenerate a class per element and their cost explodes (goober renders 5.83 ms/1k, 9.7× the css-prop fold). The fair comparison, the Example dyn-fair shown below, gives each library its hand-written best practice, a static class with the value on an inline style, and the field converges toward vanilla. That parity has a price the chart cannot show: the developer performs the split by hand, per component, on every dynamic value, forever, to reach what next-yak's css-var compile ships once as a feature. The naive pattern is what you get when nobody pays that discipline.

The variant API you pick can swing a library 16×

The Example btn-variant shown below isolates how each library expresses variants. Panda's precompiled cva recipe joins the fast cluster at 0.853 ms/1k, while its inline css() fn re-serializes every variant per element (13.3 ms of component self-time) and is the slowest lane in the case at 13.5 ms/1k, a 16× SSR-throughput swing inside one library. next-yak expresses variants as $prop class toggles that inline to a string concat and fold away, so it lands at the static-class floor with StyleX and vanilla.

One hint per library
next-yak 9.7.0 (styled API)Perf runtime and static folding are on by default; the next lever is teaching the styled fold to chain through styled(styled(…)) so composed trees reach the css-prop fold's one-class floor.
styled-componentsgenerateAndInjectStyles re-hashes and injects a class every render (most of its SSR slice on realistic-button); hash the static rule once and ride dynamic values on a CSS variable, as its own .attrs path already does.
Emotionmurmur2 re-hashes the whole serialized string each render (4.27 of 7.53 ms on realistic-button); memoize the hash on the serialized-string identity for static or lightly-dynamic components.
GooberThe s function re-serializes and re-hashes the CSS string per render, and injects a class per distinct value (client mount 1270 ms on dyn-translate); key the compile-and-hash cache on the tagged-template identity.
StyleXZero styling runtime on flat JSX; the only render-time function left is styleq's atomic merge under composition. The @layer build also trims a little payload on rule-heavy pages over the :not() specificity build.
Panda (css fn)Steer variants to the cva recipe over the inline css() fn, which re-serializes per element (13.3 ms on btn-variant); fold static css({literal}) call sites to the resolved class at build time.
tailwind-mergeget rebuilds and re-looks-up the joined-class key every render (64-70% of its slice); cache on argument identity, or hoist a fully-static merge to a constant.
cnfastNear-free on repeated class lists; only high-cardinality dynamic classes cost via the cache miss. Split the invariant prefix from the volatile token so the prefix stays a hit.

AI-generated summary by Claude Opus, from the measured data.

A whole shop page400 product tilesn = 400low cardinality

A real product listing: responsive grid, a sale badge (color by discount), a wishlist toggle, a truncated title, a dynamic rating bar, optional struck-through price and an out-of-stock add-to-cart — plus (hover:hover)-guarded hover, :focus-visible rings, WCAG ::before tap targets, a @container query per tile, reduced-motion handling and a11y semantics. Tailwind fires ~8 cn() per tile.

On a full shop page the css-prop fold (next-yak-css-9.7) leads SSR, payload and scaling at or below hand-written vanilla, while the runtime hashers pay per tile: emotion renders the page 7.4× slower and injects a sheet that forces reflow on every client phase.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriHow many times per second this lane renders the whole workload to an HTML string in Node (renderToString), timing the production render only — any build-time CSS collection (a Tailwind JIT, a Panda sheet slice) is excluded. Higher is better.

next-yak css-prop 9.7.0230,139 r/s ±9%(0.32 ms / 1k elems)
vanilla (hand-written ceiling)226,186 r/s ±9%(0.33 ms / 1k elems)
StyleX207,743 r/s ±4%(0.36 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)205,889 r/s ±4%(0.36 ms / 1k elems)
next-yak 9.7.0 (styled API)184,665 r/s ±12%(0.40 ms / 1k elems)
cnfast167,639 r/s ±8%(0.44 ms / 1k elems)
next-yak css-prop 9.6.0163,032 r/s ±13%(0.45 ms / 1k elems)
next-yak css-prop 9.7.0 foldStatic: false159,633 r/s ±11%(0.46 ms / 1k elems)
tailwind-merge151,884 r/s ±10%(0.49 ms / 1k elems)
Panda (css fn)127,848 r/s ±3%(0.58 ms / 1k elems)
next-yak 9.7.0 (styled API) foldStatic: false98,633 r/s ±6%(0.75 ms / 1k elems)
next-yak 9.6.070,961 r/s ±3%(1.04 ms / 1k elems)
styled-components66,364 r/s ±4%(1.12 ms / 1k elems)
Goober61,561 r/s ±3%(1.20 ms / 1k elems)
Emotion31,234 r/s ±2%(2.37 ms / 1k elems)
Panda (style props)6,776 r/s ±2%(10.93 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriRequests/sec the lane sustains under concurrent HTTP load (autocannon) serving the SSR render end-to-end — a more realistic server measure than the in-process microbench. Higher is better.

next-yak css-prop 9.7.0281 req/s ±1%
vanilla (hand-written ceiling)280 req/s ±3%
next-yak 9.7.0 (styled API)253 req/s ±0%
next-yak css-prop 9.6.0235 req/s ±0%
next-yak css-prop 9.7.0 foldStatic: false235 req/s ±0%
StyleX without CSS layers (:not() specificity hack)203 req/s ±0%
StyleX202 req/s ±0%
next-yak 9.7.0 (styled API) foldStatic: false173 req/s ±0%
cnfast135 req/s ±0%
next-yak 9.6.0131 req/s ±0%
tailwind-merge131 req/s ±0%
styled-components121 req/s
Goober117 req/s ±0%
Panda (css fn)107 req/s ±0%
Emotion73 req/s
Panda (style props)16 req/s

Where the SSR render time goes — Node CPU profile · median ms / renderiThe median server renderToString(), split by CPU self-time from a sampled V8 profile mapped through source maps (recorded with web-performance-debugger 0.15.1): react-dom (the floor every lane shares), the styling library's runtime, and your component. other is GC / unattributed native work.

react-dom (the shared floor)styling library runtimeyour componentnode / gc / unattributed
vanilla (hand-written ceiling)1.94 ms
next-yak css-prop 9.7.02.04 ms
StyleX without CSS layers (:not() specificity hack)2.37 ms
next-yak 9.7.0 (styled API)2.38 ms
StyleX2.45 ms
cnfast2.56 ms
next-yak css-prop 9.7.0 foldStatic: false2.81 ms
next-yak css-prop 9.6.03.05 ms
tailwind-merge3.11 ms
Panda (css fn)3.66 ms
next-yak 9.7.0 (styled API) foldStatic: false3.92 ms
next-yak 9.6.06.31 ms
styled-components6.86 ms
Goober7.04 ms
Emotion14.42 ms
Panda (style props)68.56 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiTime for React to hydrate the server HTML in the browser — attach event handlers and build the fiber tree over the existing DOM (it does not re-create markup). The first chart is the existing repeated end-to-end timing; the profiled chart then splits one instrumented commit into JS, style, layout, paint, GC, browser work and idle (recorded with web-performance-debugger 0.15.1). Lower is better.

Panda (css fn)57 ms ±1%
next-yak css-prop 9.7.058 ms ±2%
vanilla (hand-written ceiling)59 ms ±1%
StyleX61 ms ±2%
next-yak 9.7.0 (styled API)63 ms ±1%
StyleX without CSS layers (:not() specificity hack)63 ms ±2%
next-yak css-prop 9.6.065 ms ±1%
next-yak css-prop 9.7.0 foldStatic: false65 ms ±2%
cnfast69 ms ±1%
tailwind-merge69 ms ±1%
next-yak 9.7.0 (styled API) foldStatic: false72 ms ±1%
next-yak 9.6.081 ms ±2%
Emotion132 ms ±2%
styled-components247 ms ±1%
Goober255 ms ±2%
Panda (style props)293 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)7.31 ms active(9.27 ms span · 9.30 ms median)
next-yak css-prop 9.7.07.59 ms active(13.58 ms span · 13.70 ms median)
StyleX without CSS layers (:not() specificity hack)7.61 ms active(9.81 ms span · 9.90 ms median)
StyleX7.62 ms active(12.62 ms span · 12.70 ms median)
next-yak 9.7.0 (styled API)7.71 ms active(11.37 ms span · 11.50 ms median)
Panda (css fn)8.12 ms active(10.50 ms span · 10.60 ms median)
next-yak css-prop 9.6.08.22 ms active(16.03 ms span · 16.10 ms median)
next-yak css-prop 9.7.0 foldStatic: false8.22 ms active(15.47 ms span · 15.60 ms median)
next-yak 9.7.0 (styled API) foldStatic: false8.42 ms active(16.58 ms span · 16.60 ms median)
next-yak 9.6.09.38 ms active(16.84 ms span · 17.00 ms median)
tailwind-merge11.88 ms active(12.15 ms span · 12.30 ms median)
cnfast12.85 ms active(15.43 ms span · 15.50 ms median)
Emotion17.13 ms active(24.53 ms span · 24.70 ms median)
Panda (style props)21.48 ms active(22.92 ms span · 23.10 ms median)
Goober22.98 ms active(23.05 ms span · 23.10 ms median)
styled-components24.73 ms active(25.92 ms span · 26.10 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Interaction re-render — repeated timing + Chrome-profiled span anatomyiA state change triggers a synchronous re-render (flushSync) of the whole mounted workload, then we wait for the next paint — click→paint latency. The profile separates active work from the frame-alignment idle that used to dominate this number. This is where runtime CSS-in-JS libraries re-run their per-element styling on every update; build-time lanes (next-yak / Panda / Tailwind / vanilla) do almost none. Lower is better.

next-yak css-prop 9.7.08 ms ±10%
vanilla (hand-written ceiling)8 ms ±7%
cnfast9 ms ±10%
StyleX without CSS layers (:not() specificity hack)10 ms ±13%
StyleX10 ms ±10%
tailwind-merge10 ms ±11%
next-yak 9.7.0 (styled API)10 ms ±8%
Panda (css fn)11 ms ±22%
next-yak css-prop 9.7.0 foldStatic: false12 ms ±19%
next-yak css-prop 9.6.013 ms ±18%
next-yak 9.7.0 (styled API) foldStatic: false22 ms ±5%
next-yak 9.6.027 ms ±2%
styled-components29 ms ±5%
Goober33 ms ±1%
Emotion50 ms ±5%
Panda (style props)197 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
tailwind-merge0.58 ms active(16.23 ms span · 16.20 ms median · p95 18.00)
vanilla (hand-written ceiling)0.60 ms active(16.57 ms span · 16.60 ms median · p95 16.90)
next-yak css-prop 9.7.0 foldStatic: false0.66 ms active(16.61 ms span · 16.60 ms median · p95 17.00)
StyleX0.69 ms active(16.53 ms span · 16.60 ms median · p95 16.80)
Panda (css fn)0.71 ms active(16.68 ms span · 16.70 ms median · p95 17.30)
cnfast0.72 ms active(16.55 ms span · 16.60 ms median · p95 20.60)
next-yak 9.7.0 (styled API) foldStatic: false0.93 ms active(16.58 ms span · 16.60 ms median · p95 18.30)
next-yak css-prop 9.7.00.98 ms active(16.29 ms span · 16.20 ms median · p95 18.20)
next-yak css-prop 9.6.01.10 ms active(16.56 ms span · 16.50 ms median · p95 18.70)
next-yak 9.7.0 (styled API)1.27 ms active(15.35 ms span · 15.30 ms median · p95 19.80)
next-yak 9.6.01.29 ms active(14.81 ms span · 14.80 ms median · p95 20.60)
StyleX without CSS layers (:not() specificity hack)1.34 ms active(15.94 ms span · 16.00 ms median · p95 19.60)
Emotion1.88 ms active(16.67 ms span · 16.80 ms median · p95 19.30)
styled-components2.65 ms active(16.61 ms span · 16.70 ms median · p95 17.10)
Goober2.74 ms active(16.80 ms span · 16.80 ms median · p95 17.30)
Panda (style props)7.05 ms active(16.63 ms span · 16.60 ms median · p95 18.50)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Cold mount — repeated timing + Chrome-profiled span anatomyiStarting from a blank root (no SSR markup), a "click" renders the whole workload from scratch (createRoot().render()), then we wait for the first paint. Unlike hydration — which attaches to existing server HTML — this is a cold client mount, so the first paint includes each runtimelibrary's first style injection into the document. The profiled span shows how much of the commit is JS, style, layout, paint, GC, browser work and idle. Lower is better.

vanilla (hand-written ceiling)131 ms ±1%
next-yak css-prop 9.7.0132 ms ±2%
Panda (css fn)133 ms ±1%
StyleX134 ms ±2%
next-yak 9.7.0 (styled API)135 ms ±2%
next-yak css-prop 9.7.0 foldStatic: false137 ms ±1%
next-yak css-prop 9.6.0138 ms ±2%
StyleX without CSS layers (:not() specificity hack)140 ms ±2%
cnfast143 ms ±1%
tailwind-merge145 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false149 ms ±1%
next-yak 9.6.0160 ms ±1%
styled-components208 ms ±1%
Goober215 ms ±1%
Emotion245 ms ±1%
Panda (style props)376 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)16.94 ms active(18.39 ms span · 18.60 ms median)
StyleX17.73 ms active(18.69 ms span · 18.80 ms median)
next-yak css-prop 9.7.017.80 ms active(25.59 ms span · 25.70 ms median)
next-yak css-prop 9.6.017.81 ms active(18.58 ms span · 18.70 ms median)
next-yak css-prop 9.7.0 foldStatic: false18.00 ms active(19.34 ms span · 19.50 ms median)
Panda (css fn)18.25 ms active(18.78 ms span · 18.90 ms median)
StyleX without CSS layers (:not() specificity hack)18.27 ms active(19.34 ms span · 19.40 ms median)
next-yak 9.7.0 (styled API)19.80 ms active(24.57 ms span · 24.60 ms median)
next-yak 9.7.0 (styled API) foldStatic: false20.08 ms active(20.16 ms span · 20.20 ms median)
next-yak 9.6.020.11 ms active(23.65 ms span · 23.80 ms median)
cnfast24.23 ms active(24.56 ms span · 24.70 ms median)
Goober24.89 ms active(28.13 ms span · 28.30 ms median)
styled-components26.55 ms active(30.29 ms span · 30.30 ms median)
tailwind-merge29.87 ms active(33.86 ms span · 33.90 ms median)
Panda (style props)32.22 ms active(32.23 ms span · 32.40 ms median)
Emotion36.81 ms active(36.85 ms span · 36.90 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiWhere the browser's rendering time goes on a cold mount (not JS — the engine's own style-recalc, layout and paint), profiled with web-performance-debugger in two engines. This is where runtime CSS-in-JS pays a tax build-time lanes don't: it injects a style rule per instance, so the engine recalculates styles once per instance — Chrome's authoritative signal is that style-recalc count (the badge; e.g. 50 instances → ~50 recalcs vs 1 for extracted CSS). Firefox (Gecko) reports sampled style/layout time; a zero sampled slice is not proof of no work, so its exact counts are retained as diagnostics but the chart never treats zero as absence. Bars are ms; compare within an engine. Lower is better. Generated via pnpm setup:wpd + pnpm gen:wpd.

style recalclayoutpaint (Chrome)
next-yak 9.7.0 (styled API) foldStatic: falseChrome9.4 ms1 recalc
Firefox10.0 ms
next-yak css-prop 9.7.0Chrome8.5 ms1 recalc
Firefox9.0 ms
next-yak css-prop 9.7.0 foldStatic: falseChrome8.3 ms1 recalc
Firefox10.7 ms
StyleX without CSS layers (:not() specificity hack)Chrome8.8 ms1 recalc
Firefox9.9 ms
StyleXChrome8.6 ms1 recalc
Firefox12.2 ms
vanilla (hand-written ceiling)Chrome8.0 ms1 recalc
Firefox10.9 ms
styled-componentsChrome11.1 ms1 recalc
Firefox12.0 ms
next-yak 9.6.0Chrome8.9 ms1 recalc
Firefox10.9 ms
tailwind-mergeChrome15.8 ms1 recalc
Firefox9.7 ms
next-yak 9.7.0 (styled API)Chrome8.4 ms1 recalc
Firefox10.2 ms
Panda (css fn)Chrome8.3 ms1 recalc
Firefox11.0 ms
Panda (style props)Chrome8.6 ms1 recalc
Firefox14.5 ms
next-yak css-prop 9.6.0Chrome8.4 ms1 recalc
Firefox11.1 ms
cnfastChrome9.4 ms1 recalc
Firefox12.8 ms
GooberChrome10.2 ms1 recalc
Firefox14.6 ms
EmotionChrome15.7 ms1 recalc
Firefox21.7 ms

Cold mount of 50 instances. Chrome's trustworthy signal is the style-recalc count (badge); Firefox reports sampled Gecko style/layout ms but no main-thread paint. A zero sampled slice is not proof that no work occurred; Chrome's exact count badges are the reliable presence/absence signal.

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads for this page: the client JS runtime the lane ships (over the bare React floor), the CSS, and the SSR HTML. Lower is better.

JSCSSHTML
next-yak css-prop 9.7.07,201 B(0.0 / 0.9 / 6.1 kB)
next-yak css-prop 9.6.07,565 B(0.4 / 0.9 / 6.1 kB)
next-yak css-prop 9.7.0 foldStatic: false7,703 B(0.5 / 0.9 / 6.1 kB)
vanilla (hand-written ceiling)7,881 B(0.0 / 1.5 / 6.2 kB)
next-yak 9.7.0 (styled API)8,304 B(1.1 / 0.9 / 6.2 kB)
next-yak 9.6.08,311 B(1.1 / 0.9 / 6.2 kB)
next-yak 9.7.0 (styled API) foldStatic: false8,514 B(1.3 / 0.9 / 6.2 kB)
Goober11,183 B(3.4 / 1.2 / 6.3 kB)
StyleX12,976 B(2.6 / 1.3 / 8.8 kB)
StyleX without CSS layers (:not() specificity hack)13,056 B(2.5 / 1.4 / 8.8 kB)
Emotion20,246 B(12.4 / 1.2 / 6.2 kB)
Panda (css fn)21,099 B(3.5 / 1.5 / 15.6 kB)
styled-components21,160 B(12.9 / 1.4 / 6.4 kB)
Panda (style props)25,174 B(7.8 / 1.5 / 15.3 kB)
tailwind-merge26,625 B(10.2 / 1.8 / 14.0 kB)
cnfast27,617 B(11.2 / 1.8 / 14.0 kB)

Scaling — SSR render time (ms) vs instance countiSSR render time as the workload grows from a handful to thousands of instances — shows how each lane's per-element cost compounds. A flatter line scales better.

0.01513024536041005001,0002,0004,000
cnfastEmotionGoobernext-yak 9.6.0next-yak 9.7.0 (styled API)next-yak 9.7.0 (styled API) foldStatic: falsenext-yak css-prop 9.6.0next-yak css-prop 9.7.0next-yak css-prop 9.7.0 foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)tailwind-mergevanilla (hand-written ceiling)StyleX
A real componentthe DenseButtonn = 1,000low cardinality

A real-project button with pseudo-states (:hover/:focus-visible/:active/:disabled), a 992px responsive flip, a ::before WCAG target-size, composed style fragments and an icon child — rendered 1,000×. Not a toy 4-class button: this is what real buttons cost.

On a flat, real button StyleX and the two next-yak 9.7 paths tie the fastest cluster at ~0.44-0.53 ms/1k SSR; folding erases the Yak wrapper entirely, and emotion's murmur2 hash is the single most expensive styling function in the run.

realistic-button is the depth-1 anchor of the composition ladder (compose-1, compose-3, compose-6 below). Here folding, not the perf runtime, is the whole win: it deletes the styling component so the styled path ties the extracted floor, and switching folding off doubles the SSR cost. Under composition the split reverses, and the perf runtime carries the load while folding leaves a residual wrapper.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriHow many times per second this lane renders the whole workload to an HTML string in Node (renderToString), timing the production render only — any build-time CSS collection (a Tailwind JIT, a Panda sheet slice) is excluded. Higher is better.

StyleX901,274 r/s ±5%(0.44 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)852,545 r/s ±4%(0.47 ms / 1k elems)
next-yak 9.7.0 (styled API)835,190 r/s ±5%(0.48 ms / 1k elems)
next-yak css-prop 9.7.0749,719 r/s ±10%(0.53 ms / 1k elems)
vanilla (hand-written ceiling)742,666 r/s ±9%(0.54 ms / 1k elems)
next-yak css-prop 9.6.0698,894 r/s ±9%(0.57 ms / 1k elems)
next-yak css-prop 9.7.0 foldStatic: false685,753 r/s ±7%(0.58 ms / 1k elems)
cnfast564,918 r/s ±10%(0.71 ms / 1k elems)
tailwind-merge535,010 r/s ±5%(0.75 ms / 1k elems)
next-yak 9.7.0 (styled API) foldStatic: false415,952 r/s ±3%(0.96 ms / 1k elems)
next-yak 9.6.0404,810 r/s ±2%(0.99 ms / 1k elems)
styled-components330,733 r/s ±3%(1.21 ms / 1k elems)
Goober304,739 r/s ±4%(1.31 ms / 1k elems)
Panda (css fn)292,590 r/s ±2%(1.37 ms / 1k elems)
Emotion139,812 r/s ±1%(2.86 ms / 1k elems)
Panda (style props)20,082 r/s ±2%(19.92 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriRequests/sec the lane sustains under concurrent HTTP load (autocannon) serving the SSR render end-to-end — a more realistic server measure than the in-process microbench. Higher is better.

next-yak 9.7.0 (styled API)596 req/s ±0%
next-yak css-prop 9.7.0575 req/s ±0%
next-yak css-prop 9.6.0507 req/s ±0%
next-yak css-prop 9.7.0 foldStatic: false487 req/s ±0%
StyleX473 req/s ±0%
vanilla (hand-written ceiling)468 req/s ±1%
StyleX without CSS layers (:not() specificity hack)462 req/s ±1%
next-yak 9.7.0 (styled API) foldStatic: false335 req/s ±0%
next-yak 9.6.0327 req/s ±0%
cnfast326 req/s
tailwind-merge306 req/s ±0%
styled-components270 req/s ±0%
Goober254 req/s ±0%
Panda (css fn)175 req/s ±0%
Emotion131 req/s
Panda (style props)20 req/s ±3%

Where the SSR render time goes — Node CPU profile · median ms / renderiThe median server renderToString(), split by CPU self-time from a sampled V8 profile mapped through source maps (recorded with web-performance-debugger 0.15.1): react-dom (the floor every lane shares), the styling library's runtime, and your component. other is GC / unattributed native work.

react-dom (the shared floor)styling library runtimeyour componentnode / gc / unattributed
StyleX1.32 ms
next-yak css-prop 9.7.01.33 ms
StyleX without CSS layers (:not() specificity hack)1.35 ms
next-yak 9.7.0 (styled API)1.37 ms
vanilla (hand-written ceiling)1.59 ms
next-yak css-prop 9.6.01.62 ms
next-yak css-prop 9.7.0 foldStatic: false1.73 ms
cnfast2.27 ms
tailwind-merge2.50 ms
next-yak 9.6.02.55 ms
next-yak 9.7.0 (styled API) foldStatic: false2.63 ms
Goober3.55 ms
styled-components3.55 ms
Panda (css fn)4.46 ms
Emotion7.53 ms
Panda (style props)53.14 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiTime for React to hydrate the server HTML in the browser — attach event handlers and build the fiber tree over the existing DOM (it does not re-create markup). The first chart is the existing repeated end-to-end timing; the profiled chart then splits one instrumented commit into JS, style, layout, paint, GC, browser work and idle (recorded with web-performance-debugger 0.15.1). Lower is better.

Panda (css fn)60 ms ±1%
vanilla (hand-written ceiling)60 ms ±1%
next-yak 9.7.0 (styled API)60 ms ±1%
next-yak css-prop 9.7.060 ms ±2%
StyleX60 ms ±2%
StyleX without CSS layers (:not() specificity hack)61 ms ±2%
next-yak css-prop 9.6.065 ms ±2%
next-yak css-prop 9.7.0 foldStatic: false65 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false69 ms ±2%
next-yak 9.6.070 ms ±1%
cnfast74 ms ±1%
tailwind-merge76 ms ±2%
Emotion119 ms ±1%
Goober162 ms ±1%
styled-components162 ms ±1%
Panda (style props)257 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak 9.7.0 (styled API)5.70 ms active(17.91 ms span · 18.00 ms median)
Panda (css fn)5.94 ms active(12.78 ms span · 12.80 ms median)
next-yak 9.6.06.28 ms active(16.65 ms span · 16.90 ms median)
next-yak 9.7.0 (styled API) foldStatic: false6.39 ms active(14.26 ms span · 14.40 ms median)
next-yak css-prop 9.6.06.42 ms active(10.76 ms span · 10.90 ms median)
StyleX without CSS layers (:not() specificity hack)6.47 ms active(13.66 ms span · 13.80 ms median)
next-yak css-prop 9.7.06.49 ms active(16.62 ms span · 16.70 ms median)
vanilla (hand-written ceiling)6.54 ms active(12.61 ms span · 12.80 ms median)
StyleX6.65 ms active(12.05 ms span · 12.20 ms median)
next-yak css-prop 9.7.0 foldStatic: false7.04 ms active(11.54 ms span · 11.70 ms median)
Goober10.18 ms active(20.96 ms span · 21.00 ms median)
cnfast10.53 ms active(13.89 ms span · 14.00 ms median)
Emotion10.90 ms active(21.41 ms span · 21.60 ms median)
tailwind-merge11.22 ms active(13.87 ms span · 14.00 ms median)
styled-components11.47 ms active(21.20 ms span · 21.20 ms median)
Panda (style props)13.14 ms active(31.18 ms span · 31.20 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Interaction re-render — repeated timing + Chrome-profiled span anatomyiA state change triggers a synchronous re-render (flushSync) of the whole mounted workload, then we wait for the next paint — click→paint latency. The profile separates active work from the frame-alignment idle that used to dominate this number. This is where runtime CSS-in-JS libraries re-run their per-element styling on every update; build-time lanes (next-yak / Panda / Tailwind / vanilla) do almost none. Lower is better.

tailwind-merge7 ms ±9%
cnfast7 ms ±9%
next-yak 9.7.0 (styled API)7 ms ±6%
vanilla (hand-written ceiling)7 ms ±8%
next-yak css-prop 9.7.07 ms ±13%
Panda (css fn)7 ms ±8%
StyleX7 ms ±12%
StyleX without CSS layers (:not() specificity hack)7 ms ±14%
next-yak css-prop 9.7.0 foldStatic: false8 ms ±8%
next-yak css-prop 9.6.08 ms ±13%
next-yak 9.7.0 (styled API) foldStatic: false12 ms ±18%
next-yak 9.6.013 ms ±11%
Goober17 ms ±16%
styled-components17 ms ±11%
Emotion29 ms ±6%
Panda (style props)158 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak css-prop 9.7.0 foldStatic: false0.42 ms active(16.78 ms span · 16.80 ms median · p95 18.20)
next-yak 9.7.0 (styled API) foldStatic: false0.44 ms active(16.61 ms span · 16.60 ms median · p95 16.70)
tailwind-merge0.54 ms active(16.58 ms span · 16.60 ms median · p95 18.40)
StyleX without CSS layers (:not() specificity hack)0.57 ms active(16.58 ms span · 16.60 ms median · p95 17.00)
styled-components0.60 ms active(15.92 ms span · 16.00 ms median · p95 18.40)
next-yak css-prop 9.7.00.61 ms active(16.41 ms span · 16.40 ms median · p95 16.90)
vanilla (hand-written ceiling)0.64 ms active(16.63 ms span · 16.70 ms median · p95 16.70)
cnfast0.65 ms active(16.70 ms span · 16.70 ms median · p95 17.70)
next-yak 9.7.0 (styled API)0.67 ms active(15.39 ms span · 15.40 ms median · p95 20.30)
Goober0.76 ms active(16.53 ms span · 16.50 ms median · p95 17.10)
StyleX0.82 ms active(16.83 ms span · 16.90 ms median · p95 17.10)
Emotion0.88 ms active(15.10 ms span · 15.20 ms median · p95 18.00)
next-yak 9.6.00.94 ms active(15.67 ms span · 15.60 ms median · p95 17.60)
Panda (css fn)1.01 ms active(17.06 ms span · 17.10 ms median · p95 18.50)
next-yak css-prop 9.6.01.17 ms active(16.64 ms span · 16.70 ms median · p95 16.90)
Panda (style props)2.97 ms active(16.50 ms span · 16.50 ms median · p95 18.80)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Cold mount — repeated timing + Chrome-profiled span anatomyiStarting from a blank root (no SSR markup), a "click" renders the whole workload from scratch (createRoot().render()), then we wait for the first paint. Unlike hydration — which attaches to existing server HTML — this is a cold client mount, so the first paint includes each runtimelibrary's first style injection into the document. The profiled span shows how much of the commit is JS, style, layout, paint, GC, browser work and idle. Lower is better.

StyleX111 ms ±2%
vanilla (hand-written ceiling)111 ms ±1%
next-yak css-prop 9.7.0112 ms ±2%
next-yak 9.7.0 (styled API)113 ms ±3%
Panda (css fn)115 ms ±2%
next-yak css-prop 9.7.0 foldStatic: false116 ms ±1%
next-yak css-prop 9.6.0116 ms ±2%
StyleX without CSS layers (:not() specificity hack)117 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false122 ms ±2%
next-yak 9.6.0123 ms ±3%
cnfast124 ms ±1%
tailwind-merge125 ms ±1%
Goober160 ms ±1%
styled-components161 ms ±1%
Emotion185 ms ±1%
Panda (style props)309 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX12.97 ms active(20.18 ms span · 20.30 ms median)
Panda (css fn)13.56 ms active(23.33 ms span · 23.40 ms median)
next-yak css-prop 9.7.013.71 ms active(16.99 ms span · 17.10 ms median)
next-yak css-prop 9.7.0 foldStatic: false13.88 ms active(21.01 ms span · 21.10 ms median)
next-yak 9.7.0 (styled API)14.01 ms active(22.59 ms span · 22.70 ms median)
vanilla (hand-written ceiling)14.23 ms active(17.67 ms span · 17.90 ms median)
next-yak 9.7.0 (styled API) foldStatic: false14.65 ms active(17.18 ms span · 17.20 ms median)
StyleX without CSS layers (:not() specificity hack)15.24 ms active(23.09 ms span · 23.20 ms median)
Goober16.27 ms active(18.54 ms span · 18.60 ms median)
next-yak css-prop 9.6.016.64 ms active(19.14 ms span · 19.20 ms median)
tailwind-merge18.83 ms active(19.20 ms span · 19.30 ms median)
styled-components18.88 ms active(25.49 ms span · 25.60 ms median)
Emotion19.37 ms active(24.50 ms span · 24.60 ms median)
next-yak 9.6.019.41 ms active(21.45 ms span · 21.60 ms median)
cnfast20.28 ms active(28.89 ms span · 29.00 ms median)
Panda (style props)20.46 ms active(21.87 ms span · 22.00 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiWhere the browser's rendering time goes on a cold mount (not JS — the engine's own style-recalc, layout and paint), profiled with web-performance-debugger in two engines. This is where runtime CSS-in-JS pays a tax build-time lanes don't: it injects a style rule per instance, so the engine recalculates styles once per instance — Chrome's authoritative signal is that style-recalc count (the badge; e.g. 50 instances → ~50 recalcs vs 1 for extracted CSS). Firefox (Gecko) reports sampled style/layout time; a zero sampled slice is not proof of no work, so its exact counts are retained as diagnostics but the chart never treats zero as absence. Bars are ms; compare within an engine. Lower is better. Generated via pnpm setup:wpd + pnpm gen:wpd.

style recalclayoutpaint (Chrome)
cnfastChrome7.0 ms1 recalc
Firefox7.0 ms
EmotionChrome6.7 ms1 recalc
Firefox6.9 ms
next-yak 9.6.0Chrome11.1 ms1 recalc
Firefox7.3 ms
next-yak css-prop 9.6.0Chrome9.6 ms1 recalc
Firefox7.0 ms
next-yak css-prop 9.7.0Chrome6.2 ms1 recalc
Firefox5.9 ms
next-yak css-prop 9.7.0 foldStatic: falseChrome6.6 ms1 recalc
Firefox7.4 ms
Panda (css fn)Chrome6.5 ms1 recalc
Firefox7.4 ms
Panda (style props)Chrome6.5 ms1 recalc
Firefox6.0 ms
styled-componentsChrome8.2 ms1 recalc
Firefox7.0 ms
vanilla (hand-written ceiling)Chrome6.7 ms1 recalc
Firefox7.4 ms
next-yak 9.7.0 (styled API) foldStatic: falseChrome6.7 ms1 recalc
Firefox7.0 ms
StyleXChrome6.2 ms1 recalc
Firefox7.8 ms
next-yak 9.7.0 (styled API)Chrome6.4 ms1 recalc
Firefox7.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome7.7 ms1 recalc
Firefox7.0 ms
GooberChrome6.6 ms1 recalc
Firefox8.0 ms
tailwind-mergeChrome6.5 ms1 recalc
Firefox7.0 ms

Cold mount of 50 instances. Chrome's trustworthy signal is the style-recalc count (badge); Firefox reports sampled Gecko style/layout ms but no main-thread paint. A zero sampled slice is not proof that no work occurred; Chrome's exact count badges are the reliable presence/absence signal.

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads for this page: the client JS runtime the lane ships (over the bare React floor), the CSS, and the SSR HTML. Lower is better.

JSCSSHTML
next-yak css-prop 9.7.03,947 B(0.0 / 0.5 / 3.4 kB)
next-yak css-prop 9.6.04,322 B(0.4 / 0.5 / 3.4 kB)
vanilla (hand-written ceiling)4,439 B(0.0 / 0.9 / 3.5 kB)
next-yak css-prop 9.7.0 foldStatic: false4,457 B(0.5 / 0.5 / 3.4 kB)
next-yak 9.6.04,896 B(1.1 / 0.5 / 3.2 kB)
next-yak 9.7.0 (styled API)4,896 B(1.1 / 0.5 / 3.2 kB)
next-yak 9.7.0 (styled API) foldStatic: false5,096 B(1.3 / 0.5 / 3.2 kB)
Goober7,576 B(3.4 / 0.6 / 3.4 kB)
StyleX without CSS layers (:not() specificity hack)9,069 B(2.5 / 1.0 / 5.4 kB)
StyleX9,100 B(2.6 / 0.9 / 5.4 kB)
Panda (css fn)16,118 B(3.5 / 0.9 / 11.2 kB)
Emotion16,650 B(12.4 / 0.6 / 3.2 kB)
styled-components17,198 B(12.9 / 0.6 / 3.3 kB)
Panda (style props)20,430 B(7.8 / 0.9 / 11.2 kB)
tailwind-merge21,212 B(10.2 / 1.0 / 9.5 kB)
cnfast22,204 B(11.2 / 1.0 / 9.5 kB)

Scaling — SSR render time (ms) vs instance countiSSR render time as the workload grows from a handful to thousands of instances — shows how each lane's per-element cost compounds. A flatter line scales better.

0.0501011512021005001,0002,0004,000
cnfastEmotionGoobernext-yak 9.6.0next-yak 9.7.0 (styled API)next-yak 9.7.0 (styled API) foldStatic: falsenext-yak css-prop 9.6.0next-yak css-prop 9.7.0next-yak css-prop 9.7.0 foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)tailwind-mergevanilla (hand-written ceiling)StyleX
A real Tabs component150 groupsn = 150low cardinality

A real design-system Tabs: responsive typography, the full active/hover/focus-visible/disabled state matrix, an animated active underline via CSS anchor positioning (with a per-tab ::after fallback gated on @supports), a ::before WCAG tap target, hidden-scrollbar overflow and a composed FullWidthTabs wrapper. Tailwind needs a ~40-token list per tab; next-yak compiles it all at build time.

On a real composed tab group the css-prop fold merges the whole chain into one className and beats vanilla on SSR CPU, while the styled perf runtime keeps a residual Yak on its styled(Component) seams and sits mid-pack.

tabs is the single-file twin of multifile-composition below. Here FullWidthTabs is declared and used in one module, so its styled(Tabs) use site can fold; in the split version the primitives are imported and the styled perf runtime's SSR figure drifts from 1.45 to 1.48 ms/1k, while the css-prop fold, which has no styled(...) wrapper to lose across the boundary, holds (0.842 to 0.915) and ships an identical 2.39 kB.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriHow many times per second this lane renders the whole workload to an HTML string in Node (renderToString), timing the production render only — any build-time CSS collection (a Tailwind JIT, a Panda sheet slice) is excluded. Higher is better.

vanilla (hand-written ceiling)153,767 r/s ±6%(0.81 ms / 1k elems)
next-yak css-prop 9.7.0148,386 r/s ±4%(0.84 ms / 1k elems)
StyleX142,467 r/s ±5%(0.88 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)134,635 r/s ±5%(0.93 ms / 1k elems)
next-yak css-prop 9.7.0 foldStatic: false111,156 r/s ±9%(1.12 ms / 1k elems)
next-yak css-prop 9.6.0109,512 r/s ±9%(1.14 ms / 1k elems)
next-yak 9.7.0 (styled API)86,358 r/s ±3%(1.45 ms / 1k elems)
cnfast72,551 r/s ±3%(1.72 ms / 1k elems)
next-yak 9.7.0 (styled API) foldStatic: false72,350 r/s ±4%(1.73 ms / 1k elems)
tailwind-merge66,106 r/s ±2%(1.89 ms / 1k elems)
next-yak 9.6.056,402 r/s ±2%(2.22 ms / 1k elems)
Goober53,407 r/s ±2%(2.34 ms / 1k elems)
Panda (css fn)47,172 r/s ±4%(2.65 ms / 1k elems)
styled-components42,747 r/s ±4%(2.92 ms / 1k elems)
Emotion21,619 r/s ±1%(5.78 ms / 1k elems)
Panda (style props)3,220 r/s ±2%(38.82 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriRequests/sec the lane sustains under concurrent HTTP load (autocannon) serving the SSR render end-to-end — a more realistic server measure than the in-process microbench. Higher is better.

vanilla (hand-written ceiling)714 req/s ±0%
next-yak css-prop 9.7.0669 req/s ±0%
StyleX without CSS layers (:not() specificity hack)525 req/s ±0%
StyleX523 req/s ±0%
next-yak css-prop 9.6.0522 req/s ±0%
next-yak css-prop 9.7.0 foldStatic: false522 req/s ±0%
next-yak 9.7.0 (styled API)434 req/s ±0%
next-yak 9.7.0 (styled API) foldStatic: false378 req/s ±0%
next-yak 9.6.0305 req/s ±0%
Goober293 req/s ±1%
cnfast284 req/s ±0%
tailwind-merge270 req/s ±0%
styled-components233 req/s ±0%
Panda (css fn)199 req/s ±1%
Emotion135 req/s
Panda (style props)22 req/s

Where the SSR render time goes — Node CPU profile · median ms / renderiThe median server renderToString(), split by CPU self-time from a sampled V8 profile mapped through source maps (recorded with web-performance-debugger 0.15.1): react-dom (the floor every lane shares), the styling library's runtime, and your component. other is GC / unattributed native work.

react-dom (the shared floor)styling library runtimeyour componentnode / gc / unattributed
vanilla (hand-written ceiling)1.23 ms
next-yak css-prop 9.7.01.29 ms
StyleX without CSS layers (:not() specificity hack)1.46 ms
StyleX1.49 ms
next-yak css-prop 9.6.01.69 ms
next-yak css-prop 9.7.0 foldStatic: false1.74 ms
next-yak 9.7.0 (styled API)2.11 ms
cnfast2.34 ms
next-yak 9.7.0 (styled API) foldStatic: false2.55 ms
next-yak 9.6.02.88 ms
Goober3.02 ms
tailwind-merge3.05 ms
styled-components3.88 ms
Panda (css fn)3.89 ms
Emotion7.43 ms
Panda (style props)49.84 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiTime for React to hydrate the server HTML in the browser — attach event handlers and build the fiber tree over the existing DOM (it does not re-create markup). The first chart is the existing repeated end-to-end timing; the profiled chart then splits one instrumented commit into JS, style, layout, paint, GC, browser work and idle (recorded with web-performance-debugger 0.15.1). Lower is better.

vanilla (hand-written ceiling)46 ms ±2%
next-yak css-prop 9.7.047 ms ±2%
StyleX48 ms ±1%
StyleX without CSS layers (:not() specificity hack)49 ms ±2%
Panda (css fn)50 ms ±1%
next-yak css-prop 9.7.0 foldStatic: false52 ms ±3%
next-yak css-prop 9.6.052 ms ±3%
next-yak 9.7.0 (styled API)54 ms ±1%
next-yak 9.7.0 (styled API) foldStatic: false55 ms ±2%
next-yak 9.6.059 ms ±2%
tailwind-merge60 ms ±1%
cnfast61 ms ±1%
Emotion98 ms ±1%
Goober194 ms ±2%
styled-components195 ms ±1%
Panda (style props)233 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)7.17 ms active(11.78 ms span · 11.90 ms median)
StyleX7.40 ms active(13.23 ms span · 13.40 ms median)
next-yak css-prop 9.7.07.72 ms active(16.69 ms span · 16.70 ms median)
Panda (css fn)7.92 ms active(16.10 ms span · 16.20 ms median)
StyleX without CSS layers (:not() specificity hack)7.99 ms active(11.17 ms span · 11.30 ms median)
next-yak css-prop 9.6.08.29 ms active(14.29 ms span · 14.40 ms median)
next-yak css-prop 9.7.0 foldStatic: false8.41 ms active(14.84 ms span · 15.00 ms median)
next-yak 9.7.0 (styled API)8.63 ms active(18.30 ms span · 18.50 ms median)
next-yak 9.7.0 (styled API) foldStatic: false9.02 ms active(17.41 ms span · 17.50 ms median)
next-yak 9.6.09.29 ms active(15.80 ms span · 16.00 ms median)
cnfast11.75 ms active(13.96 ms span · 14.00 ms median)
tailwind-merge12.12 ms active(19.90 ms span · 20.00 ms median)
Emotion17.95 ms active(28.97 ms span · 29.10 ms median)
Goober19.78 ms active(25.90 ms span · 26.00 ms median)
styled-components22.21 ms active(24.48 ms span · 24.60 ms median)
Panda (style props)27.78 ms active(32.62 ms span · 32.60 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Interaction re-render — repeated timing + Chrome-profiled span anatomyiA state change triggers a synchronous re-render (flushSync) of the whole mounted workload, then we wait for the next paint — click→paint latency. The profile separates active work from the frame-alignment idle that used to dominate this number. This is where runtime CSS-in-JS libraries re-run their per-element styling on every update; build-time lanes (next-yak / Panda / Tailwind / vanilla) do almost none. Lower is better.

next-yak css-prop 9.6.07 ms ±18%
next-yak css-prop 9.7.07 ms ±13%
next-yak 9.7.0 (styled API)7 ms ±15%
StyleX without CSS layers (:not() specificity hack)7 ms ±12%
cnfast7 ms ±2%
Panda (css fn)7 ms ±6%
tailwind-merge7 ms ±8%
vanilla (hand-written ceiling)8 ms ±15%
StyleX8 ms ±17%
next-yak css-prop 9.7.0 foldStatic: false8 ms ±14%
next-yak 9.7.0 (styled API) foldStatic: false9 ms ±8%
next-yak 9.6.013 ms ±21%
styled-components13 ms ±18%
Goober14 ms ±15%
Emotion27 ms ±4%
Panda (style props)142 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX without CSS layers (:not() specificity hack)0.54 ms active(15.94 ms span · 16.00 ms median · p95 18.60)
cnfast0.65 ms active(15.86 ms span · 15.90 ms median · p95 18.10)
vanilla (hand-written ceiling)0.67 ms active(16.54 ms span · 16.50 ms median · p95 17.30)
next-yak css-prop 9.7.00.69 ms active(16.65 ms span · 16.60 ms median · p95 17.30)
next-yak css-prop 9.7.0 foldStatic: false0.74 ms active(16.59 ms span · 16.50 ms median · p95 16.80)
StyleX0.77 ms active(16.48 ms span · 16.40 ms median · p95 17.90)
next-yak css-prop 9.6.00.81 ms active(16.30 ms span · 16.30 ms median · p95 18.40)
tailwind-merge0.84 ms active(15.03 ms span · 15.00 ms median · p95 18.50)
next-yak 9.7.0 (styled API)0.84 ms active(16.76 ms span · 16.80 ms median · p95 16.90)
styled-components1.21 ms active(16.29 ms span · 16.30 ms median · p95 20.20)
Panda (css fn)1.22 ms active(16.75 ms span · 16.70 ms median · p95 18.60)
next-yak 9.6.01.29 ms active(16.30 ms span · 16.30 ms median · p95 18.70)
next-yak 9.7.0 (styled API) foldStatic: false1.60 ms active(16.57 ms span · 16.60 ms median · p95 17.00)
Goober1.62 ms active(16.57 ms span · 16.60 ms median · p95 16.60)
Emotion2.48 ms active(15.53 ms span · 15.50 ms median · p95 20.00)
Panda (style props)13.24 ms active(16.72 ms span · 16.80 ms median · p95 19.40)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Cold mount — repeated timing + Chrome-profiled span anatomyiStarting from a blank root (no SSR markup), a "click" renders the whole workload from scratch (createRoot().render()), then we wait for the first paint. Unlike hydration — which attaches to existing server HTML — this is a cold client mount, so the first paint includes each runtimelibrary's first style injection into the document. The profiled span shows how much of the commit is JS, style, layout, paint, GC, browser work and idle. Lower is better.

vanilla (hand-written ceiling)92 ms ±1%
next-yak css-prop 9.7.092 ms ±2%
StyleX93 ms ±2%
StyleX without CSS layers (:not() specificity hack)95 ms ±2%
next-yak css-prop 9.6.096 ms ±2%
Panda (css fn)97 ms ±1%
next-yak css-prop 9.7.0 foldStatic: false97 ms ±2%
next-yak 9.7.0 (styled API)100 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false102 ms ±1%
next-yak 9.6.0105 ms ±3%
cnfast106 ms ±1%
tailwind-merge108 ms ±2%
Goober193 ms ±1%
styled-components198 ms ±1%
Emotion224 ms ±3%
Panda (style props)284 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX17.36 ms active(18.21 ms span · 18.30 ms median)
next-yak css-prop 9.7.017.58 ms active(17.65 ms span · 17.80 ms median)
vanilla (hand-written ceiling)17.64 ms active(17.66 ms span · 17.70 ms median)
StyleX without CSS layers (:not() specificity hack)17.92 ms active(18.14 ms span · 18.20 ms median)
next-yak 9.7.0 (styled API)18.89 ms active(19.89 ms span · 20.00 ms median)
Panda (css fn)19.16 ms active(20.12 ms span · 20.20 ms median)
next-yak css-prop 9.6.019.40 ms active(23.72 ms span · 23.80 ms median)
next-yak css-prop 9.7.0 foldStatic: false19.43 ms active(21.06 ms span · 21.20 ms median)
next-yak 9.7.0 (styled API) foldStatic: false19.83 ms active(23.34 ms span · 23.40 ms median)
next-yak 9.6.020.91 ms active(27.25 ms span · 27.40 ms median)
cnfast23.10 ms active(28.81 ms span · 28.80 ms median)
tailwind-merge26.67 ms active(26.73 ms span · 26.80 ms median)
Goober26.78 ms active(27.28 ms span · 27.40 ms median)
styled-components27.08 ms active(33.64 ms span · 33.80 ms median)
Emotion31.24 ms active(32.60 ms span · 32.80 ms median)
Panda (style props)38.25 ms active(44.01 ms span · 44.20 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiWhere the browser's rendering time goes on a cold mount (not JS — the engine's own style-recalc, layout and paint), profiled with web-performance-debugger in two engines. This is where runtime CSS-in-JS pays a tax build-time lanes don't: it injects a style rule per instance, so the engine recalculates styles once per instance — Chrome's authoritative signal is that style-recalc count (the badge; e.g. 50 instances → ~50 recalcs vs 1 for extracted CSS). Firefox (Gecko) reports sampled style/layout time; a zero sampled slice is not proof of no work, so its exact counts are retained as diagnostics but the chart never treats zero as absence. Bars are ms; compare within an engine. Lower is better. Generated via pnpm setup:wpd + pnpm gen:wpd.

style recalclayoutpaint (Chrome)
next-yak 9.6.0Chrome9.3 ms1 recalc
Firefox9.0 ms
next-yak 9.7.0 (styled API)Chrome8.7 ms1 recalc
Firefox9.9 ms
next-yak css-prop 9.7.0Chrome8.5 ms1 recalc
Firefox11.9 ms
next-yak css-prop 9.7.0 foldStatic: falseChrome8.9 ms1 recalc
Firefox7.9 ms
tailwind-mergeChrome12.0 ms1 recalc
Firefox9.0 ms
vanilla (hand-written ceiling)Chrome8.8 ms1 recalc
Firefox8.7 ms
StyleXChrome8.6 ms1 recalc
Firefox10.7 ms
Panda (css fn)Chrome8.5 ms1 recalc
Firefox9.7 ms
styled-componentsChrome11.3 ms1 recalc
Firefox11.9 ms
next-yak 9.7.0 (styled API) foldStatic: falseChrome9.0 ms1 recalc
Firefox10.0 ms
EmotionChrome11.3 ms1 recalc
Firefox12.0 ms
cnfastChrome8.7 ms1 recalc
Firefox9.0 ms
Panda (style props)Chrome8.4 ms1 recalc
Firefox11.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome9.0 ms1 recalc
Firefox10.9 ms
GooberChrome12.0 ms1 recalc
Firefox11.0 ms
next-yak css-prop 9.6.0Chrome8.7 ms1 recalc
Firefox11.0 ms

Cold mount of 50 instances. Chrome's trustworthy signal is the style-recalc count (badge); Firefox reports sampled Gecko style/layout ms but no main-thread paint. A zero sampled slice is not proof that no work occurred; Chrome's exact count badges are the reliable presence/absence signal.

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads for this page: the client JS runtime the lane ships (over the bare React floor), the CSS, and the SSR HTML. Lower is better.

JSCSSHTML
next-yak css-prop 9.7.02,389 B(0.0 / 0.7 / 1.7 kB)
vanilla (hand-written ceiling)2,724 B(0.0 / 1.0 / 1.6 kB)
next-yak css-prop 9.6.02,757 B(0.4 / 0.7 / 1.7 kB)
next-yak css-prop 9.7.0 foldStatic: false2,897 B(0.5 / 0.7 / 1.7 kB)
next-yak 9.7.0 (styled API)3,457 B(1.1 / 0.6 / 1.7 kB)
next-yak 9.6.03,463 B(1.1 / 0.6 / 1.7 kB)
next-yak 9.7.0 (styled API) foldStatic: false3,660 B(1.3 / 0.6 / 1.7 kB)
Goober6,007 B(3.4 / 0.7 / 1.7 kB)
StyleX without CSS layers (:not() specificity hack)8,130 B(2.5 / 0.9 / 4.5 kB)
StyleX8,143 B(2.6 / 0.9 / 4.5 kB)
Panda (css fn)15,053 B(3.5 / 1.2 / 9.9 kB)
Emotion15,233 B(12.4 / 0.8 / 1.6 kB)
styled-components15,875 B(12.9 / 0.8 / 1.8 kB)
Panda (style props)19,911 B(7.8 / 1.2 / 10.5 kB)
tailwind-merge20,891 B(10.2 / 1.3 / 8.9 kB)
cnfast21,883 B(11.2 / 1.3 / 8.9 kB)

Scaling — SSR render time (ms) vs instance countiSSR render time as the workload grows from a handful to thousands of instances — shows how each lane's per-element cost compounds. A flatter line scales better.

0.031062092912391005001,0002,0004,000
cnfastEmotionGoobernext-yak 9.6.0next-yak 9.7.0 (styled API)next-yak 9.7.0 (styled API) foldStatic: falsenext-yak css-prop 9.6.0next-yak css-prop 9.7.0next-yak css-prop 9.7.0 foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)tailwind-mergevanilla (hand-written ceiling)StyleX
Multi-file compositionimported styled primitivesn = 150low cardinality

The EXACT Tabs workload — identical DOM, identical CSS — but the styled primitives are moved to an imported parts.tsx module, the way a design system ships components. Per-module compile-time optimizations such as next-yak's JSX folding cannot see across the module boundary, while runtime libraries do not care about file layout. Any gap between this case and tabs is the cost of that boundary.

Splitting the styled primitives across modules costs the styled perf runtime little; the css-prop fold has no styled(...) wrapper to lose across the boundary and still ties vanilla on payload and SSR.

multifile-composition is the split-module twin of tabs above. Importing the styled primitives nudges the styled perf runtime from 1.45 to 1.48 ms/1k SSR; the css-prop fold concatenates classes without a styled(...) wrapper to resolve across the boundary, so it barely moves (0.842 to 0.915) and its payload is identical at 2.39 kB.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriHow many times per second this lane renders the whole workload to an HTML string in Node (renderToString), timing the production render only — any build-time CSS collection (a Tailwind JIT, a Panda sheet slice) is excluded. Higher is better.

vanilla (hand-written ceiling)151,089 r/s ±5%(0.83 ms / 1k elems)
StyleX139,077 r/s ±4%(0.90 ms / 1k elems)
next-yak css-prop 9.7.0136,669 r/s ±4%(0.91 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)134,278 r/s ±5%(0.93 ms / 1k elems)
next-yak css-prop 9.7.0 foldStatic: false102,687 r/s ±9%(1.22 ms / 1k elems)
next-yak css-prop 9.6.0100,691 r/s ±8%(1.24 ms / 1k elems)
next-yak 9.7.0 (styled API)84,569 r/s ±3%(1.48 ms / 1k elems)
cnfast71,241 r/s ±6%(1.75 ms / 1k elems)
next-yak 9.7.0 (styled API) foldStatic: false69,955 r/s ±5%(1.79 ms / 1k elems)
tailwind-merge65,683 r/s ±6%(1.90 ms / 1k elems)
Goober53,226 r/s ±3%(2.35 ms / 1k elems)
next-yak 9.6.052,645 r/s ±10%(2.37 ms / 1k elems)
Panda (css fn)47,564 r/s ±3%(2.63 ms / 1k elems)
styled-components43,577 r/s ±5%(2.87 ms / 1k elems)
Emotion21,278 r/s ±5%(5.87 ms / 1k elems)
Panda (style props)3,298 r/s ±1%(37.90 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriRequests/sec the lane sustains under concurrent HTTP load (autocannon) serving the SSR render end-to-end — a more realistic server measure than the in-process microbench. Higher is better.

vanilla (hand-written ceiling)718 req/s ±1%
next-yak css-prop 9.7.0634 req/s ±0%
StyleX524 req/s ±0%
StyleX without CSS layers (:not() specificity hack)522 req/s ±1%
next-yak css-prop 9.6.0506 req/s ±1%
next-yak css-prop 9.7.0 foldStatic: false500 req/s ±0%
next-yak 9.7.0 (styled API)431 req/s ±1%
next-yak 9.7.0 (styled API) foldStatic: false380 req/s ±0%
next-yak 9.6.0310 req/s ±0%
Goober293 req/s ±1%
cnfast287 req/s ±0%
tailwind-merge270 req/s ±0%
styled-components234 req/s ±1%
Panda (css fn)201 req/s ±0%
Emotion135 req/s
Panda (style props)22 req/s

Where the SSR render time goes — Node CPU profile · median ms / renderiThe median server renderToString(), split by CPU self-time from a sampled V8 profile mapped through source maps (recorded with web-performance-debugger 0.15.1): react-dom (the floor every lane shares), the styling library's runtime, and your component. other is GC / unattributed native work.

react-dom (the shared floor)styling library runtimeyour componentnode / gc / unattributed
vanilla (hand-written ceiling)1.23 ms
next-yak css-prop 9.7.01.25 ms
StyleX1.38 ms
StyleX without CSS layers (:not() specificity hack)1.43 ms
next-yak css-prop 9.6.01.69 ms
next-yak css-prop 9.7.0 foldStatic: false1.85 ms
next-yak 9.7.0 (styled API)2.11 ms
next-yak 9.7.0 (styled API) foldStatic: false2.37 ms
cnfast2.43 ms
next-yak 9.6.02.89 ms
Goober2.99 ms
tailwind-merge3.17 ms
Panda (css fn)3.69 ms
styled-components3.75 ms
Emotion7.30 ms
Panda (style props)51.03 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiTime for React to hydrate the server HTML in the browser — attach event handlers and build the fiber tree over the existing DOM (it does not re-create markup). The first chart is the existing repeated end-to-end timing; the profiled chart then splits one instrumented commit into JS, style, layout, paint, GC, browser work and idle (recorded with web-performance-debugger 0.15.1). Lower is better.

vanilla (hand-written ceiling)47 ms ±2%
next-yak css-prop 9.7.047 ms ±4%
StyleX48 ms ±1%
Panda (css fn)49 ms ±2%
StyleX without CSS layers (:not() specificity hack)49 ms ±2%
next-yak css-prop 9.6.051 ms ±2%
next-yak css-prop 9.7.0 foldStatic: false52 ms ±1%
next-yak 9.7.0 (styled API)53 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false55 ms ±1%
next-yak 9.6.058 ms ±1%
cnfast61 ms ±2%
tailwind-merge61 ms ±1%
Emotion98 ms ±1%
Goober192 ms ±1%
styled-components195 ms ±1%
Panda (style props)235 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak css-prop 9.7.07.26 ms active(13.71 ms span · 13.80 ms median)
StyleX7.50 ms active(10.69 ms span · 10.80 ms median)
StyleX without CSS layers (:not() specificity hack)7.69 ms active(17.35 ms span · 17.40 ms median)
vanilla (hand-written ceiling)7.85 ms active(13.78 ms span · 13.90 ms median)
Panda (css fn)7.95 ms active(15.11 ms span · 15.10 ms median)
next-yak css-prop 9.6.08.27 ms active(15.17 ms span · 15.20 ms median)
next-yak css-prop 9.7.0 foldStatic: false8.75 ms active(11.00 ms span · 11.10 ms median)
next-yak 9.7.0 (styled API)8.89 ms active(12.45 ms span · 12.50 ms median)
next-yak 9.7.0 (styled API) foldStatic: false9.09 ms active(13.06 ms span · 13.10 ms median)
next-yak 9.6.09.38 ms active(13.69 ms span · 13.80 ms median)
cnfast11.60 ms active(13.23 ms span · 13.30 ms median)
tailwind-merge12.47 ms active(25.99 ms span · 26.10 ms median)
Emotion17.98 ms active(25.25 ms span · 25.40 ms median)
Goober22.12 ms active(24.47 ms span · 24.50 ms median)
styled-components22.54 ms active(26.64 ms span · 26.70 ms median)
Panda (style props)28.89 ms active(30.09 ms span · 30.20 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Interaction re-render — repeated timing + Chrome-profiled span anatomyiA state change triggers a synchronous re-render (flushSync) of the whole mounted workload, then we wait for the next paint — click→paint latency. The profile separates active work from the frame-alignment idle that used to dominate this number. This is where runtime CSS-in-JS libraries re-run their per-element styling on every update; build-time lanes (next-yak / Panda / Tailwind / vanilla) do almost none. Lower is better.

tailwind-merge7 ms ±9%
Panda (css fn)7 ms ±9%
next-yak 9.7.0 (styled API)7 ms ±6%
StyleX without CSS layers (:not() specificity hack)7 ms ±9%
cnfast8 ms ±7%
next-yak css-prop 9.6.08 ms ±13%
StyleX8 ms ±23%
next-yak css-prop 9.7.08 ms ±10%
next-yak css-prop 9.7.0 foldStatic: false8 ms ±10%
vanilla (hand-written ceiling)8 ms ±13%
next-yak 9.7.0 (styled API) foldStatic: false8 ms ±10%
next-yak 9.6.012 ms ±20%
Goober16 ms ±15%
styled-components16 ms ±19%
Emotion27 ms ±5%
Panda (style props)156 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX0.56 ms active(16.53 ms span · 16.60 ms median · p95 16.90)
vanilla (hand-written ceiling)0.63 ms active(16.53 ms span · 16.60 ms median · p95 18.20)
cnfast0.70 ms active(16.28 ms span · 16.30 ms median · p95 19.80)
next-yak css-prop 9.7.00.70 ms active(15.68 ms span · 15.70 ms median · p95 19.60)
tailwind-merge0.73 ms active(16.55 ms span · 16.60 ms median · p95 18.60)
next-yak css-prop 9.7.0 foldStatic: false0.80 ms active(14.98 ms span · 15.00 ms median · p95 20.20)
next-yak css-prop 9.6.00.87 ms active(16.38 ms span · 16.40 ms median · p95 16.80)
Panda (css fn)0.96 ms active(16.63 ms span · 16.60 ms median · p95 16.80)
StyleX without CSS layers (:not() specificity hack)1.11 ms active(16.11 ms span · 16.20 ms median · p95 17.90)
next-yak 9.7.0 (styled API) foldStatic: false1.14 ms active(16.50 ms span · 16.50 ms median · p95 18.00)
next-yak 9.7.0 (styled API)1.20 ms active(17.89 ms span · 17.90 ms median · p95 18.40)
next-yak 9.6.01.27 ms active(15.61 ms span · 15.50 ms median · p95 18.80)
styled-components1.71 ms active(16.54 ms span · 16.60 ms median · p95 17.70)
Goober1.96 ms active(16.38 ms span · 16.40 ms median · p95 18.60)
Emotion2.60 ms active(16.65 ms span · 16.60 ms median · p95 17.00)
Panda (style props)13.20 ms active(16.26 ms span · 16.30 ms median · p95 23.70)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Cold mount — repeated timing + Chrome-profiled span anatomyiStarting from a blank root (no SSR markup), a "click" renders the whole workload from scratch (createRoot().render()), then we wait for the first paint. Unlike hydration — which attaches to existing server HTML — this is a cold client mount, so the first paint includes each runtimelibrary's first style injection into the document. The profiled span shows how much of the commit is JS, style, layout, paint, GC, browser work and idle. Lower is better.

vanilla (hand-written ceiling)92 ms ±3%
next-yak css-prop 9.7.092 ms ±2%
StyleX93 ms ±3%
StyleX without CSS layers (:not() specificity hack)96 ms ±3%
Panda (css fn)96 ms ±2%
next-yak css-prop 9.7.0 foldStatic: false97 ms ±2%
next-yak css-prop 9.6.099 ms ±2%
next-yak 9.7.0 (styled API)101 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false101 ms ±1%
next-yak 9.6.0105 ms ±3%
cnfast107 ms ±2%
tailwind-merge109 ms ±2%
Goober193 ms ±1%
styled-components199 ms ±1%
Emotion224 ms ±1%
Panda (style props)281 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)17.42 ms active(18.37 ms span · 18.40 ms median)
next-yak css-prop 9.7.017.98 ms active(20.00 ms span · 20.00 ms median)
StyleX18.04 ms active(19.14 ms span · 19.40 ms median)
StyleX without CSS layers (:not() specificity hack)18.47 ms active(20.72 ms span · 20.70 ms median)
next-yak css-prop 9.7.0 foldStatic: false18.81 ms active(19.30 ms span · 19.40 ms median)
next-yak css-prop 9.6.019.01 ms active(19.38 ms span · 19.50 ms median)
next-yak 9.7.0 (styled API)19.21 ms active(22.11 ms span · 22.30 ms median)
Panda (css fn)19.44 ms active(23.43 ms span · 23.60 ms median)
next-yak 9.7.0 (styled API) foldStatic: false20.30 ms active(23.71 ms span · 23.80 ms median)
next-yak 9.6.022.02 ms active(30.59 ms span · 30.70 ms median)
tailwind-merge22.77 ms active(28.36 ms span · 28.50 ms median)
cnfast25.87 ms active(30.55 ms span · 30.70 ms median)
Goober29.51 ms active(30.00 ms span · 30.10 ms median)
styled-components31.14 ms active(32.96 ms span · 33.10 ms median)
Emotion37.26 ms active(41.83 ms span · 42.00 ms median)
Panda (style props)39.83 ms active(45.51 ms span · 45.60 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiWhere the browser's rendering time goes on a cold mount (not JS — the engine's own style-recalc, layout and paint), profiled with web-performance-debugger in two engines. This is where runtime CSS-in-JS pays a tax build-time lanes don't: it injects a style rule per instance, so the engine recalculates styles once per instance — Chrome's authoritative signal is that style-recalc count (the badge; e.g. 50 instances → ~50 recalcs vs 1 for extracted CSS). Firefox (Gecko) reports sampled style/layout time; a zero sampled slice is not proof of no work, so its exact counts are retained as diagnostics but the chart never treats zero as absence. Bars are ms; compare within an engine. Lower is better. Generated via pnpm setup:wpd + pnpm gen:wpd.

style recalclayoutpaint (Chrome)
next-yak 9.6.0Chrome9.6 ms1 recalc
Firefox9.3 ms
next-yak 9.7.0 (styled API) foldStatic: falseChrome8.9 ms1 recalc
Firefox9.0 ms
next-yak css-prop 9.6.0Chrome8.6 ms1 recalc
Firefox9.0 ms
next-yak css-prop 9.7.0Chrome8.7 ms1 recalc
Firefox10.0 ms
Panda (style props)Chrome8.7 ms1 recalc
Firefox11.2 ms
StyleXChrome8.7 ms1 recalc
Firefox10.6 ms
tailwind-mergeChrome8.7 ms1 recalc
Firefox8.8 ms
Panda (css fn)Chrome9.0 ms1 recalc
Firefox11.0 ms
cnfastChrome10.1 ms1 recalc
Firefox10.0 ms
vanilla (hand-written ceiling)Chrome8.5 ms1 recalc
Firefox11.0 ms
GooberChrome15.5 ms1 recalc
Firefox13.0 ms
EmotionChrome15.1 ms1 recalc
Firefox11.0 ms
next-yak 9.7.0 (styled API)Chrome8.8 ms1 recalc
Firefox11.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome8.9 ms1 recalc
Firefox9.3 ms
styled-componentsChrome12.2 ms1 recalc
Firefox16.4 ms
next-yak css-prop 9.7.0 foldStatic: falseChrome8.6 ms1 recalc
Firefox10.9 ms

Cold mount of 50 instances. Chrome's trustworthy signal is the style-recalc count (badge); Firefox reports sampled Gecko style/layout ms but no main-thread paint. A zero sampled slice is not proof that no work occurred; Chrome's exact count badges are the reliable presence/absence signal.

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads for this page: the client JS runtime the lane ships (over the bare React floor), the CSS, and the SSR HTML. Lower is better.

JSCSSHTML
next-yak css-prop 9.7.02,389 B(0.0 / 0.7 / 1.7 kB)
vanilla (hand-written ceiling)2,724 B(0.0 / 1.0 / 1.6 kB)
next-yak css-prop 9.6.02,755 B(0.4 / 0.7 / 1.7 kB)
next-yak css-prop 9.7.0 foldStatic: false2,894 B(0.5 / 0.7 / 1.7 kB)
next-yak 9.7.0 (styled API)3,455 B(1.1 / 0.6 / 1.7 kB)
next-yak 9.6.03,464 B(1.1 / 0.6 / 1.7 kB)
next-yak 9.7.0 (styled API) foldStatic: false3,662 B(1.3 / 0.6 / 1.7 kB)
Goober6,007 B(3.4 / 0.7 / 1.7 kB)
StyleX without CSS layers (:not() specificity hack)8,130 B(2.5 / 0.9 / 4.5 kB)
StyleX8,143 B(2.6 / 0.9 / 4.5 kB)
Panda (css fn)15,053 B(3.5 / 1.2 / 9.9 kB)
Emotion15,233 B(12.4 / 0.8 / 1.6 kB)
styled-components15,853 B(12.9 / 0.8 / 1.8 kB)
Panda (style props)19,911 B(7.8 / 1.2 / 10.5 kB)
tailwind-merge20,891 B(10.2 / 1.3 / 8.9 kB)
cnfast21,883 B(11.2 / 1.3 / 8.9 kB)

Scaling — SSR render time (ms) vs instance countiSSR render time as the workload grows from a handful to thousands of instances — shows how each lane's per-element cost compounds. A flatter line scales better.

0.031262593712491005001,0002,0004,000
cnfastEmotionGoobernext-yak 9.6.0next-yak 9.7.0 (styled API)next-yak 9.7.0 (styled API) foldStatic: falsenext-yak css-prop 9.6.0next-yak css-prop 9.7.0next-yak css-prop 9.7.0 foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)tailwind-mergevanilla (hand-written ceiling)StyleX
Dynamic valuetranslateX (the naive way)n = 1,000high cardinality

1,000 elements each with a unique translateX. Baking the value into the class name produces a brand-new class string every render that a merger can't cache, and styled-components emits a CSS rule per value — while next-yak/Panda turn it into a CSS variable, so their per-instance work stays constant.

With a distinct translateX per element and nobody hand-optimizing, next-yak's automatic css-var split holds flat while the runtime hashers regenerate a class per element: goober renders 9.7× slower and its client mount blows out to 1270 ms.

dyn-translate is the naive end of the dynamic-value trio with dyn-fair and dyn-inline below. It is what you get when nobody hand-optimizes: the hashers regenerate a class per value and blow out (goober 5.83 vs 0.601 ms/1k for the css-var split). dyn-fair gives each library its hand-written best practice and the field converges; dyn-inline is the pure inline-style control. next-yak's css-var split ships that optimization as a feature, so it already sits at the dyn-fair floor here with no manual work.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriHow many times per second this lane renders the whole workload to an HTML string in Node (renderToString), timing the production render only — any build-time CSS collection (a Tailwind JIT, a Panda sheet slice) is excluded. Higher is better.

next-yak css-prop 9.7.01,664,240 r/s ±3%(0.60 ms / 1k elems)
vanilla (hand-written ceiling)1,654,717 r/s ±2%(0.60 ms / 1k elems)
next-yak css-prop 9.7.0 foldStatic: false1,453,930 r/s ±6%(0.69 ms / 1k elems)
next-yak css-prop 9.6.01,421,887 r/s ±4%(0.70 ms / 1k elems)
StyleX1,163,524 r/s ±5%(0.86 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,128,350 r/s ±2%(0.89 ms / 1k elems)
next-yak 9.7.0 (styled API)844,803 r/s ±4%(1.18 ms / 1k elems)
next-yak 9.7.0 (styled API) foldStatic: false785,520 r/s ±3%(1.27 ms / 1k elems)
next-yak 9.6.0738,916 r/s ±3%(1.35 ms / 1k elems)
cnfast558,386 r/s ±7%(1.79 ms / 1k elems)
tailwind-merge480,077 r/s ±5%(2.08 ms / 1k elems)
Emotion346,365 r/s ±5%(2.89 ms / 1k elems)
Panda (css fn)282,233 r/s ±6%(3.54 ms / 1k elems)
styled-components210,604 r/s ±10%(4.75 ms / 1k elems)
Panda (style props)203,704 r/s ±6%(4.91 ms / 1k elems)
Goober171,414 r/s ±3%(5.83 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriRequests/sec the lane sustains under concurrent HTTP load (autocannon) serving the SSR render end-to-end — a more realistic server measure than the in-process microbench. Higher is better.

next-yak css-prop 9.7.01,276 req/s ±0%
vanilla (hand-written ceiling)1,213 req/s ±0%
next-yak css-prop 9.7.0 foldStatic: false1,159 req/s ±0%
next-yak css-prop 9.6.01,134 req/s ±0%
StyleX875 req/s ±0%
StyleX without CSS layers (:not() specificity hack)874 req/s ±1%
next-yak 9.7.0 (styled API)713 req/s ±0%
next-yak 9.7.0 (styled API) foldStatic: false677 req/s ±0%
next-yak 9.6.0641 req/s ±0%
cnfast533 req/s ±1%
tailwind-merge432 req/s ±0%
Emotion323 req/s ±0%
Panda (css fn)239 req/s ±0%
styled-components196 req/s ±3%
Panda (style props)177 req/s ±0%
Goober158 req/s ±0%

Where the SSR render time goes — Node CPU profile · median ms / renderiThe median server renderToString(), split by CPU self-time from a sampled V8 profile mapped through source maps (recorded with web-performance-debugger 0.15.1): react-dom (the floor every lane shares), the styling library's runtime, and your component. other is GC / unattributed native work.

react-dom (the shared floor)styling library runtimeyour componentnode / gc / unattributed
next-yak css-prop 9.7.00.65 ms
next-yak css-prop 9.6.00.83 ms
vanilla (hand-written ceiling)0.92 ms
StyleX without CSS layers (:not() specificity hack)1.12 ms
next-yak css-prop 9.7.0 foldStatic: false1.19 ms
StyleX1.19 ms
next-yak 9.7.0 (styled API) foldStatic: false1.27 ms
next-yak 9.7.0 (styled API)1.30 ms
next-yak 9.6.01.42 ms
cnfast2.14 ms
tailwind-merge2.52 ms
Emotion3.20 ms
Panda (css fn)4.70 ms
styled-components4.88 ms
Goober6.49 ms
Panda (style props)7.16 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiTime for React to hydrate the server HTML in the browser — attach event handlers and build the fiber tree over the existing DOM (it does not re-create markup). The first chart is the existing repeated end-to-end timing; the profiled chart then splits one instrumented commit into JS, style, layout, paint, GC, browser work and idle (recorded with web-performance-debugger 0.15.1). Lower is better.

next-yak css-prop 9.7.047 ms ±4%
next-yak css-prop 9.7.0 foldStatic: false48 ms ±4%
next-yak css-prop 9.6.049 ms ±33%
vanilla (hand-written ceiling)50 ms ±33%
StyleX50 ms ±2%
StyleX without CSS layers (:not() specificity hack)52 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false52 ms ±30%
next-yak 9.7.0 (styled API)52 ms ±31%
next-yak 9.6.053 ms ±3%
Panda (css fn)65 ms ±1%
tailwind-merge68 ms ±22%
cnfast68 ms ±23%
Panda (style props)79 ms ±2%
Emotion98 ms ±15%
styled-components114 ms ±15%
Goober1,279 ms ±0%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak css-prop 9.7.05.25 ms active(12.93 ms span · 13.00 ms median)
next-yak css-prop 9.7.0 foldStatic: false5.37 ms active(11.05 ms span · 11.20 ms median)
StyleX without CSS layers (:not() specificity hack)5.50 ms active(10.86 ms span · 10.80 ms median)
next-yak 9.6.05.66 ms active(14.14 ms span · 14.20 ms median)
next-yak css-prop 9.6.05.70 ms active(14.62 ms span · 14.70 ms median)
next-yak 9.7.0 (styled API) foldStatic: false5.72 ms active(12.14 ms span · 12.20 ms median)
StyleX5.78 ms active(17.54 ms span · 17.60 ms median)
vanilla (hand-written ceiling)5.89 ms active(16.73 ms span · 16.80 ms median)
next-yak 9.7.0 (styled API)6.19 ms active(17.81 ms span · 17.80 ms median)
Panda (css fn)6.48 ms active(15.00 ms span · 15.10 ms median)
Panda (style props)7.06 ms active(11.98 ms span · 12.00 ms median)
Goober8.65 ms active(19.53 ms span · 19.70 ms median)
styled-components8.85 ms active(23.72 ms span · 23.80 ms median)
Emotion9.53 ms active(20.89 ms span · 21.00 ms median)
cnfast9.63 ms active(16.71 ms span · 16.80 ms median)
tailwind-merge9.65 ms active(17.03 ms span · 17.10 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Interaction re-render — repeated timing + Chrome-profiled span anatomyiA state change triggers a synchronous re-render (flushSync) of the whole mounted workload, then we wait for the next paint — click→paint latency. The profile separates active work from the frame-alignment idle that used to dominate this number. This is where runtime CSS-in-JS libraries re-run their per-element styling on every update; build-time lanes (next-yak / Panda / Tailwind / vanilla) do almost none. Lower is better.

next-yak 9.6.07 ms ±12%
next-yak css-prop 9.7.0 foldStatic: false7 ms ±15%
next-yak css-prop 9.7.07 ms ±9%
StyleX without CSS layers (:not() specificity hack)7 ms ±10%
next-yak 9.7.0 (styled API)7 ms ±4%
next-yak 9.7.0 (styled API) foldStatic: false7 ms ±9%
vanilla (hand-written ceiling)7 ms ±17%
cnfast8 ms ±6%
StyleX8 ms ±9%
styled-components8 ms ±5%
next-yak css-prop 9.6.08 ms ±9%
tailwind-merge9 ms ±10%
Emotion10 ms ±12%
Panda (css fn)17 ms ±12%
Goober23 ms ±10%
Panda (style props)23 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
tailwind-merge0.36 ms active(16.47 ms span · 16.50 ms median · p95 17.00)
cnfast0.40 ms active(16.59 ms span · 16.60 ms median · p95 16.80)
next-yak css-prop 9.6.00.42 ms active(15.88 ms span · 15.90 ms median · p95 16.60)
next-yak 9.7.0 (styled API) foldStatic: false0.44 ms active(16.62 ms span · 16.70 ms median · p95 16.90)
vanilla (hand-written ceiling)0.52 ms active(16.78 ms span · 16.80 ms median · p95 17.50)
StyleX0.56 ms active(15.88 ms span · 15.90 ms median · p95 18.20)
Emotion0.57 ms active(16.60 ms span · 16.70 ms median · p95 16.70)
next-yak css-prop 9.7.00.58 ms active(16.63 ms span · 16.50 ms median · p95 18.70)
next-yak css-prop 9.7.0 foldStatic: false0.59 ms active(16.62 ms span · 16.60 ms median · p95 18.60)
next-yak 9.6.00.60 ms active(15.61 ms span · 15.60 ms median · p95 19.70)
StyleX without CSS layers (:not() specificity hack)0.60 ms active(16.65 ms span · 16.60 ms median · p95 16.90)
next-yak 9.7.0 (styled API)0.63 ms active(14.85 ms span · 14.90 ms median · p95 20.40)
Panda (css fn)0.78 ms active(16.57 ms span · 16.60 ms median · p95 16.90)
Panda (style props)0.78 ms active(16.76 ms span · 16.80 ms median · p95 18.70)
styled-components1.04 ms active(16.57 ms span · 16.60 ms median · p95 18.80)
Goober1.08 ms active(16.53 ms span · 16.50 ms median · p95 16.60)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Cold mount — repeated timing + Chrome-profiled span anatomyiStarting from a blank root (no SSR markup), a "click" renders the whole workload from scratch (createRoot().render()), then we wait for the first paint. Unlike hydration — which attaches to existing server HTML — this is a cold client mount, so the first paint includes each runtimelibrary's first style injection into the document. The profiled span shows how much of the commit is JS, style, layout, paint, GC, browser work and idle. Lower is better.

next-yak css-prop 9.7.062 ms ±2%
next-yak css-prop 9.7.0 foldStatic: false65 ms ±1%
vanilla (hand-written ceiling)66 ms ±3%
next-yak css-prop 9.6.066 ms ±2%
StyleX67 ms ±2%
next-yak 9.7.0 (styled API)67 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false67 ms ±2%
next-yak 9.6.069 ms ±2%
StyleX without CSS layers (:not() specificity hack)70 ms ±3%
Panda (css fn)80 ms ±1%
tailwind-merge82 ms ±2%
cnfast83 ms ±2%
Panda (style props)95 ms ±2%
styled-components111 ms ±3%
Emotion117 ms ±1%
Goober1,270 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak css-prop 9.6.09.94 ms active(16.84 ms span · 16.90 ms median)
vanilla (hand-written ceiling)10.04 ms active(14.85 ms span · 14.90 ms median)
next-yak css-prop 9.7.010.21 ms active(18.99 ms span · 19.20 ms median)
next-yak css-prop 9.7.0 foldStatic: false10.23 ms active(18.51 ms span · 18.50 ms median)
StyleX10.37 ms active(17.24 ms span · 17.30 ms median)
StyleX without CSS layers (:not() specificity hack)10.63 ms active(17.88 ms span · 17.90 ms median)
next-yak 9.7.0 (styled API) foldStatic: false10.79 ms active(14.82 ms span · 14.90 ms median)
next-yak 9.7.0 (styled API)10.93 ms active(23.11 ms span · 23.20 ms median)
next-yak 9.6.011.08 ms active(13.14 ms span · 13.20 ms median)
Panda (css fn)11.58 ms active(14.76 ms span · 14.90 ms median)
Panda (style props)12.43 ms active(21.32 ms span · 21.50 ms median)
Goober13.40 ms active(21.19 ms span · 21.40 ms median)
Emotion13.53 ms active(20.11 ms span · 20.20 ms median)
tailwind-merge14.77 ms active(20.21 ms span · 20.30 ms median)
cnfast15.18 ms active(22.92 ms span · 23.10 ms median)
styled-components35.48 ms active(38.37 ms span · 38.40 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiWhere the browser's rendering time goes on a cold mount (not JS — the engine's own style-recalc, layout and paint), profiled with web-performance-debugger in two engines. This is where runtime CSS-in-JS pays a tax build-time lanes don't: it injects a style rule per instance, so the engine recalculates styles once per instance — Chrome's authoritative signal is that style-recalc count (the badge; e.g. 50 instances → ~50 recalcs vs 1 for extracted CSS). Firefox (Gecko) reports sampled style/layout time; a zero sampled slice is not proof of no work, so its exact counts are retained as diagnostics but the chart never treats zero as absence. Bars are ms; compare within an engine. Lower is better. Generated via pnpm setup:wpd + pnpm gen:wpd.

style recalclayoutpaint (Chrome)
cnfastChrome4.6 ms1 recalc
Firefox2.0 ms
EmotionChrome4.3 ms1 recalc
Firefox1.6 ms
next-yak 9.6.0Chrome4.7 ms1 recalc
Firefox3.5 ms
next-yak 9.7.0 (styled API)Chrome4.6 ms1 recalc
Firefox2.0 ms
next-yak 9.7.0 (styled API) foldStatic: falseChrome4.7 ms1 recalc
Firefox2.9 ms
next-yak css-prop 9.7.0Chrome4.3 ms1 recalc
Firefox1.8 ms
next-yak css-prop 9.7.0 foldStatic: falseChrome4.2 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome4.6 ms1 recalc
Firefox2.0 ms
Panda (style props)Chrome4.2 ms1 recalc
Firefox1.3 ms
styled-componentsChrome26.5 ms1 recalc
Firefox1.9 ms
StyleX without CSS layers (:not() specificity hack)Chrome4.5 ms1 recalc
Firefox2.1 ms
tailwind-mergeChrome4.4 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome4.4 ms1 recalc
Firefox2.0 ms
StyleXChrome4.2 ms1 recalc
Firefox2.0 ms
GooberChrome4.7 ms1 recalc
Firefox2.0 ms
next-yak css-prop 9.6.0Chrome4.2 ms1 recalc
Firefox3.0 ms

Cold mount of 50 instances. Chrome's trustworthy signal is the style-recalc count (badge); Firefox reports sampled Gecko style/layout ms but no main-thread paint. A zero sampled slice is not proof that no work occurred; Chrome's exact count badges are the reliable presence/absence signal.

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads for this page: the client JS runtime the lane ships (over the bare React floor), the CSS, and the SSR HTML. Lower is better.

JSCSSHTML
next-yak css-prop 9.7.05,238 B(0.0 / 0.1 / 5.0 kB)
vanilla (hand-written ceiling)5,499 B(0.0 / 0.3 / 5.0 kB)
next-yak css-prop 9.6.05,610 B(0.4 / 0.1 / 5.0 kB)
next-yak css-prop 9.7.0 foldStatic: false5,744 B(0.5 / 0.1 / 5.0 kB)
next-yak 9.7.0 (styled API)6,323 B(1.1 / 0.1 / 5.0 kB)
next-yak 9.6.06,325 B(1.1 / 0.1 / 5.0 kB)
next-yak 9.7.0 (styled API) foldStatic: false6,524 B(1.3 / 0.1 / 5.0 kB)
StyleX without CSS layers (:not() specificity hack)8,200 B(2.5 / 0.1 / 5.4 kB)
StyleX8,297 B(2.6 / 0.1 / 5.4 kB)
Panda (css fn)9,288 B(3.5 / 0.1 / 5.4 kB)
Panda (style props)13,603 B(7.8 / 0.1 / 5.4 kB)
tailwind-merge20,853 B(10.2 / 5.1 / 5.0 kB)
cnfast21,845 B(11.2 / 5.1 / 5.0 kB)
Goober22,080 B(3.4 / 9.4 / 8.7 kB)
styled-components35,298 B(12.9 / 13.6 / 8.0 kB)
Emotion39,080 B(12.4 / 17.9 / 7.8 kB)

Scaling — SSR render time (ms) vs instance countiSSR render time as the workload grows from a handful to thousands of instances — shows how each lane's per-element cost compounds. A flatter line scales better.

0.040791191581005001,0002,0004,000
cnfastEmotionGoobernext-yak 9.6.0next-yak 9.7.0 (styled API)next-yak 9.7.0 (styled API) foldStatic: falsenext-yak css-prop 9.6.0next-yak css-prop 9.7.0next-yak css-prop 9.7.0 foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)tailwind-mergevanilla (hand-written ceiling)StyleX
Dynamic valuetranslateX (each lane's best practice)n = 1,000high cardinality

The same 1,000-unique-translateX workload as the naive case, but idiomatic: lanes without native dynamic-value support pass the value as an inline style over a static class (their documented answer to high-cardinality dynamic values), while next-yak keeps its CSS-variable path and StyleX its dynamic style function. Compare with dyn-translate to see what the naive pattern costs each ecosystem.

Given each library its hand-written best practice, a static class with the value on an inline style, the field converges toward vanilla; next-yak reaches the same shape as a compiler feature, everyone else by writing the split by hand on every dynamic value.

dyn-fair is the hand-optimized middle of the dynamic trio (dyn-translate above is the naive version, dyn-inline below the pure inline-style control). Every library reaches near-vanilla here only because the developer manually splits the static class from the dynamic inline value, replicating by hand what next-yak's css-var compile does automatically. The parity is real, and so is its price: the same optimization, written per component, on every dynamic value, forever. dyn-translate shows what the same code costs when nobody pays that discipline.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriHow many times per second this lane renders the whole workload to an HTML string in Node (renderToString), timing the production render only — any build-time CSS collection (a Tailwind JIT, a Panda sheet slice) is excluded. Higher is better.

next-yak css-prop 9.7.01,658,259 r/s ±3%(0.60 ms / 1k elems)
vanilla (hand-written ceiling)1,628,333 r/s ±5%(0.61 ms / 1k elems)
cnfast1,599,468 r/s ±6%(0.63 ms / 1k elems)
tailwind-merge1,501,033 r/s ±8%(0.67 ms / 1k elems)
next-yak css-prop 9.7.0 foldStatic: false1,424,838 r/s ±7%(0.70 ms / 1k elems)
next-yak css-prop 9.6.01,381,454 r/s ±6%(0.72 ms / 1k elems)
StyleX1,119,611 r/s ±4%(0.89 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,109,364 r/s ±6%(0.90 ms / 1k elems)
Goober937,060 r/s ±3%(1.07 ms / 1k elems)
next-yak 9.7.0 (styled API)801,469 r/s ±6%(1.25 ms / 1k elems)
next-yak 9.7.0 (styled API) foldStatic: false757,480 r/s ±5%(1.32 ms / 1k elems)
next-yak 9.6.0756,644 r/s ±2%(1.32 ms / 1k elems)
styled-components732,511 r/s ±4%(1.37 ms / 1k elems)
Emotion409,969 r/s ±4%(2.44 ms / 1k elems)
Panda (css fn)364,238 r/s ±5%(2.75 ms / 1k elems)
Panda (style props)222,455 r/s ±4%(4.50 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriRequests/sec the lane sustains under concurrent HTTP load (autocannon) serving the SSR render end-to-end — a more realistic server measure than the in-process microbench. Higher is better.

next-yak css-prop 9.7.01,279 req/s ±0%
vanilla (hand-written ceiling)1,221 req/s ±1%
cnfast1,202 req/s ±0%
next-yak css-prop 9.7.0 foldStatic: false1,167 req/s ±0%
tailwind-merge1,159 req/s ±0%
next-yak css-prop 9.6.01,136 req/s ±1%
StyleX878 req/s ±0%
StyleX without CSS layers (:not() specificity hack)872 req/s ±0%
Goober766 req/s ±0%
next-yak 9.7.0 (styled API)717 req/s ±0%
next-yak 9.7.0 (styled API) foldStatic: false678 req/s ±0%
next-yak 9.6.0653 req/s ±0%
styled-components634 req/s ±0%
Emotion391 req/s ±0%
Panda (css fn)285 req/s ±1%
Panda (style props)193 req/s ±1%

Where the SSR render time goes — Node CPU profile · median ms / renderiThe median server renderToString(), split by CPU self-time from a sampled V8 profile mapped through source maps (recorded with web-performance-debugger 0.15.1): react-dom (the floor every lane shares), the styling library's runtime, and your component. other is GC / unattributed native work.

react-dom (the shared floor)styling library runtimeyour componentnode / gc / unattributed
vanilla (hand-written ceiling)0.76 ms
cnfast0.80 ms
next-yak css-prop 9.7.0 foldStatic: false0.82 ms
tailwind-merge0.87 ms
next-yak css-prop 9.7.00.96 ms
next-yak css-prop 9.6.01.03 ms
StyleX without CSS layers (:not() specificity hack)1.12 ms
StyleX1.20 ms
next-yak 9.7.0 (styled API)1.26 ms
Goober1.27 ms
next-yak 9.7.0 (styled API) foldStatic: false1.36 ms
next-yak 9.6.01.37 ms
styled-components1.70 ms
Emotion2.44 ms
Panda (css fn)3.80 ms
Panda (style props)5.96 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiTime for React to hydrate the server HTML in the browser — attach event handlers and build the fiber tree over the existing DOM (it does not re-create markup). The first chart is the existing repeated end-to-end timing; the profiled chart then splits one instrumented commit into JS, style, layout, paint, GC, browser work and idle (recorded with web-performance-debugger 0.15.1). Lower is better.

next-yak css-prop 9.7.047 ms ±36%
vanilla (hand-written ceiling)49 ms ±2%
next-yak css-prop 9.7.0 foldStatic: false50 ms ±32%
StyleX50 ms ±2%
next-yak 9.7.0 (styled API)51 ms ±5%
next-yak 9.7.0 (styled API) foldStatic: false51 ms ±3%
StyleX without CSS layers (:not() specificity hack)52 ms ±30%
next-yak 9.6.054 ms ±30%
cnfast59 ms ±2%
tailwind-merge60 ms ±28%
Panda (css fn)62 ms ±2%
Emotion74 ms ±2%
Panda (style props)77 ms ±1%
next-yak css-prop 9.6.077 ms ±22%
Goober84 ms ±20%
styled-components85 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak css-prop 9.7.05.26 ms active(17.34 ms span · 17.50 ms median)
next-yak css-prop 9.7.0 foldStatic: false5.37 ms active(15.47 ms span · 15.60 ms median)
vanilla (hand-written ceiling)5.49 ms active(15.84 ms span · 16.00 ms median)
next-yak css-prop 9.6.05.49 ms active(20.12 ms span · 20.20 ms median)
next-yak 9.7.0 (styled API)5.51 ms active(14.42 ms span · 14.50 ms median)
StyleX without CSS layers (:not() specificity hack)5.57 ms active(14.43 ms span · 14.50 ms median)
StyleX5.63 ms active(11.94 ms span · 12.00 ms median)
next-yak 9.7.0 (styled API) foldStatic: false5.69 ms active(11.62 ms span · 11.70 ms median)
next-yak 9.6.05.86 ms active(12.21 ms span · 12.40 ms median)
Panda (css fn)6.24 ms active(11.83 ms span · 12.00 ms median)
Goober6.75 ms active(16.44 ms span · 16.50 ms median)
Panda (style props)6.79 ms active(12.23 ms span · 12.40 ms median)
styled-components7.44 ms active(23.61 ms span · 23.60 ms median)
Emotion7.80 ms active(24.22 ms span · 24.30 ms median)
cnfast8.46 ms active(11.80 ms span · 12.00 ms median)
tailwind-merge8.48 ms active(15.83 ms span · 16.00 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Interaction re-render — repeated timing + Chrome-profiled span anatomyiA state change triggers a synchronous re-render (flushSync) of the whole mounted workload, then we wait for the next paint — click→paint latency. The profile separates active work from the frame-alignment idle that used to dominate this number. This is where runtime CSS-in-JS libraries re-run their per-element styling on every update; build-time lanes (next-yak / Panda / Tailwind / vanilla) do almost none. Lower is better.

styled-components7 ms ±14%
next-yak 9.6.07 ms ±14%
cnfast7 ms ±13%
tailwind-merge7 ms ±9%
StyleX without CSS layers (:not() specificity hack)7 ms ±14%
Goober8 ms ±6%
next-yak 9.7.0 (styled API)8 ms ±11%
next-yak 9.7.0 (styled API) foldStatic: false8 ms ±9%
next-yak css-prop 9.7.0 foldStatic: false8 ms ±14%
StyleX8 ms ±5%
next-yak css-prop 9.7.08 ms ±16%
vanilla (hand-written ceiling)8 ms ±11%
next-yak css-prop 9.6.08 ms ±19%
Emotion11 ms ±6%
Panda (css fn)13 ms ±18%
Panda (style props)21 ms ±8%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
cnfast0.37 ms active(16.58 ms span · 16.60 ms median · p95 18.90)
tailwind-merge0.38 ms active(15.83 ms span · 15.90 ms median · p95 17.40)
next-yak css-prop 9.6.00.44 ms active(16.61 ms span · 16.70 ms median · p95 16.70)
next-yak css-prop 9.7.00.45 ms active(16.36 ms span · 16.30 ms median · p95 16.80)
StyleX without CSS layers (:not() specificity hack)0.46 ms active(17.15 ms span · 17.20 ms median · p95 18.10)
vanilla (hand-written ceiling)0.50 ms active(16.63 ms span · 16.80 ms median · p95 17.50)
Goober0.51 ms active(16.14 ms span · 16.20 ms median · p95 17.30)
next-yak 9.7.0 (styled API) foldStatic: false0.51 ms active(16.57 ms span · 16.50 ms median · p95 18.60)
StyleX0.52 ms active(16.78 ms span · 16.70 ms median · p95 18.50)
next-yak 9.7.0 (styled API)0.52 ms active(15.07 ms span · 15.00 ms median · p95 20.60)
next-yak css-prop 9.7.0 foldStatic: false0.54 ms active(16.59 ms span · 16.50 ms median · p95 18.80)
Emotion0.55 ms active(16.64 ms span · 16.70 ms median · p95 20.50)
styled-components0.58 ms active(16.63 ms span · 16.60 ms median · p95 17.80)
next-yak 9.6.00.60 ms active(16.68 ms span · 16.70 ms median · p95 18.90)
Panda (css fn)0.62 ms active(16.62 ms span · 16.60 ms median · p95 17.00)
Panda (style props)0.98 ms active(16.60 ms span · 16.60 ms median · p95 17.00)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Cold mount — repeated timing + Chrome-profiled span anatomyiStarting from a blank root (no SSR markup), a "click" renders the whole workload from scratch (createRoot().render()), then we wait for the first paint. Unlike hydration — which attaches to existing server HTML — this is a cold client mount, so the first paint includes each runtimelibrary's first style injection into the document. The profiled span shows how much of the commit is JS, style, layout, paint, GC, browser work and idle. Lower is better.

next-yak css-prop 9.7.063 ms ±2%
next-yak css-prop 9.7.0 foldStatic: false64 ms ±2%
vanilla (hand-written ceiling)65 ms ±2%
next-yak css-prop 9.6.065 ms ±2%
StyleX67 ms ±1%
next-yak 9.7.0 (styled API) foldStatic: false68 ms ±2%
StyleX without CSS layers (:not() specificity hack)69 ms ±2%
next-yak 9.7.0 (styled API)69 ms ±2%
next-yak 9.6.070 ms ±2%
cnfast77 ms ±2%
tailwind-merge78 ms ±1%
Panda (css fn)81 ms ±1%
Goober82 ms ±1%
styled-components84 ms ±1%
Emotion96 ms ±1%
Panda (style props)97 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak css-prop 9.7.09.99 ms active(14.29 ms span · 14.40 ms median)
StyleX10.23 ms active(14.92 ms span · 15.00 ms median)
next-yak css-prop 9.7.0 foldStatic: false10.26 ms active(16.81 ms span · 16.90 ms median)
vanilla (hand-written ceiling)10.40 ms active(22.31 ms span · 22.30 ms median)
next-yak 9.7.0 (styled API) foldStatic: false10.54 ms active(19.67 ms span · 19.80 ms median)
next-yak 9.6.010.78 ms active(24.35 ms span · 24.50 ms median)
StyleX without CSS layers (:not() specificity hack)10.84 ms active(16.99 ms span · 17.10 ms median)
Panda (css fn)10.84 ms active(15.35 ms span · 15.50 ms median)
styled-components11.73 ms active(17.98 ms span · 18.00 ms median)
Goober11.77 ms active(22.09 ms span · 22.20 ms median)
Panda (style props)12.52 ms active(20.85 ms span · 20.90 ms median)
next-yak css-prop 9.6.012.59 ms active(18.82 ms span · 18.90 ms median)
Emotion13.04 ms active(16.68 ms span · 16.80 ms median)
next-yak 9.7.0 (styled API)13.75 ms active(18.64 ms span · 18.70 ms median)
cnfast14.24 ms active(20.95 ms span · 21.10 ms median)
tailwind-merge14.28 ms active(27.43 ms span · 27.50 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiWhere the browser's rendering time goes on a cold mount (not JS — the engine's own style-recalc, layout and paint), profiled with web-performance-debugger in two engines. This is where runtime CSS-in-JS pays a tax build-time lanes don't: it injects a style rule per instance, so the engine recalculates styles once per instance — Chrome's authoritative signal is that style-recalc count (the badge; e.g. 50 instances → ~50 recalcs vs 1 for extracted CSS). Firefox (Gecko) reports sampled style/layout time; a zero sampled slice is not proof of no work, so its exact counts are retained as diagnostics but the chart never treats zero as absence. Bars are ms; compare within an engine. Lower is better. Generated via pnpm setup:wpd + pnpm gen:wpd.

style recalclayoutpaint (Chrome)
EmotionChrome4.4 ms1 recalc
Firefox1.8 ms
GooberChrome4.7 ms1 recalc
Firefox1.0 ms
next-yak 9.7.0 (styled API) foldStatic: falseChrome4.4 ms1 recalc
Firefox2.0 ms
next-yak css-prop 9.6.0Chrome6.7 ms1 recalc
Firefox2.0 ms
next-yak css-prop 9.7.0Chrome4.3 ms1 recalc
Firefox1.9 ms
next-yak css-prop 9.7.0 foldStatic: falseChrome4.3 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome4.4 ms1 recalc
Firefox2.0 ms
Panda (style props)Chrome4.4 ms1 recalc
Firefox3.3 ms
styled-componentsChrome4.7 ms1 recalc
Firefox2.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome4.8 ms1 recalc
Firefox2.0 ms
tailwind-mergeChrome4.5 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome4.4 ms1 recalc
Firefox2.0 ms
StyleXChrome4.1 ms1 recalc
Firefox2.0 ms
next-yak 9.6.0Chrome3.9 ms1 recalc
Firefox2.2 ms
cnfastChrome4.5 ms1 recalc
Firefox2.0 ms
next-yak 9.7.0 (styled API)Chrome7.1 ms1 recalc
Firefox3.0 ms

Cold mount of 50 instances. Chrome's trustworthy signal is the style-recalc count (badge); Firefox reports sampled Gecko style/layout ms but no main-thread paint. A zero sampled slice is not proof that no work occurred; Chrome's exact count badges are the reliable presence/absence signal.

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads for this page: the client JS runtime the lane ships (over the bare React floor), the CSS, and the SSR HTML. Lower is better.

JSCSSHTML
next-yak css-prop 9.7.05,239 B(0.0 / 0.1 / 5.0 kB)
vanilla (hand-written ceiling)5,499 B(0.0 / 0.3 / 5.0 kB)
next-yak css-prop 9.6.05,608 B(0.4 / 0.1 / 5.0 kB)
next-yak css-prop 9.7.0 foldStatic: false5,745 B(0.5 / 0.1 / 5.0 kB)
next-yak 9.7.0 (styled API)6,322 B(1.1 / 0.1 / 5.0 kB)
next-yak 9.6.06,324 B(1.1 / 0.1 / 5.0 kB)
next-yak 9.7.0 (styled API) foldStatic: false6,523 B(1.3 / 0.1 / 5.0 kB)
StyleX without CSS layers (:not() specificity hack)8,200 B(2.5 / 0.1 / 5.4 kB)
StyleX8,297 B(2.6 / 0.1 / 5.4 kB)
Goober8,906 B(3.4 / 0.1 / 5.2 kB)
Panda (css fn)9,241 B(3.5 / 0.1 / 5.4 kB)
Panda (style props)13,716 B(7.8 / 0.1 / 5.5 kB)
tailwind-merge15,848 B(10.2 / 0.1 / 5.2 kB)
cnfast16,840 B(11.2 / 0.1 / 5.2 kB)
Emotion18,091 B(12.4 / 0.1 / 5.2 kB)
styled-components18,659 B(12.9 / 0.1 / 5.2 kB)

Scaling — SSR render time (ms) vs instance countiSSR render time as the workload grows from a handful to thousands of instances — shows how each lane's per-element cost compounds. A flatter line scales better.

0.04.79.414191005001,0002,0004,000
cnfastEmotionGoobernext-yak 9.6.0next-yak 9.7.0 (styled API)next-yak 9.7.0 (styled API) foldStatic: falsenext-yak css-prop 9.6.0next-yak css-prop 9.7.0next-yak css-prop 9.7.0 foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)tailwind-mergevanilla (hand-written ceiling)StyleX
Dynamic valuetranslateX (inline-style control)n = 1,000high cardinality

Every lane — including next-yak — uses a static class plus a plain inline style for the unique translateX. Paired with dyn-fair this isolates what next-yak's CSS-variable indirection itself costs.

The inline-style control isolates next-yak's wrapper tax: with the value written straight to style and folding on, next-yak-9.7 ties vanilla (0.629 vs 0.615 ms/1k), and the 9.6.0 baseline without the perf runtime is 2.2× slower.

dyn-inline closes the dynamic trio (dyn-translate the naive version, dyn-fair the hand-optimized one, both above). It carries only the next-yak family and vanilla because it is the reference floor for a value that never needs a class at all. Folded, next-yak-9.7 ties vanilla; this is where the perf runtime's worth over 9.6.0 is starkest (0.629 vs 1.36 ms/1k) with no styling to hide behind.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriHow many times per second this lane renders the whole workload to an HTML string in Node (renderToString), timing the production render only — any build-time CSS collection (a Tailwind JIT, a Panda sheet slice) is excluded. Higher is better.

vanilla (hand-written ceiling)1,624,804 r/s ±3%(0.62 ms / 1k elems)
next-yak 9.7.0 (styled API)1,590,667 r/s ±3%(0.63 ms / 1k elems)
next-yak 9.7.0 (styled API) foldStatic: false1,088,732 r/s ±5%(0.92 ms / 1k elems)
next-yak 9.6.0734,754 r/s ±2%(1.36 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriRequests/sec the lane sustains under concurrent HTTP load (autocannon) serving the SSR render end-to-end — a more realistic server measure than the in-process microbench. Higher is better.

vanilla (hand-written ceiling)1,220 req/s ±0%
next-yak 9.7.0 (styled API)1,189 req/s ±0%
next-yak 9.7.0 (styled API) foldStatic: false909 req/s ±0%
next-yak 9.6.0625 req/s ±0%

Where the SSR render time goes — Node CPU profile · median ms / renderiThe median server renderToString(), split by CPU self-time from a sampled V8 profile mapped through source maps (recorded with web-performance-debugger 0.15.1): react-dom (the floor every lane shares), the styling library's runtime, and your component. other is GC / unattributed native work.

react-dom (the shared floor)styling library runtimeyour componentnode / gc / unattributed
vanilla (hand-written ceiling)0.69 ms
next-yak 9.7.0 (styled API)0.75 ms
next-yak 9.7.0 (styled API) foldStatic: false1.09 ms
next-yak 9.6.01.54 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiTime for React to hydrate the server HTML in the browser — attach event handlers and build the fiber tree over the existing DOM (it does not re-create markup). The first chart is the existing repeated end-to-end timing; the profiled chart then splits one instrumented commit into JS, style, layout, paint, GC, browser work and idle (recorded with web-performance-debugger 0.15.1). Lower is better.

vanilla (hand-written ceiling)49 ms ±3%
next-yak 9.7.0 (styled API)50 ms ±33%
next-yak 9.7.0 (styled API) foldStatic: false52 ms ±2%
next-yak 9.6.055 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak 9.7.0 (styled API)5.49 ms active(15.03 ms span · 15.10 ms median)
vanilla (hand-written ceiling)5.57 ms active(14.66 ms span · 14.90 ms median)
next-yak 9.7.0 (styled API) foldStatic: false5.57 ms active(10.14 ms span · 10.30 ms median)
next-yak 9.6.05.63 ms active(13.89 ms span · 14.10 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Interaction re-render — repeated timing + Chrome-profiled span anatomyiA state change triggers a synchronous re-render (flushSync) of the whole mounted workload, then we wait for the next paint — click→paint latency. The profile separates active work from the frame-alignment idle that used to dominate this number. This is where runtime CSS-in-JS libraries re-run their per-element styling on every update; build-time lanes (next-yak / Panda / Tailwind / vanilla) do almost none. Lower is better.

next-yak 9.7.0 (styled API) foldStatic: false7 ms ±20%
vanilla (hand-written ceiling)7 ms ±10%
next-yak 9.6.08 ms ±10%
next-yak 9.7.0 (styled API)8 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak 9.7.0 (styled API)0.41 ms active(15.60 ms span · 15.70 ms median · p95 20.40)
vanilla (hand-written ceiling)0.43 ms active(16.48 ms span · 16.40 ms median · p95 18.40)
next-yak 9.7.0 (styled API) foldStatic: false0.53 ms active(16.41 ms span · 16.40 ms median · p95 17.10)
next-yak 9.6.00.75 ms active(16.61 ms span · 16.60 ms median · p95 18.40)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Cold mount — repeated timing + Chrome-profiled span anatomyiStarting from a blank root (no SSR markup), a "click" renders the whole workload from scratch (createRoot().render()), then we wait for the first paint. Unlike hydration — which attaches to existing server HTML — this is a cold client mount, so the first paint includes each runtimelibrary's first style injection into the document. The profiled span shows how much of the commit is JS, style, layout, paint, GC, browser work and idle. Lower is better.

next-yak 9.7.0 (styled API)65 ms ±2%
vanilla (hand-written ceiling)65 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false69 ms ±2%
next-yak 9.6.074 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)10.16 ms active(15.43 ms span · 15.60 ms median)
next-yak 9.7.0 (styled API)10.20 ms active(19.45 ms span · 19.50 ms median)
next-yak 9.7.0 (styled API) foldStatic: false10.76 ms active(17.32 ms span · 17.30 ms median)
next-yak 9.6.015.93 ms active(21.40 ms span · 21.60 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiWhere the browser's rendering time goes on a cold mount (not JS — the engine's own style-recalc, layout and paint), profiled with web-performance-debugger in two engines. This is where runtime CSS-in-JS pays a tax build-time lanes don't: it injects a style rule per instance, so the engine recalculates styles once per instance — Chrome's authoritative signal is that style-recalc count (the badge; e.g. 50 instances → ~50 recalcs vs 1 for extracted CSS). Firefox (Gecko) reports sampled style/layout time; a zero sampled slice is not proof of no work, so its exact counts are retained as diagnostics but the chart never treats zero as absence. Bars are ms; compare within an engine. Lower is better. Generated via pnpm setup:wpd + pnpm gen:wpd.

style recalclayoutpaint (Chrome)
next-yak 9.7.0 (styled API)Chrome4.2 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome4.4 ms1 recalc
Firefox2.0 ms
next-yak 9.7.0 (styled API) foldStatic: falseChrome4.8 ms1 recalc
Firefox3.0 ms
next-yak 9.6.0Chrome9.8 ms1 recalc
Firefox3.1 ms

Cold mount of 50 instances. Chrome's trustworthy signal is the style-recalc count (badge); Firefox reports sampled Gecko style/layout ms but no main-thread paint. A zero sampled slice is not proof that no work occurred; Chrome's exact count badges are the reliable presence/absence signal.

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads for this page: the client JS runtime the lane ships (over the bare React floor), the CSS, and the SSR HTML. Lower is better.

JSCSSHTML
vanilla (hand-written ceiling)5,499 B(0.0 / 0.3 / 5.0 kB)
next-yak 9.7.0 (styled API)6,513 B(1.1 / 0.1 / 5.2 kB)
next-yak 9.6.06,518 B(1.1 / 0.1 / 5.2 kB)
next-yak 9.7.0 (styled API) foldStatic: false6,714 B(1.3 / 0.1 / 5.2 kB)

Scaling — SSR render time (ms) vs instance countiSSR render time as the workload grows from a handful to thousands of instances — shows how each lane's per-element cost compounds. A flatter line scales better.

0.01.63.14.76.21005001,0002,0004,000
next-yak 9.6.0next-yak 9.7.0 (styled API)next-yak 9.7.0 (styled API) foldStatic: falsevanilla (hand-written ceiling)
Variant / state buttonn = 1,000low cardinality

A button rendered 1,000× cycling ~12 distinct class strings (variant × active × fullWidth). With so few repeated strings almost every cn() is a cache hit (nearly free), while wrapper-component libraries still run their machinery per instance — the case where a cached merger is hard to beat.

Variant expression swings a library 16×: panda's inline css() re-serializes every variant per element (13.5 ms/1k, the case's slowest) while its precompiled recipe joins the fast cluster; next-yak's $prop class toggles fold to a string concat at the static floor.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriHow many times per second this lane renders the whole workload to an HTML string in Node (renderToString), timing the production render only — any build-time CSS collection (a Tailwind JIT, a Panda sheet slice) is excluded. Higher is better.

StyleX without CSS layers (:not() specificity hack)1,635,882 r/s ±18%(0.61 ms / 1k elems)
next-yak 9.7.0 (styled API)1,490,868 r/s ±19%(0.67 ms / 1k elems)
StyleX1,477,286 r/s ±14%(0.68 ms / 1k elems)
vanilla (hand-written ceiling)1,464,933 r/s ±17%(0.68 ms / 1k elems)
next-yak css-prop 9.7.01,303,710 r/s ±20%(0.77 ms / 1k elems)
tailwind-merge1,260,106 r/s ±18%(0.79 ms / 1k elems)
cnfast1,173,595 r/s ±26%(0.85 ms / 1k elems)
Panda (recipe)1,172,963 r/s ±18%(0.85 ms / 1k elems)
next-yak css-prop 9.6.01,169,990 r/s ±18%(0.85 ms / 1k elems)
next-yak css-prop 9.7.0 foldStatic: false1,159,700 r/s ±14%(0.86 ms / 1k elems)
next-yak 9.6.0671,573 r/s ±11%(1.49 ms / 1k elems)
next-yak 9.7.0 (styled API) foldStatic: false669,194 r/s ±11%(1.49 ms / 1k elems)
styled-components659,558 r/s ±14%(1.52 ms / 1k elems)
Goober572,861 r/s ±10%(1.75 ms / 1k elems)
Emotion333,737 r/s ±6%(3.00 ms / 1k elems)
Panda (style props)89,772 r/s ±4%(11.14 ms / 1k elems)
Panda (css fn)74,294 r/s ±6%(13.46 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriRequests/sec the lane sustains under concurrent HTTP load (autocannon) serving the SSR render end-to-end — a more realistic server measure than the in-process microbench. Higher is better.

next-yak 9.7.0 (styled API)1,525 req/s ±0%
vanilla (hand-written ceiling)1,400 req/s ±0%
next-yak css-prop 9.7.01,370 req/s ±0%
StyleX without CSS layers (:not() specificity hack)1,307 req/s ±0%
StyleX1,296 req/s ±0%
cnfast1,151 req/s ±0%
tailwind-merge1,096 req/s ±0%
next-yak css-prop 9.6.01,094 req/s ±1%
next-yak css-prop 9.7.0 foldStatic: false1,069 req/s ±0%
Panda (recipe)1,004 req/s ±1%
next-yak 9.7.0 (styled API) foldStatic: false678 req/s
next-yak 9.6.0656 req/s ±0%
styled-components630 req/s ±1%
Goober578 req/s ±0%
Emotion340 req/s ±0%
Panda (style props)88 req/s
Panda (css fn)74 req/s ±1%

Where the SSR render time goes — Node CPU profile · median ms / renderiThe median server renderToString(), split by CPU self-time from a sampled V8 profile mapped through source maps (recorded with web-performance-debugger 0.15.1): react-dom (the floor every lane shares), the styling library's runtime, and your component. other is GC / unattributed native work.

react-dom (the shared floor)styling library runtimeyour componentnode / gc / unattributed
next-yak 9.7.0 (styled API)0.61 ms
next-yak css-prop 9.7.00.67 ms
StyleX without CSS layers (:not() specificity hack)0.68 ms
cnfast0.69 ms
tailwind-merge0.73 ms
Panda (recipe)0.79 ms
vanilla (hand-written ceiling)0.90 ms
next-yak css-prop 9.7.0 foldStatic: false0.90 ms
next-yak css-prop 9.6.00.91 ms
StyleX0.95 ms
next-yak 9.7.0 (styled API) foldStatic: false1.38 ms
next-yak 9.6.01.41 ms
styled-components1.64 ms
Goober1.82 ms
Emotion2.94 ms
Panda (style props)14.29 ms
Panda (css fn)14.98 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiTime for React to hydrate the server HTML in the browser — attach event handlers and build the fiber tree over the existing DOM (it does not re-create markup). The first chart is the existing repeated end-to-end timing; the profiled chart then splits one instrumented commit into JS, style, layout, paint, GC, browser work and idle (recorded with web-performance-debugger 0.15.1). Lower is better.

next-yak 9.7.0 (styled API)45 ms ±51%
next-yak css-prop 9.7.047 ms ±47%
StyleX48 ms ±48%
StyleX without CSS layers (:not() specificity hack)48 ms ±46%
vanilla (hand-written ceiling)49 ms ±48%
Panda (recipe)51 ms ±3%
next-yak 9.6.053 ms ±43%
cnfast58 ms ±2%
tailwind-merge59 ms ±39%
Emotion78 ms ±29%
next-yak css-prop 9.6.089 ms ±26%
styled-components92 ms ±24%
next-yak css-prop 9.7.0 foldStatic: false94 ms ±24%
next-yak 9.7.0 (styled API) foldStatic: false96 ms ±24%
Panda (css fn)102 ms ±1%
Panda (style props)103 ms ±1%
Goober132 ms ±17%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak 9.7.0 (styled API)5.05 ms active(12.44 ms span · 12.50 ms median)
next-yak css-prop 9.7.05.36 ms active(13.10 ms span · 13.20 ms median)
StyleX5.47 ms active(15.85 ms span · 16.00 ms median)
vanilla (hand-written ceiling)5.50 ms active(10.26 ms span · 10.30 ms median)
next-yak css-prop 9.6.05.53 ms active(11.03 ms span · 11.10 ms median)
StyleX without CSS layers (:not() specificity hack)5.60 ms active(13.51 ms span · 13.70 ms median)
next-yak css-prop 9.7.0 foldStatic: false5.61 ms active(11.10 ms span · 11.20 ms median)
next-yak 9.6.05.73 ms active(10.35 ms span · 10.40 ms median)
next-yak 9.7.0 (styled API) foldStatic: false5.73 ms active(12.06 ms span · 12.10 ms median)
Panda (recipe)6.26 ms active(13.28 ms span · 13.40 ms median)
Panda (css fn)8.04 ms active(16.82 ms span · 17.00 ms median)
Panda (style props)8.46 ms active(12.18 ms span · 12.30 ms median)
tailwind-merge8.59 ms active(18.85 ms span · 19.00 ms median)
Emotion8.78 ms active(23.35 ms span · 23.40 ms median)
cnfast9.01 ms active(12.49 ms span · 12.70 ms median)
styled-components9.13 ms active(18.70 ms span · 18.70 ms median)
Goober10.34 ms active(16.69 ms span · 16.80 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Interaction re-render — repeated timing + Chrome-profiled span anatomyiA state change triggers a synchronous re-render (flushSync) of the whole mounted workload, then we wait for the next paint — click→paint latency. The profile separates active work from the frame-alignment idle that used to dominate this number. This is where runtime CSS-in-JS libraries re-run their per-element styling on every update; build-time lanes (next-yak / Panda / Tailwind / vanilla) do almost none. Lower is better.

next-yak css-prop 9.7.07 ms ±19%
next-yak 9.7.0 (styled API) foldStatic: false7 ms ±5%
styled-components7 ms ±17%
StyleX7 ms ±19%
next-yak css-prop 9.7.0 foldStatic: false7 ms ±18%
next-yak css-prop 9.6.07 ms ±14%
cnfast7 ms ±7%
tailwind-merge7 ms ±11%
Panda (recipe)8 ms ±8%
next-yak 9.6.08 ms ±12%
StyleX without CSS layers (:not() specificity hack)8 ms ±15%
next-yak 9.7.0 (styled API)8 ms ±10%
vanilla (hand-written ceiling)8 ms ±13%
Goober8 ms ±5%
Emotion12 ms ±9%
Panda (style props)43 ms ±2%
Panda (css fn)47 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak 9.7.0 (styled API)0.30 ms active(14.71 ms span · 14.70 ms median · p95 18.60)
next-yak css-prop 9.6.00.37 ms active(16.63 ms span · 16.60 ms median · p95 18.30)
StyleX0.41 ms active(16.60 ms span · 16.60 ms median · p95 17.10)
next-yak css-prop 9.7.00.49 ms active(16.61 ms span · 16.50 ms median · p95 18.60)
Emotion0.50 ms active(16.01 ms span · 16.00 ms median · p95 17.80)
tailwind-merge0.51 ms active(15.93 ms span · 15.90 ms median · p95 19.00)
styled-components0.51 ms active(15.71 ms span · 15.70 ms median · p95 19.00)
Panda (recipe)0.52 ms active(16.31 ms span · 16.40 ms median · p95 17.90)
StyleX without CSS layers (:not() specificity hack)0.52 ms active(16.24 ms span · 16.30 ms median · p95 19.00)
next-yak 9.7.0 (styled API) foldStatic: false0.53 ms active(16.99 ms span · 17.00 ms median · p95 18.40)
next-yak 9.6.00.54 ms active(16.32 ms span · 16.20 ms median · p95 17.80)
vanilla (hand-written ceiling)0.60 ms active(16.30 ms span · 16.30 ms median · p95 17.80)
cnfast0.61 ms active(15.47 ms span · 15.50 ms median · p95 19.40)
next-yak css-prop 9.7.0 foldStatic: false0.90 ms active(16.51 ms span · 16.40 ms median · p95 17.00)
Goober1.03 ms active(15.77 ms span · 15.80 ms median · p95 18.30)
Panda (style props)1.19 ms active(16.61 ms span · 16.70 ms median · p95 18.00)
Panda (css fn)1.30 ms active(16.45 ms span · 16.40 ms median · p95 18.70)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Cold mount — repeated timing + Chrome-profiled span anatomyiStarting from a blank root (no SSR markup), a "click" renders the whole workload from scratch (createRoot().render()), then we wait for the first paint. Unlike hydration — which attaches to existing server HTML — this is a cold client mount, so the first paint includes each runtimelibrary's first style injection into the document. The profiled span shows how much of the commit is JS, style, layout, paint, GC, browser work and idle. Lower is better.

next-yak 9.7.0 (styled API)73 ms ±34%
StyleX74 ms ±2%
StyleX without CSS layers (:not() specificity hack)75 ms ±2%
vanilla (hand-written ceiling)75 ms ±2%
next-yak css-prop 9.7.078 ms ±3%
Panda (recipe)79 ms ±1%
next-yak css-prop 9.6.080 ms ±3%
next-yak css-prop 9.7.0 foldStatic: false80 ms ±3%
next-yak 9.6.083 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false83 ms ±3%
cnfast86 ms ±2%
tailwind-merge87 ms ±2%
styled-components89 ms ±2%
Goober89 ms ±1%
Emotion104 ms ±2%
Panda (css fn)130 ms ±1%
Panda (style props)132 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak css-prop 9.7.0 foldStatic: false11.75 ms active(19.72 ms span · 19.90 ms median)
StyleX without CSS layers (:not() specificity hack)11.85 ms active(21.39 ms span · 21.40 ms median)
StyleX11.87 ms active(15.03 ms span · 15.20 ms median)
next-yak css-prop 9.6.011.89 ms active(19.25 ms span · 19.30 ms median)
next-yak 9.6.012.30 ms active(16.77 ms span · 16.80 ms median)
next-yak css-prop 9.7.012.32 ms active(18.85 ms span · 18.90 ms median)
Panda (recipe)13.14 ms active(22.02 ms span · 22.10 ms median)
Goober14.24 ms active(21.85 ms span · 21.90 ms median)
Panda (css fn)14.32 ms active(16.95 ms span · 17.00 ms median)
next-yak 9.7.0 (styled API)14.41 ms active(17.40 ms span · 17.50 ms median)
styled-components14.67 ms active(18.40 ms span · 18.60 ms median)
tailwind-merge15.14 ms active(23.78 ms span · 23.80 ms median)
Panda (style props)15.20 ms active(19.73 ms span · 19.80 ms median)
Emotion18.45 ms active(18.50 ms span · 18.60 ms median)
vanilla (hand-written ceiling)18.93 ms active(19.72 ms span · 19.90 ms median)
next-yak 9.7.0 (styled API) foldStatic: false21.82 ms active(22.64 ms span · 22.80 ms median)
cnfast22.84 ms active(28.50 ms span · 28.60 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiWhere the browser's rendering time goes on a cold mount (not JS — the engine's own style-recalc, layout and paint), profiled with web-performance-debugger in two engines. This is where runtime CSS-in-JS pays a tax build-time lanes don't: it injects a style rule per instance, so the engine recalculates styles once per instance — Chrome's authoritative signal is that style-recalc count (the badge; e.g. 50 instances → ~50 recalcs vs 1 for extracted CSS). Firefox (Gecko) reports sampled style/layout time; a zero sampled slice is not proof of no work, so its exact counts are retained as diagnostics but the chart never treats zero as absence. Bars are ms; compare within an engine. Lower is better. Generated via pnpm setup:wpd + pnpm gen:wpd.

style recalclayoutpaint (Chrome)
cnfastChrome8.4 ms1 recalc
Firefox11.0 ms
EmotionChrome7.1 ms1 recalc
Firefox7.9 ms
next-yak 9.6.0Chrome6.0 ms1 recalc
Firefox8.0 ms
next-yak 9.7.0 (styled API)Chrome8.7 ms1 recalc
Firefox7.0 ms
next-yak 9.7.0 (styled API) foldStatic: falseChrome14.5 ms1 recalc
Firefox8.0 ms
next-yak css-prop 9.7.0Chrome6.5 ms1 recalc
Firefox7.3 ms
next-yak css-prop 9.7.0 foldStatic: falseChrome5.7 ms1 recalc
Firefox9.3 ms
Panda (css fn)Chrome5.6 ms1 recalc
Firefox8.0 ms
Panda (style props)Chrome5.5 ms1 recalc
Firefox8.0 ms
Panda (recipe)Chrome5.6 ms1 recalc
Firefox7.1 ms
styled-componentsChrome6.1 ms1 recalc
Firefox7.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.7 ms1 recalc
Firefox9.4 ms
tailwind-mergeChrome5.6 ms1 recalc
Firefox8.8 ms
vanilla (hand-written ceiling)Chrome13.1 ms1 recalc
Firefox8.2 ms
StyleXChrome5.8 ms1 recalc
Firefox8.2 ms
next-yak css-prop 9.6.0Chrome5.8 ms1 recalc
Firefox8.9 ms
GooberChrome5.7 ms1 recalc
Firefox9.1 ms

Cold mount of 50 instances. Chrome's trustworthy signal is the style-recalc count (badge); Firefox reports sampled Gecko style/layout ms but no main-thread paint. A zero sampled slice is not proof that no work occurred; Chrome's exact count badges are the reliable presence/absence signal.

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads for this page: the client JS runtime the lane ships (over the bare React floor), the CSS, and the SSR HTML. Lower is better.

JSCSSHTML
next-yak css-prop 9.7.03,391 B(0.0 / 0.2 / 3.1 kB)
next-yak css-prop 9.6.03,761 B(0.4 / 0.2 / 3.1 kB)
vanilla (hand-written ceiling)3,781 B(0.0 / 0.5 / 3.2 kB)
next-yak css-prop 9.7.0 foldStatic: false3,893 B(0.5 / 0.2 / 3.1 kB)
next-yak 9.7.0 (styled API)4,472 B(1.1 / 0.2 / 3.1 kB)
next-yak 9.6.04,476 B(1.1 / 0.2 / 3.1 kB)
next-yak 9.7.0 (styled API) foldStatic: false4,679 B(1.3 / 0.2 / 3.1 kB)
Panda (recipe)5,750 B(0.7 / 0.3 / 4.6 kB)
StyleX without CSS layers (:not() specificity hack)6,932 B(2.5 / 0.3 / 4.0 kB)
Goober6,939 B(3.4 / 0.3 / 3.1 kB)
StyleX7,023 B(2.6 / 0.3 / 4.0 kB)
Panda (css fn)8,661 B(3.5 / 0.3 / 4.6 kB)
Panda (style props)12,976 B(7.8 / 0.3 / 4.6 kB)
tailwind-merge14,965 B(10.2 / 0.4 / 4.0 kB)
cnfast15,957 B(11.2 / 0.4 / 4.0 kB)
Emotion16,188 B(12.4 / 0.3 / 3.1 kB)
styled-components16,777 B(12.9 / 0.3 / 3.2 kB)

Scaling — SSR render time (ms) vs instance countiSSR render time as the workload grows from a handful to thousands of instances — shows how each lane's per-element cost compounds. A flatter line scales better.

0.0132740531005001,0002,0004,000
cnfastEmotionGoobernext-yak 9.6.0next-yak 9.7.0 (styled API)next-yak 9.7.0 (styled API) foldStatic: falsenext-yak css-prop 9.6.0next-yak css-prop 9.7.0next-yak css-prop 9.7.0 foldStatic: falsePanda (css fn)Panda (style props)Panda (recipe)styled-componentsStyleX without CSS layers (:not() specificity hack)tailwind-mergevanilla (hand-written ceiling)StyleX
Composition1 level (control)n = 1,000low cardinality

The compose-3 button family at depth 1: one styled component carrying only the base styles, no wrapper chain. Brackets compose-3 from below (compose-6 brackets it from above) to chart how per-element cost grows with composition depth — the boundary that defeats next-yak's JSX folding.

At one composition level the styled perf runtime leads the field (0.458 ms/1k); the gap to the 9.6.0 baseline (1.18) and to panda's inline css() (5.24) is the story the deeper ladder amplifies.

compose-1, compose-3 and compose-6 are the composition ladder. At one level the styled perf runtime leads; the 9.6.0 baseline's library slice already scales with depth (0.460 ms here, 1.38 at three, 3.65 at six), which the perf runtime holds flat (~0.138 ms at every depth). The css-prop fold ties vanilla throughout.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriHow many times per second this lane renders the whole workload to an HTML string in Node (renderToString), timing the production render only — any build-time CSS collection (a Tailwind JIT, a Panda sheet slice) is excluded. Higher is better.

next-yak 9.7.0 (styled API)2,182,215 r/s ±6%(0.46 ms / 1k elems)
next-yak css-prop 9.7.01,912,196 r/s ±5%(0.52 ms / 1k elems)
StyleX1,911,439 r/s ±6%(0.52 ms / 1k elems)
vanilla (hand-written ceiling)1,882,498 r/s ±9%(0.53 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,830,382 r/s ±6%(0.55 ms / 1k elems)
cnfast1,808,318 r/s ±6%(0.55 ms / 1k elems)
tailwind-merge1,794,256 r/s ±5%(0.56 ms / 1k elems)
next-yak css-prop 9.7.0 foldStatic: false1,664,932 r/s ±3%(0.60 ms / 1k elems)
next-yak css-prop 9.6.01,574,597 r/s ±5%(0.64 ms / 1k elems)
next-yak 9.7.0 (styled API) foldStatic: false1,393,565 r/s ±3%(0.72 ms / 1k elems)
Goober1,179,014 r/s ±3%(0.85 ms / 1k elems)
styled-components860,524 r/s ±4%(1.16 ms / 1k elems)
next-yak 9.6.0851,245 r/s ±3%(1.17 ms / 1k elems)
Emotion455,183 r/s ±3%(2.20 ms / 1k elems)
Panda (css fn)190,856 r/s ±4%(5.24 ms / 1k elems)
Panda (style props)141,040 r/s ±8%(7.09 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriRequests/sec the lane sustains under concurrent HTTP load (autocannon) serving the SSR render end-to-end — a more realistic server measure than the in-process microbench. Higher is better.

next-yak 9.7.0 (styled API)1,730 req/s ±0%
vanilla (hand-written ceiling)1,568 req/s ±0%
next-yak css-prop 9.7.01,546 req/s ±0%
StyleX1,474 req/s ±0%
StyleX without CSS layers (:not() specificity hack)1,469 req/s ±0%
tailwind-merge1,380 req/s ±1%
cnfast1,359 req/s ±1%
next-yak css-prop 9.7.0 foldStatic: false1,357 req/s
next-yak css-prop 9.6.01,335 req/s ±0%
next-yak 9.7.0 (styled API) foldStatic: false1,217 req/s ±0%
Goober1,000 req/s
next-yak 9.6.0764 req/s ±0%
styled-components749 req/s ±0%
Emotion440 req/s ±0%
Panda (css fn)166 req/s ±1%
Panda (style props)127 req/s ±1%

Where the SSR render time goes — Node CPU profile · median ms / renderiThe median server renderToString(), split by CPU self-time from a sampled V8 profile mapped through source maps (recorded with web-performance-debugger 0.15.1): react-dom (the floor every lane shares), the styling library's runtime, and your component. other is GC / unattributed native work.

react-dom (the shared floor)styling library runtimeyour componentnode / gc / unattributed
next-yak css-prop 9.7.00.55 ms
StyleX0.58 ms
vanilla (hand-written ceiling)0.58 ms
tailwind-merge0.60 ms
next-yak 9.7.0 (styled API) foldStatic: false0.67 ms
next-yak css-prop 9.7.0 foldStatic: false0.69 ms
next-yak 9.7.0 (styled API)0.74 ms
cnfast0.74 ms
next-yak css-prop 9.6.00.93 ms
StyleX without CSS layers (:not() specificity hack)0.94 ms
Goober1.09 ms
next-yak 9.6.01.16 ms
styled-components1.34 ms
Emotion2.25 ms
Panda (css fn)6.40 ms
Panda (style props)9.51 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiTime for React to hydrate the server HTML in the browser — attach event handlers and build the fiber tree over the existing DOM (it does not re-create markup). The first chart is the existing repeated end-to-end timing; the profiled chart then splits one instrumented commit into JS, style, layout, paint, GC, browser work and idle (recorded with web-performance-debugger 0.15.1). Lower is better.

StyleX without CSS layers (:not() specificity hack)48 ms ±49%
next-yak css-prop 9.7.0 foldStatic: false50 ms ±46%
next-yak css-prop 9.6.052 ms ±44%
tailwind-merge58 ms ±39%
cnfast59 ms ±39%
Panda (css fn)70 ms ±1%
next-yak 9.7.0 (styled API)87 ms ±27%
Panda (style props)87 ms ±1%
StyleX89 ms ±26%
vanilla (hand-written ceiling)89 ms ±26%
next-yak 9.7.0 (styled API) foldStatic: false89 ms ±26%
next-yak css-prop 9.7.089 ms ±27%
next-yak 9.6.094 ms ±24%
Goober103 ms ±21%
Emotion111 ms ±20%
styled-components140 ms ±16%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak 9.7.0 (styled API)5.12 ms active(12.89 ms span · 13.00 ms median)
StyleX5.21 ms active(15.52 ms span · 15.60 ms median)
StyleX without CSS layers (:not() specificity hack)5.27 ms active(17.31 ms span · 17.40 ms median)
next-yak 9.7.0 (styled API) foldStatic: false5.37 ms active(10.16 ms span · 10.30 ms median)
next-yak css-prop 9.7.05.38 ms active(17.10 ms span · 17.20 ms median)
vanilla (hand-written ceiling)5.39 ms active(14.21 ms span · 14.30 ms median)
next-yak css-prop 9.6.05.39 ms active(15.16 ms span · 15.30 ms median)
next-yak 9.6.05.41 ms active(15.93 ms span · 16.00 ms median)
next-yak css-prop 9.7.0 foldStatic: false5.55 ms active(13.84 ms span · 14.00 ms median)
Goober7.08 ms active(18.78 ms span · 18.80 ms median)
Panda (css fn)7.14 ms active(11.06 ms span · 11.20 ms median)
Emotion7.24 ms active(19.06 ms span · 19.20 ms median)
Panda (style props)7.50 ms active(18.47 ms span · 18.50 ms median)
styled-components7.78 ms active(22.83 ms span · 22.90 ms median)
tailwind-merge8.55 ms active(13.70 ms span · 13.70 ms median)
cnfast9.20 ms active(15.02 ms span · 15.10 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Interaction re-render — repeated timing + Chrome-profiled span anatomyiA state change triggers a synchronous re-render (flushSync) of the whole mounted workload, then we wait for the next paint — click→paint latency. The profile separates active work from the frame-alignment idle that used to dominate this number. This is where runtime CSS-in-JS libraries re-run their per-element styling on every update; build-time lanes (next-yak / Panda / Tailwind / vanilla) do almost none. Lower is better.

styled-components6 ms ±18%
cnfast7 ms ±14%
next-yak 9.6.07 ms ±10%
next-yak css-prop 9.6.07 ms ±8%
next-yak 9.7.0 (styled API) foldStatic: false7 ms ±6%
StyleX without CSS layers (:not() specificity hack)7 ms ±5%
next-yak css-prop 9.7.08 ms ±7%
vanilla (hand-written ceiling)8 ms ±8%
Goober8 ms ±18%
next-yak 9.7.0 (styled API)8 ms ±12%
tailwind-merge8 ms ±6%
next-yak css-prop 9.7.0 foldStatic: false8 ms ±8%
StyleX8 ms ±16%
Emotion9 ms ±5%
Panda (css fn)22 ms ±3%
Panda (style props)30 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
styled-components0.38 ms active(16.57 ms span · 16.60 ms median · p95 18.20)
tailwind-merge0.40 ms active(16.24 ms span · 16.30 ms median · p95 18.00)
next-yak css-prop 9.7.00.43 ms active(14.96 ms span · 15.00 ms median · p95 20.10)
StyleX without CSS layers (:not() specificity hack)0.43 ms active(16.61 ms span · 16.70 ms median · p95 18.10)
next-yak 9.7.0 (styled API) foldStatic: false0.48 ms active(16.05 ms span · 16.10 ms median · p95 17.00)
next-yak css-prop 9.7.0 foldStatic: false0.49 ms active(16.68 ms span · 16.70 ms median · p95 18.60)
next-yak css-prop 9.6.00.49 ms active(15.21 ms span · 15.20 ms median · p95 18.90)
cnfast0.51 ms active(16.77 ms span · 16.70 ms median · p95 19.30)
Panda (css fn)0.53 ms active(16.17 ms span · 16.20 ms median · p95 17.80)
next-yak 9.7.0 (styled API)0.54 ms active(16.63 ms span · 16.70 ms median · p95 17.50)
StyleX0.56 ms active(16.61 ms span · 16.70 ms median · p95 17.90)
vanilla (hand-written ceiling)0.58 ms active(16.71 ms span · 16.80 ms median · p95 18.30)
Goober0.64 ms active(16.66 ms span · 16.60 ms median · p95 17.40)
next-yak 9.6.00.66 ms active(16.45 ms span · 16.50 ms median · p95 18.80)
Emotion0.71 ms active(15.86 ms span · 15.90 ms median · p95 18.20)
Panda (style props)1.29 ms active(16.98 ms span · 17.10 ms median · p95 19.40)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Cold mount — repeated timing + Chrome-profiled span anatomyiStarting from a blank root (no SSR markup), a "click" renders the whole workload from scratch (createRoot().render()), then we wait for the first paint. Unlike hydration — which attaches to existing server HTML — this is a cold client mount, so the first paint includes each runtimelibrary's first style injection into the document. The profiled span shows how much of the commit is JS, style, layout, paint, GC, browser work and idle. Lower is better.

next-yak css-prop 9.7.028 ms ±86%
next-yak 9.7.0 (styled API)70 ms ±33%
vanilla (hand-written ceiling)71 ms ±32%
StyleX71 ms ±3%
StyleX without CSS layers (:not() specificity hack)73 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false74 ms ±2%
next-yak css-prop 9.7.0 foldStatic: false75 ms ±1%
next-yak css-prop 9.6.076 ms ±2%
next-yak 9.6.079 ms ±3%
cnfast85 ms ±2%
tailwind-merge86 ms ±2%
Goober94 ms ±1%
styled-components98 ms ±1%
Panda (css fn)99 ms ±2%
Emotion108 ms ±2%
Panda (style props)115 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak css-prop 9.7.011.28 ms active(16.85 ms span · 17.00 ms median)
StyleX without CSS layers (:not() specificity hack)11.31 ms active(14.41 ms span · 14.60 ms median)
next-yak css-prop 9.6.011.56 ms active(20.19 ms span · 20.20 ms median)
StyleX11.67 ms active(17.81 ms span · 17.90 ms median)
Goober11.70 ms active(15.86 ms span · 16.00 ms median)
next-yak css-prop 9.7.0 foldStatic: false11.74 ms active(21.26 ms span · 21.40 ms median)
vanilla (hand-written ceiling)11.80 ms active(23.06 ms span · 23.20 ms median)
next-yak 9.6.012.16 ms active(19.83 ms span · 19.90 ms median)
styled-components12.71 ms active(17.31 ms span · 17.40 ms median)
next-yak 9.7.0 (styled API) foldStatic: false13.04 ms active(24.07 ms span · 24.10 ms median)
Panda (css fn)13.22 ms active(18.54 ms span · 18.60 ms median)
next-yak 9.7.0 (styled API)13.62 ms active(21.42 ms span · 21.50 ms median)
Emotion13.82 ms active(16.23 ms span · 16.30 ms median)
Panda (style props)13.92 ms active(18.94 ms span · 19.10 ms median)
cnfast15.31 ms active(21.43 ms span · 21.50 ms median)
tailwind-merge15.70 ms active(21.56 ms span · 21.70 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiWhere the browser's rendering time goes on a cold mount (not JS — the engine's own style-recalc, layout and paint), profiled with web-performance-debugger in two engines. This is where runtime CSS-in-JS pays a tax build-time lanes don't: it injects a style rule per instance, so the engine recalculates styles once per instance — Chrome's authoritative signal is that style-recalc count (the badge; e.g. 50 instances → ~50 recalcs vs 1 for extracted CSS). Firefox (Gecko) reports sampled style/layout time; a zero sampled slice is not proof of no work, so its exact counts are retained as diagnostics but the chart never treats zero as absence. Bars are ms; compare within an engine. Lower is better. Generated via pnpm setup:wpd + pnpm gen:wpd.

style recalclayoutpaint (Chrome)
cnfastChrome5.9 ms1 recalc
Firefox11.0 ms
EmotionChrome5.9 ms1 recalc
Firefox11.0 ms
next-yak 9.6.0Chrome5.9 ms1 recalc
Firefox8.0 ms
next-yak 9.7.0 (styled API)Chrome8.1 ms1 recalc
Firefox7.7 ms
next-yak css-prop 9.6.0Chrome5.7 ms1 recalc
Firefox8.1 ms
next-yak css-prop 9.7.0Chrome5.7 ms1 recalc
Firefox7.3 ms
next-yak css-prop 9.7.0 foldStatic: falseChrome5.6 ms1 recalc
Firefox9.0 ms
Panda (css fn)Chrome5.6 ms1 recalc
Firefox8.9 ms
Panda (style props)Chrome5.7 ms1 recalc
Firefox8.1 ms
styled-componentsChrome5.7 ms1 recalc
Firefox10.7 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.8 ms1 recalc
Firefox8.1 ms
tailwind-mergeChrome6.3 ms1 recalc
Firefox7.9 ms
vanilla (hand-written ceiling)Chrome5.8 ms1 recalc
Firefox9.1 ms
StyleXChrome5.9 ms1 recalc
Firefox8.1 ms
GooberChrome5.5 ms1 recalc
Firefox7.8 ms
next-yak 9.7.0 (styled API) foldStatic: falseChrome7.2 ms1 recalc
Firefox10.1 ms

Cold mount of 50 instances. Chrome's trustworthy signal is the style-recalc count (badge); Firefox reports sampled Gecko style/layout ms but no main-thread paint. A zero sampled slice is not proof that no work occurred; Chrome's exact count badges are the reliable presence/absence signal.

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads for this page: the client JS runtime the lane ships (over the bare React floor), the CSS, and the SSR HTML. Lower is better.

JSCSSHTML
next-yak css-prop 9.7.02,669 B(0.0 / 0.1 / 2.5 kB)
vanilla (hand-written ceiling)2,701 B(0.0 / 0.3 / 2.4 kB)
next-yak css-prop 9.6.03,047 B(0.4 / 0.1 / 2.5 kB)
next-yak css-prop 9.7.0 foldStatic: false3,176 B(0.5 / 0.1 / 2.5 kB)
next-yak 9.7.0 (styled API)3,768 B(1.1 / 0.1 / 2.5 kB)
next-yak 9.6.03,771 B(1.1 / 0.1 / 2.5 kB)
next-yak 9.7.0 (styled API) foldStatic: false3,971 B(1.3 / 0.1 / 2.5 kB)
StyleX without CSS layers (:not() specificity hack)5,524 B(2.5 / 0.2 / 2.7 kB)
StyleX5,616 B(2.6 / 0.2 / 2.7 kB)
Goober6,273 B(3.4 / 0.1 / 2.6 kB)
Panda (css fn)6,957 B(3.5 / 0.2 / 3.1 kB)
Panda (style props)11,272 B(7.8 / 0.2 / 3.1 kB)
tailwind-merge13,631 B(10.2 / 0.2 / 2.9 kB)
cnfast14,623 B(11.2 / 0.2 / 2.9 kB)
Emotion15,373 B(12.4 / 0.2 / 2.4 kB)
styled-components16,017 B(12.9 / 0.2 / 2.6 kB)

Scaling — SSR render time (ms) vs instance countiSSR render time as the workload grows from a handful to thousands of instances — shows how each lane's per-element cost compounds. A flatter line scales better.

0.07.41522291005001,0002,0004,000
cnfastEmotionGoobernext-yak 9.6.0next-yak 9.7.0 (styled API)next-yak 9.7.0 (styled API) foldStatic: falsenext-yak css-prop 9.6.0next-yak css-prop 9.7.0next-yak css-prop 9.7.0 foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)tailwind-mergevanilla (hand-written ceiling)StyleX
Composed components (3 levels)n = 1,000low cardinality

A Button wrapped by two more components, each adding styles and threading className down. next-yak flattens the chain at build time (depth ≈ free); the Tailwind lanes pay one merge per level and styled-components/Emotion run a wrapper component at each.

At three levels the styled perf runtime holds its library slice flat at 0.139 ms while the 9.6.0 baseline's climbs to 1.38 ms; the css-prop fold merges all three levels into one class and ties vanilla.

The middle of the composition ladder (compose-1, compose-6 its siblings). The perf runtime's library slice is flat across all three depths (~0.138-0.139 ms), where the 9.6.0 baseline's climbs 0.460 to 1.38 to 3.65 ms; the css-prop fold ties vanilla at every depth by merging the chain into one class.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriHow many times per second this lane renders the whole workload to an HTML string in Node (renderToString), timing the production render only — any build-time CSS collection (a Tailwind JIT, a Panda sheet slice) is excluded. Higher is better.

next-yak css-prop 9.7.01,971,741 r/s ±3%(0.51 ms / 1k elems)
vanilla (hand-written ceiling)1,969,799 r/s ±3%(0.51 ms / 1k elems)
next-yak css-prop 9.7.0 foldStatic: false1,676,797 r/s ±2%(0.60 ms / 1k elems)
next-yak css-prop 9.6.01,587,405 r/s ±7%(0.63 ms / 1k elems)
next-yak 9.7.0 (styled API)1,467,980 r/s ±5%(0.68 ms / 1k elems)
cnfast1,309,329 r/s ±3%(0.76 ms / 1k elems)
next-yak 9.7.0 (styled API) foldStatic: false1,245,848 r/s ±4%(0.80 ms / 1k elems)
StyleX1,061,384 r/s ±7%(0.94 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,046,983 r/s ±4%(0.96 ms / 1k elems)
tailwind-merge1,029,866 r/s ±5%(0.97 ms / 1k elems)
styled-components791,035 r/s ±2%(1.26 ms / 1k elems)
Goober561,627 r/s ±4%(1.78 ms / 1k elems)
Emotion411,607 r/s ±3%(2.43 ms / 1k elems)
next-yak 9.6.0400,849 r/s ±3%(2.49 ms / 1k elems)
Panda (css fn)100,320 r/s ±9%(9.97 ms / 1k elems)
Panda (style props)93,367 r/s ±5%(10.71 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriRequests/sec the lane sustains under concurrent HTTP load (autocannon) serving the SSR render end-to-end — a more realistic server measure than the in-process microbench. Higher is better.

next-yak css-prop 9.7.01,544 req/s ±0%
vanilla (hand-written ceiling)1,543 req/s ±0%
next-yak css-prop 9.7.0 foldStatic: false1,358 req/s ±1%
next-yak css-prop 9.6.01,336 req/s ±0%
next-yak 9.7.0 (styled API)1,243 req/s ±0%
next-yak 9.7.0 (styled API) foldStatic: false1,094 req/s ±1%
cnfast947 req/s ±1%
StyleX without CSS layers (:not() specificity hack)854 req/s ±0%
StyleX849 req/s ±0%
tailwind-merge822 req/s ±0%
styled-components675 req/s ±0%
Goober502 req/s ±0%
Emotion399 req/s ±0%
next-yak 9.6.0379 req/s ±0%
Panda (style props)87 req/s ±1%
Panda (css fn)86 req/s ±1%

Where the SSR render time goes — Node CPU profile · median ms / renderiThe median server renderToString(), split by CPU self-time from a sampled V8 profile mapped through source maps (recorded with web-performance-debugger 0.15.1): react-dom (the floor every lane shares), the styling library's runtime, and your component. other is GC / unattributed native work.

react-dom (the shared floor)styling library runtimeyour componentnode / gc / unattributed
next-yak css-prop 9.7.00.56 ms
vanilla (hand-written ceiling)0.58 ms
next-yak css-prop 9.6.00.66 ms
next-yak 9.7.0 (styled API)0.70 ms
next-yak 9.7.0 (styled API) foldStatic: false0.90 ms
cnfast0.94 ms
next-yak css-prop 9.7.0 foldStatic: false0.97 ms
tailwind-merge1.11 ms
StyleX1.16 ms
StyleX without CSS layers (:not() specificity hack)1.16 ms
styled-components1.41 ms
Goober1.76 ms
Emotion2.43 ms
next-yak 9.6.02.45 ms
Panda (style props)11.32 ms
Panda (css fn)12.55 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiTime for React to hydrate the server HTML in the browser — attach event handlers and build the fiber tree over the existing DOM (it does not re-create markup). The first chart is the existing repeated end-to-end timing; the profiled chart then splits one instrumented commit into JS, style, layout, paint, GC, browser work and idle (recorded with web-performance-debugger 0.15.1). Lower is better.

next-yak css-prop 9.7.048 ms ±46%
next-yak css-prop 9.7.0 foldStatic: false48 ms ±49%
next-yak 9.7.0 (styled API)49 ms ±48%
vanilla (hand-written ceiling)49 ms ±47%
next-yak 9.7.0 (styled API) foldStatic: false50 ms ±45%
StyleX51 ms ±1%
StyleX without CSS layers (:not() specificity hack)53 ms ±44%
next-yak 9.6.059 ms ±38%
cnfast59 ms ±1%
tailwind-merge60 ms ±3%
Emotion73 ms ±31%
next-yak css-prop 9.6.091 ms ±25%
Panda (css fn)92 ms ±1%
Panda (style props)99 ms ±1%
Goober102 ms ±21%
styled-components142 ms ±16%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak 9.7.0 (styled API) foldStatic: false5.28 ms active(14.57 ms span · 14.70 ms median)
next-yak css-prop 9.7.05.31 ms active(15.01 ms span · 15.10 ms median)
next-yak 9.7.0 (styled API)5.50 ms active(12.15 ms span · 12.30 ms median)
vanilla (hand-written ceiling)5.56 ms active(16.46 ms span · 16.60 ms median)
StyleX5.60 ms active(18.00 ms span · 18.10 ms median)
next-yak css-prop 9.7.0 foldStatic: false5.61 ms active(12.10 ms span · 12.20 ms median)
next-yak 9.6.05.62 ms active(12.27 ms span · 12.30 ms median)
next-yak css-prop 9.6.05.72 ms active(13.31 ms span · 13.50 ms median)
StyleX without CSS layers (:not() specificity hack)5.76 ms active(12.46 ms span · 12.60 ms median)
Goober7.37 ms active(18.92 ms span · 19.00 ms median)
Panda (css fn)7.51 ms active(16.32 ms span · 16.40 ms median)
styled-components7.74 ms active(22.73 ms span · 22.90 ms median)
Emotion7.89 ms active(21.34 ms span · 21.50 ms median)
Panda (style props)8.34 ms active(16.74 ms span · 16.80 ms median)
tailwind-merge9.38 ms active(18.04 ms span · 18.20 ms median)
cnfast9.48 ms active(13.09 ms span · 13.10 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Interaction re-render — repeated timing + Chrome-profiled span anatomyiA state change triggers a synchronous re-render (flushSync) of the whole mounted workload, then we wait for the next paint — click→paint latency. The profile separates active work from the frame-alignment idle that used to dominate this number. This is where runtime CSS-in-JS libraries re-run their per-element styling on every update; build-time lanes (next-yak / Panda / Tailwind / vanilla) do almost none. Lower is better.

StyleX6 ms ±14%
StyleX without CSS layers (:not() specificity hack)7 ms ±15%
cnfast7 ms ±17%
next-yak css-prop 9.6.07 ms ±10%
next-yak css-prop 9.7.0 foldStatic: false7 ms ±8%
tailwind-merge7 ms ±9%
next-yak 9.7.0 (styled API) foldStatic: false8 ms ±7%
styled-components8 ms ±10%
vanilla (hand-written ceiling)8 ms ±9%
next-yak 9.7.0 (styled API)8 ms ±11%
next-yak css-prop 9.7.08 ms ±11%
Emotion10 ms ±6%
Goober10 ms ±30%
next-yak 9.6.011 ms ±29%
Panda (css fn)37 ms ±8%
Panda (style props)38 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak css-prop 9.7.00.36 ms active(16.70 ms span · 16.70 ms median · p95 17.50)
next-yak css-prop 9.7.0 foldStatic: false0.36 ms active(15.81 ms span · 15.80 ms median · p95 17.60)
cnfast0.37 ms active(16.36 ms span · 16.40 ms median · p95 17.90)
Goober0.45 ms active(17.18 ms span · 17.20 ms median · p95 18.50)
next-yak 9.7.0 (styled API)0.48 ms active(16.59 ms span · 16.60 ms median · p95 17.10)
next-yak 9.7.0 (styled API) foldStatic: false0.50 ms active(16.59 ms span · 16.70 ms median · p95 19.60)
tailwind-merge0.54 ms active(16.71 ms span · 16.70 ms median · p95 16.90)
styled-components0.54 ms active(14.95 ms span · 15.00 ms median · p95 19.30)
vanilla (hand-written ceiling)0.55 ms active(16.76 ms span · 16.80 ms median · p95 18.50)
StyleX without CSS layers (:not() specificity hack)0.56 ms active(16.67 ms span · 16.80 ms median · p95 18.20)
next-yak css-prop 9.6.00.57 ms active(16.66 ms span · 16.60 ms median · p95 17.30)
next-yak 9.6.00.63 ms active(15.42 ms span · 15.50 ms median · p95 17.70)
StyleX0.65 ms active(15.43 ms span · 15.50 ms median · p95 18.90)
Emotion0.93 ms active(16.41 ms span · 16.40 ms median · p95 17.10)
Panda (css fn)0.95 ms active(15.02 ms span · 15.10 ms median · p95 19.80)
Panda (style props)1.02 ms active(16.55 ms span · 16.60 ms median · p95 16.70)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Cold mount — repeated timing + Chrome-profiled span anatomyiStarting from a blank root (no SSR markup), a "click" renders the whole workload from scratch (createRoot().render()), then we wait for the first paint. Unlike hydration — which attaches to existing server HTML — this is a cold client mount, so the first paint includes each runtimelibrary's first style injection into the document. The profiled span shows how much of the commit is JS, style, layout, paint, GC, browser work and idle. Lower is better.

next-yak css-prop 9.7.028 ms ±85%
vanilla (hand-written ceiling)70 ms ±32%
next-yak 9.7.0 (styled API)75 ms ±3%
next-yak css-prop 9.7.0 foldStatic: false75 ms ±3%
next-yak 9.7.0 (styled API) foldStatic: false75 ms ±3%
next-yak css-prop 9.6.076 ms ±2%
StyleX80 ms ±3%
StyleX without CSS layers (:not() specificity hack)81 ms ±3%
next-yak 9.6.086 ms ±2%
cnfast87 ms ±2%
tailwind-merge88 ms ±1%
styled-components97 ms ±1%
Goober101 ms ±2%
Emotion109 ms ±2%
Panda (css fn)119 ms ±2%
Panda (style props)127 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak 9.7.0 (styled API)11.25 ms active(18.09 ms span · 18.10 ms median)
next-yak 9.7.0 (styled API) foldStatic: false11.29 ms active(16.00 ms span · 16.10 ms median)
next-yak css-prop 9.7.011.35 ms active(15.66 ms span · 15.80 ms median)
next-yak css-prop 9.6.011.49 ms active(16.48 ms span · 16.50 ms median)
StyleX11.63 ms active(18.71 ms span · 18.80 ms median)
next-yak css-prop 9.7.0 foldStatic: false11.65 ms active(19.22 ms span · 19.40 ms median)
StyleX without CSS layers (:not() specificity hack)11.75 ms active(16.78 ms span · 16.90 ms median)
next-yak 9.6.012.02 ms active(13.95 ms span · 14.00 ms median)
vanilla (hand-written ceiling)12.26 ms active(17.32 ms span · 17.40 ms median)
styled-components13.13 ms active(15.24 ms span · 15.40 ms median)
Goober13.75 ms active(20.47 ms span · 20.50 ms median)
Panda (css fn)14.34 ms active(17.26 ms span · 17.40 ms median)
Emotion14.60 ms active(19.67 ms span · 19.80 ms median)
Panda (style props)14.89 ms active(16.23 ms span · 16.30 ms median)
tailwind-merge15.02 ms active(23.56 ms span · 23.60 ms median)
cnfast15.66 ms active(17.76 ms span · 17.90 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiWhere the browser's rendering time goes on a cold mount (not JS — the engine's own style-recalc, layout and paint), profiled with web-performance-debugger in two engines. This is where runtime CSS-in-JS pays a tax build-time lanes don't: it injects a style rule per instance, so the engine recalculates styles once per instance — Chrome's authoritative signal is that style-recalc count (the badge; e.g. 50 instances → ~50 recalcs vs 1 for extracted CSS). Firefox (Gecko) reports sampled style/layout time; a zero sampled slice is not proof of no work, so its exact counts are retained as diagnostics but the chart never treats zero as absence. Bars are ms; compare within an engine. Lower is better. Generated via pnpm setup:wpd + pnpm gen:wpd.

style recalclayoutpaint (Chrome)
EmotionChrome6.2 ms1 recalc
Firefox8.0 ms
GooberChrome6.8 ms1 recalc
Firefox8.0 ms
next-yak 9.6.0Chrome5.9 ms1 recalc
Firefox8.0 ms
next-yak 9.7.0 (styled API)Chrome5.6 ms1 recalc
Firefox8.0 ms
next-yak 9.7.0 (styled API) foldStatic: falseChrome5.6 ms1 recalc
Firefox8.0 ms
next-yak css-prop 9.6.0Chrome5.7 ms1 recalc
Firefox7.7 ms
next-yak css-prop 9.7.0Chrome5.7 ms1 recalc
Firefox7.0 ms
next-yak css-prop 9.7.0 foldStatic: falseChrome5.8 ms1 recalc
Firefox8.0 ms
Panda (css fn)Chrome6.0 ms1 recalc
Firefox8.9 ms
Panda (style props)Chrome6.3 ms1 recalc
Firefox7.0 ms
styled-componentsChrome5.9 ms1 recalc
Firefox14.9 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.7 ms1 recalc
Firefox6.9 ms
tailwind-mergeChrome5.5 ms1 recalc
Firefox8.0 ms
vanilla (hand-written ceiling)Chrome6.3 ms1 recalc
Firefox8.9 ms
StyleXChrome5.5 ms1 recalc
Firefox10.0 ms
cnfastChrome5.8 ms1 recalc
Firefox9.1 ms

Cold mount of 50 instances. Chrome's trustworthy signal is the style-recalc count (badge); Firefox reports sampled Gecko style/layout ms but no main-thread paint. A zero sampled slice is not proof that no work occurred; Chrome's exact count badges are the reliable presence/absence signal.

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads for this page: the client JS runtime the lane ships (over the bare React floor), the CSS, and the SSR HTML. Lower is better.

JSCSSHTML
next-yak css-prop 9.7.02,658 B(0.0 / 0.1 / 2.5 kB)
next-yak css-prop 9.6.03,032 B(0.4 / 0.1 / 2.5 kB)
vanilla (hand-written ceiling)3,045 B(0.0 / 0.4 / 2.6 kB)
next-yak css-prop 9.7.0 foldStatic: false3,168 B(0.5 / 0.1 / 2.5 kB)
next-yak 9.7.0 (styled API)3,884 B(1.1 / 0.2 / 2.6 kB)
next-yak 9.6.03,895 B(1.1 / 0.2 / 2.6 kB)
next-yak 9.7.0 (styled API) foldStatic: false4,090 B(1.3 / 0.2 / 2.6 kB)
StyleX without CSS layers (:not() specificity hack)5,750 B(2.5 / 0.3 / 2.9 kB)
StyleX5,840 B(2.6 / 0.3 / 2.9 kB)
Goober6,358 B(3.4 / 0.2 / 2.6 kB)
Panda (css fn)7,173 B(3.5 / 0.3 / 3.2 kB)
Panda (style props)11,438 B(7.8 / 0.2 / 3.2 kB)
tailwind-merge13,911 B(10.2 / 0.3 / 3.1 kB)
cnfast14,903 B(11.2 / 0.3 / 3.1 kB)
Emotion15,424 B(12.4 / 0.2 / 2.4 kB)
styled-components16,278 B(12.9 / 0.3 / 2.7 kB)

Scaling — SSR render time (ms) vs instance countiSSR render time as the workload grows from a handful to thousands of instances — shows how each lane's per-element cost compounds. A flatter line scales better.

0.0112233441005001,0002,0004,000
cnfastEmotionGoobernext-yak 9.6.0next-yak 9.7.0 (styled API)next-yak 9.7.0 (styled API) foldStatic: falsenext-yak css-prop 9.6.0next-yak css-prop 9.7.0next-yak css-prop 9.7.0 foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)tailwind-mergevanilla (hand-written ceiling)StyleX
Composition6 levelsn = 1,000low cardinality

The compose-3 button family at depth 6: the same base button wrapped five times, each level adding one small border-left/padding-left declaration. Brackets compose-3 from above (compose-1 brackets it from below) to chart how per-element cost grows with composition depth — the boundary that defeats next-yak's JSX folding.

At six levels the split is total: the styled perf runtime holds 0.671 ms/1k where the 9.6.0 baseline hits 5.26 (7.8×), and the css-prop fold ships one 2.66 kB class that ties vanilla while panda's inline css() reaches 18.5 ms/1k.

The deep end of the composition ladder (compose-1, compose-3 above). Six levels is where the perf runtime earns its default: 0.671 ms/1k against the 9.6.0 baseline's 5.26 (7.8×), while its library slice stays flat at 0.138 ms. The css-prop fold's one concatenated class holds vanilla's render cost and its 2.66 kB payload at every depth.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriHow many times per second this lane renders the whole workload to an HTML string in Node (renderToString), timing the production render only — any build-time CSS collection (a Tailwind JIT, a Panda sheet slice) is excluded. Higher is better.

next-yak css-prop 9.7.01,968,341 r/s ±2%(0.51 ms / 1k elems)
vanilla (hand-written ceiling)1,963,834 r/s ±3%(0.51 ms / 1k elems)
next-yak css-prop 9.7.0 foldStatic: false1,674,458 r/s ±4%(0.60 ms / 1k elems)
next-yak css-prop 9.6.01,664,010 r/s ±3%(0.60 ms / 1k elems)
next-yak 9.7.0 (styled API)1,491,331 r/s ±7%(0.67 ms / 1k elems)
next-yak 9.7.0 (styled API) foldStatic: false1,144,547 r/s ±4%(0.87 ms / 1k elems)
cnfast876,488 r/s ±4%(1.14 ms / 1k elems)
StyleX767,951 r/s ±4%(1.30 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)740,329 r/s ±6%(1.35 ms / 1k elems)
styled-components680,234 r/s ±4%(1.47 ms / 1k elems)
tailwind-merge639,250 r/s ±4%(1.56 ms / 1k elems)
Emotion337,126 r/s ±2%(2.97 ms / 1k elems)
Goober321,363 r/s ±2%(3.11 ms / 1k elems)
next-yak 9.6.0190,230 r/s ±2%(5.26 ms / 1k elems)
Panda (style props)80,313 r/s ±4%(12.45 ms / 1k elems)
Panda (css fn)54,108 r/s ±7%(18.48 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriRequests/sec the lane sustains under concurrent HTTP load (autocannon) serving the SSR render end-to-end — a more realistic server measure than the in-process microbench. Higher is better.

next-yak css-prop 9.7.01,544 req/s ±1%
vanilla (hand-written ceiling)1,517 req/s ±0%
next-yak css-prop 9.7.0 foldStatic: false1,357 req/s ±0%
next-yak css-prop 9.6.01,352 req/s ±0%
next-yak 9.7.0 (styled API)1,219 req/s ±0%
next-yak 9.7.0 (styled API) foldStatic: false999 req/s ±0%
cnfast701 req/s ±0%
StyleX626 req/s ±0%
StyleX without CSS layers (:not() specificity hack)625 req/s ±0%
styled-components572 req/s ±0%
tailwind-merge544 req/s ±0%
Emotion321 req/s ±0%
Goober293 req/s ±0%
next-yak 9.6.0181 req/s
Panda (style props)78 req/s ±1%
Panda (css fn)52 req/s ±1%

Where the SSR render time goes — Node CPU profile · median ms / renderiThe median server renderToString(), split by CPU self-time from a sampled V8 profile mapped through source maps (recorded with web-performance-debugger 0.15.1): react-dom (the floor every lane shares), the styling library's runtime, and your component. other is GC / unattributed native work.

react-dom (the shared floor)styling library runtimeyour componentnode / gc / unattributed
vanilla (hand-written ceiling)0.59 ms
next-yak css-prop 9.6.00.66 ms
next-yak css-prop 9.7.0 foldStatic: false0.66 ms
next-yak 9.7.0 (styled API)0.70 ms
next-yak css-prop 9.7.00.78 ms
next-yak 9.7.0 (styled API) foldStatic: false0.86 ms
cnfast1.17 ms
StyleX1.43 ms
StyleX without CSS layers (:not() specificity hack)1.45 ms
styled-components1.66 ms
tailwind-merge1.67 ms
Goober2.94 ms
Emotion3.11 ms
next-yak 9.6.05.19 ms
Panda (style props)13.27 ms
Panda (css fn)22.77 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiTime for React to hydrate the server HTML in the browser — attach event handlers and build the fiber tree over the existing DOM (it does not re-create markup). The first chart is the existing repeated end-to-end timing; the profiled chart then splits one instrumented commit into JS, style, layout, paint, GC, browser work and idle (recorded with web-performance-debugger 0.15.1). Lower is better.

vanilla (hand-written ceiling)47 ms ±48%
next-yak 9.7.0 (styled API) foldStatic: false48 ms ±44%
next-yak css-prop 9.6.049 ms ±49%
next-yak css-prop 9.7.0 foldStatic: false49 ms ±47%
next-yak 9.7.0 (styled API)50 ms ±46%
StyleX56 ms ±2%
StyleX without CSS layers (:not() specificity hack)57 ms ±2%
cnfast63 ms ±2%
tailwind-merge65 ms ±1%
next-yak 9.6.070 ms ±2%
next-yak css-prop 9.7.091 ms ±24%
styled-components101 ms ±3%
Panda (style props)110 ms ±3%
Goober112 ms ±18%
Emotion120 ms ±19%
Panda (css fn)122 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)5.21 ms active(16.61 ms span · 16.60 ms median)
next-yak css-prop 9.7.05.32 ms active(20.86 ms span · 20.90 ms median)
next-yak 9.7.0 (styled API) foldStatic: false5.35 ms active(16.02 ms span · 16.10 ms median)
next-yak css-prop 9.6.05.38 ms active(13.85 ms span · 14.00 ms median)
next-yak 9.7.0 (styled API)5.42 ms active(11.28 ms span · 11.40 ms median)
next-yak css-prop 9.7.0 foldStatic: false5.44 ms active(16.32 ms span · 16.50 ms median)
StyleX5.80 ms active(16.72 ms span · 16.90 ms median)
next-yak 9.6.05.88 ms active(11.56 ms span · 11.60 ms median)
StyleX without CSS layers (:not() specificity hack)6.19 ms active(14.93 ms span · 15.10 ms median)
styled-components7.91 ms active(18.84 ms span · 19.00 ms median)
Emotion8.10 ms active(16.22 ms span · 16.40 ms median)
Goober8.21 ms active(23.54 ms span · 23.60 ms median)
Panda (style props)8.26 ms active(10.59 ms span · 10.70 ms median)
Panda (css fn)8.50 ms active(16.81 ms span · 16.80 ms median)
tailwind-merge9.54 ms active(13.80 ms span · 13.80 ms median)
cnfast9.72 ms active(14.57 ms span · 14.70 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Interaction re-render — repeated timing + Chrome-profiled span anatomyiA state change triggers a synchronous re-render (flushSync) of the whole mounted workload, then we wait for the next paint — click→paint latency. The profile separates active work from the frame-alignment idle that used to dominate this number. This is where runtime CSS-in-JS libraries re-run their per-element styling on every update; build-time lanes (next-yak / Panda / Tailwind / vanilla) do almost none. Lower is better.

styled-components7 ms ±15%
cnfast7 ms ±12%
next-yak css-prop 9.6.07 ms ±14%
tailwind-merge7 ms ±12%
vanilla (hand-written ceiling)7 ms ±5%
next-yak 9.7.0 (styled API)7 ms ±9%
next-yak 9.7.0 (styled API) foldStatic: false7 ms ±12%
next-yak css-prop 9.7.0 foldStatic: false8 ms ±11%
next-yak css-prop 9.7.08 ms ±6%
StyleX8 ms ±7%
StyleX without CSS layers (:not() specificity hack)8 ms ±6%
Emotion12 ms ±10%
Goober23 ms ±8%
next-yak 9.6.024 ms ±2%
Panda (style props)44 ms ±3%
Panda (css fn)59 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)0.31 ms active(16.33 ms span · 16.40 ms median · p95 17.50)
next-yak 9.7.0 (styled API) foldStatic: false0.31 ms active(15.46 ms span · 15.50 ms median · p95 19.70)
next-yak css-prop 9.7.00.35 ms active(15.96 ms span · 15.90 ms median · p95 18.70)
next-yak css-prop 9.6.00.45 ms active(16.73 ms span · 16.70 ms median · p95 18.10)
cnfast0.47 ms active(14.97 ms span · 15.00 ms median · p95 20.60)
tailwind-merge0.48 ms active(15.62 ms span · 15.70 ms median · p95 20.50)
StyleX without CSS layers (:not() specificity hack)0.49 ms active(15.88 ms span · 15.90 ms median · p95 19.00)
next-yak 9.7.0 (styled API)0.49 ms active(16.46 ms span · 16.50 ms median · p95 17.40)
StyleX0.51 ms active(16.67 ms span · 16.70 ms median · p95 16.90)
Emotion0.52 ms active(16.62 ms span · 16.60 ms median · p95 16.70)
Goober0.66 ms active(16.72 ms span · 16.70 ms median · p95 17.00)
next-yak css-prop 9.7.0 foldStatic: false0.71 ms active(17.25 ms span · 17.30 ms median · p95 17.80)
next-yak 9.6.00.78 ms active(16.71 ms span · 16.70 ms median · p95 16.80)
styled-components0.83 ms active(16.64 ms span · 16.60 ms median · p95 16.80)
Panda (style props)1.22 ms active(16.26 ms span · 16.30 ms median · p95 16.70)
Panda (css fn)1.30 ms active(16.00 ms span · 16.00 ms median · p95 19.00)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Cold mount — repeated timing + Chrome-profiled span anatomyiStarting from a blank root (no SSR markup), a "click" renders the whole workload from scratch (createRoot().render()), then we wait for the first paint. Unlike hydration — which attaches to existing server HTML — this is a cold client mount, so the first paint includes each runtimelibrary's first style injection into the document. The profiled span shows how much of the commit is JS, style, layout, paint, GC, browser work and idle. Lower is better.

next-yak css-prop 9.7.070 ms ±33%
vanilla (hand-written ceiling)72 ms ±32%
next-yak css-prop 9.6.074 ms ±2%
next-yak css-prop 9.7.0 foldStatic: false74 ms ±3%
next-yak 9.7.0 (styled API)75 ms ±2%
next-yak 9.7.0 (styled API) foldStatic: false75 ms ±2%
StyleX83 ms ±2%
StyleX without CSS layers (:not() specificity hack)85 ms ±3%
cnfast91 ms ±3%
tailwind-merge94 ms ±1%
styled-components99 ms ±1%
next-yak 9.6.099 ms ±2%
Goober112 ms ±1%
Emotion114 ms ±2%
Panda (style props)137 ms ±2%
Panda (css fn)151 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak css-prop 9.7.011.31 ms active(16.46 ms span · 16.60 ms median)
vanilla (hand-written ceiling)11.54 ms active(17.69 ms span · 17.80 ms median)
next-yak css-prop 9.6.011.77 ms active(19.02 ms span · 19.10 ms median)
StyleX without CSS layers (:not() specificity hack)11.84 ms active(18.27 ms span · 18.50 ms median)
next-yak css-prop 9.7.0 foldStatic: false11.95 ms active(22.54 ms span · 22.60 ms median)
next-yak 9.7.0 (styled API) foldStatic: false12.25 ms active(14.66 ms span · 14.70 ms median)
next-yak 9.7.0 (styled API)12.33 ms active(22.34 ms span · 22.60 ms median)
StyleX12.37 ms active(21.45 ms span · 21.60 ms median)
next-yak 9.6.012.44 ms active(22.00 ms span · 22.10 ms median)
Goober12.79 ms active(22.51 ms span · 22.60 ms median)
styled-components13.55 ms active(19.06 ms span · 19.20 ms median)
Emotion14.35 ms active(17.36 ms span · 17.40 ms median)
Panda (css fn)15.30 ms active(18.41 ms span · 18.50 ms median)
cnfast16.66 ms active(23.91 ms span · 24.00 ms median)
tailwind-merge17.04 ms active(25.25 ms span · 25.30 ms median)
Panda (style props)17.06 ms active(21.54 ms span · 21.70 ms median)

Chrome-profiled first-span anatomy (web-performance-debugger 0.15.1); segments reconcile exactly to span wall. Rank uses active time (wall minus idle). Repeated timing median is shown when available; slice anatomy is retained for the first iteration only.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiWhere the browser's rendering time goes on a cold mount (not JS — the engine's own style-recalc, layout and paint), profiled with web-performance-debugger in two engines. This is where runtime CSS-in-JS pays a tax build-time lanes don't: it injects a style rule per instance, so the engine recalculates styles once per instance — Chrome's authoritative signal is that style-recalc count (the badge; e.g. 50 instances → ~50 recalcs vs 1 for extracted CSS). Firefox (Gecko) reports sampled style/layout time; a zero sampled slice is not proof of no work, so its exact counts are retained as diagnostics but the chart never treats zero as absence. Bars are ms; compare within an engine. Lower is better. Generated via pnpm setup:wpd + pnpm gen:wpd.

style recalclayoutpaint (Chrome)
cnfastChrome5.7 ms1 recalc
Firefox8.0 ms
GooberChrome5.6 ms1 recalc
Firefox8.0 ms
next-yak 9.6.0Chrome6.0 ms1 recalc
Firefox8.1 ms
next-yak 9.7.0 (styled API)Chrome6.5 ms1 recalc
Firefox8.0 ms
next-yak 9.7.0 (styled API) foldStatic: falseChrome6.1 ms1 recalc
Firefox8.8 ms
next-yak css-prop 9.6.0Chrome5.8 ms1 recalc
Firefox6.9 ms
next-yak css-prop 9.7.0Chrome5.6 ms1 recalc
Firefox7.1 ms
next-yak css-prop 9.7.0 foldStatic: falseChrome5.8 ms1 recalc
Firefox8.2 ms
styled-componentsChrome6.1 ms1 recalc
Firefox9.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.7 ms1 recalc
Firefox8.0 ms
tailwind-mergeChrome6.6 ms1 recalc
Firefox9.3 ms
vanilla (hand-written ceiling)Chrome5.8 ms1 recalc
Firefox7.0 ms
StyleXChrome5.6 ms1 recalc
Firefox8.0 ms
Panda (style props)Chrome7.5 ms1 recalc
Firefox7.9 ms
EmotionChrome5.8 ms1 recalc
Firefox8.0 ms
Panda (css fn)Chrome5.9 ms1 recalc
Firefox14.1 ms

Cold mount of 50 instances. Chrome's trustworthy signal is the style-recalc count (badge); Firefox reports sampled Gecko style/layout ms but no main-thread paint. A zero sampled slice is not proof that no work occurred; Chrome's exact count badges are the reliable presence/absence signal.

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads for this page: the client JS runtime the lane ships (over the bare React floor), the CSS, and the SSR HTML. Lower is better.

JSCSSHTML
next-yak css-prop 9.7.02,661 B(0.0 / 0.1 / 2.5 kB)
next-yak css-prop 9.6.03,029 B(0.4 / 0.1 / 2.5 kB)
vanilla (hand-written ceiling)3,077 B(0.0 / 0.4 / 2.6 kB)
next-yak css-prop 9.7.0 foldStatic: false3,159 B(0.5 / 0.1 / 2.4 kB)
next-yak 9.7.0 (styled API)4,044 B(1.1 / 0.2 / 2.7 kB)
next-yak 9.6.04,049 B(1.1 / 0.2 / 2.7 kB)
next-yak 9.7.0 (styled API) foldStatic: false4,245 B(1.3 / 0.2 / 2.7 kB)
StyleX without CSS layers (:not() specificity hack)5,774 B(2.5 / 0.3 / 2.9 kB)
StyleX5,860 B(2.6 / 0.3 / 2.9 kB)
Goober6,669 B(3.4 / 0.2 / 2.8 kB)
Panda (css fn)7,927 B(3.5 / 0.3 / 3.9 kB)
Panda (style props)11,438 B(7.8 / 0.2 / 3.2 kB)
tailwind-merge13,914 B(10.2 / 0.3 / 3.1 kB)
cnfast14,906 B(11.2 / 0.3 / 3.1 kB)
Emotion15,445 B(12.4 / 0.2 / 2.4 kB)
styled-components16,556 B(12.9 / 0.3 / 2.9 kB)

Scaling — SSR render time (ms) vs instance countiSSR render time as the workload grows from a handful to thousands of instances — shows how each lane's per-element cost compounds. A flatter line scales better.

0.0193856751005001,0002,0004,000
cnfastEmotionGoobernext-yak 9.6.0next-yak 9.7.0 (styled API)next-yak 9.7.0 (styled API) foldStatic: falsenext-yak css-prop 9.6.0next-yak css-prop 9.7.0next-yak css-prop 9.7.0 foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)tailwind-mergevanilla (hand-written ceiling)StyleX

Build time — full client build · lower is betteriWall time for a lane's whole production client build — the vite bundle that ships to the browser (react + react-dom + the styling runtime + every workload's components), the same build measured for page bytes. cold clears that lane's build output, vite's on-disk caches and Panda's generatedstyled-system first, so it includes the cache-miss regen; warm is the same build run again with nothing cleared. Median of 3. This is build-time developer experience, machine-dependent — not user-facing runtime. Opt-in via pnpm gen:samples --measure=buildtime.

vanilla (hand-written ceiling)69 msnone · warm 73 ms
Goober71 msruntime · warm 70 ms
tailwind-merge73 msutility · warm 79 ms
Emotion76 msruntime · warm 75 ms
cnfast77 msutility · warm 75 ms
styled-components78 msruntime · warm 74 ms
next-yak 9.7.0 (styled API) foldStatic: false110 msextracted · warm 111 ms
next-yak 9.6.0111 msextracted · warm 115 ms
next-yak css-prop 9.6.0111 msextracted · warm 110 ms
next-yak 9.7.0 (styled API)112 msextracted · warm 114 ms
next-yak css-prop 9.7.0120 msextracted · warm 111 ms
next-yak css-prop 9.7.0 foldStatic: false121 msextracted · warm 119 ms
StyleX without CSS layers (:not() specificity hack)343 msatomic · warm 363 ms
StyleX363 msatomic · warm 362 ms
Panda (recipe)510 msatomic · warm 476 ms
Panda (style props)536 msatomic · warm 491 ms
Panda (css fn)548 msatomic · warm 490 ms

How this was measured

Source, raw data and methodology: github.com/jantimon/css-in-js-bench. Run it locally: clone the repo, pnpm install, then pnpm report renders this report from the committed samples — pnpm gen re-measures everything on your own machine.