CSS-in-JS benchmarks

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

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

Where a style gets resolved decides nearly everything: the lanes that finish at build time run 2 to 8× the runtime libraries, and the one thing that reliably switches the compiler off is a module boundary between a styled component and the JSX that uses it.

A module boundary switches off next-yak's compiler, unless what you export is styles rather than components

The Example multifile-shop shown below is the Example product-grid re-laid-out across separate modules, byte-identical in DOM and CSS and verified pixel-identical. next-yak falls from 185,000 to 105,000 renders/s and lands beside its own foldStatic: false lane at 103,000, because fold_jsx_usages rewrites a declaration and its use site only when both live in one module. The css prop is untouched, 231,000 against 223,000, since it folds where it is written and inlines an imported fragment at build time. The Example multifile-composition shows which boundary is free: move a component and its JSX together and folding still sees both.

Composition depth is free only where the chain collapses at build time

Across the Examples compose-1, compose-3 and compose-6 shown below, next-yak costs 2.24M, 1.40M and 1.48M renders/s. Six levels of wrapping compile to one component carrying six class names in a single string, so everything after the first wrapper is free. Hand-written vanilla pays once per level, 1.92M to 1.37M to 919,000, and panda pays hardest, 1.12M to 310,000. The Example button-variants shows the ceiling of the same mechanism: three imported style values fold into className="yTV3SxX" and the wrapper component disappears from the output.

The two next-yak APIs are not interchangeable, and they fail in opposite places

The css prop folds across a module boundary and the styled API does not, worth 2.12× on the Example multifile-shop shown below. Under composition the ordering reverses: the styled API concatenates a chain into one component while the css prop merges at every level, so on the Example compose-6 the styled lane runs 1.48M renders/s against 600,000. On the Example button-variants-nested the css prop has no cell at all, because a style fragment cannot arrive as a component prop and the case does not compile for it. Choosing between the two is a bet on which pattern a codebase uses most.

Runtime libraries pay the same cost three times, and hydration is where it shows

On the Example product-grid shown below, styled-components spends 5.66 ms and goober 5.93 ms of server CPU serializing rules per instance, against 0.000 ms for the lanes that resolve at build time. The same work returns on the client, where both need around 240 ms to hydrate the page against 56 ms for the leaders, because the sheet must be rebuilt before React can attach. It arrives a third time on interaction: one wishlist toggle costs emotion 46.2 ms against 8.5 ms.

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

The Example dyn-translate shown below measures the naive pattern, the value baked into the class name, which is what a codebase produces when nobody optimizes: goober renders at 166,000 renders/s and takes 1.27 s to hydrate. The Example dyn-fair gives every lane its own best practice, and for a library without native support that best practice is hand-written, splitting the static class from an inline style per component, for every dynamic value, indefinitely. It works, lifting goober to 945,000 and panda to 1.00M. next-yak ships that separation as a compiler feature and pays for it: the Example dyn-inline puts every lane on a plain inline style, where next-yak renders 1.51M against 724,000 through its CSS custom property.

Payload separates the extracted lanes from everything else by a factor of eight

On the Example tabs shown below, next-yak-css ships 2,707 gz B and cnfast 22.0 kB for the identical page. The split is not the stylesheet, which stays within a few hundred bytes across lanes; it is the JS and the markup. emotion and styled-components each carry about 13 kB of library, while the utility lanes write their class lists into the HTML. The Example product-grid holds the same ordering at 7,475 gz B against 27,718.

One hint per library
next-yak (styled API)Folding stops at a module boundary: imported styled components stay runtime wrappers, 185k → 105k renders/s on multifile-shop.
next-yak (css prop)Folds across modules, but merges once per composition level: 1.91M → 600k renders/s from depth 1 to 6, and again on hydration.
BambooNo styling runtime, but readable class names reach the markup: tabs spends 2.67 of 2.95 ms inside React writing them.
Panda (css fn)css() resolves per render where a call site is not hoisted. Recipes avoid it, 1.30M vs 768k on btn-variant; fold static call sites too.
StyleXNothing on the render path for flat JSX; styleq's per-element merge is what remains, 2.01M → 799k renders/s from depth 1 to 6.
styled-componentsRe-hashes and injects per render: 5.66 ms of product-grid CPU, 240 ms to hydrate. Hash the static rule once, ride values on a variable.
EmotionRe-hashes the serialized string every render, 7.79 ms of product-grid CPU and 46.2 ms per interaction; memoize on string identity.
GooberInjects a class per distinct value, so unique values are a cliff: dyn-translate hydrates in 1.27 s. Key the cache on template identity.
tailwind-mergeThe merge rebuilds its joined-class key every render, six times over at depth 6 (419k renders/s). Cache on argument identity.
cnfastNear-free on repeated class lists, 1.66M renders/s on btn-variant; only high-cardinality values cost, through the cache miss.

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.

The heaviest page in the suite sorts lanes by how much of the styling library survives the build: folding leaves next-yak 0.548 ms of lib self-time per render, while Emotion spends 7.79 ms of its 13.9 ms there.

The Example multifile-shop shown below renders this exact page from separate modules instead of one, with the DOM, the CSS and the 400 products byte-identical and verified pixel-identical. The lanes that resolve styles at render do not move. next-yak falls from 185,000 to 105,000 renders/s, because its fold cannot cross a module boundary.

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)237,618 r/s ±2%(0.31 ms / 1k elems)
next-yak (css prop)230,925 r/s ±3%(0.32 ms / 1k elems)
StyleX212,469 r/s ±4%(0.35 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)212,098 r/s ±2%(0.35 ms / 1k elems)
next-yak (styled API)185,342 r/s ±2%(0.40 ms / 1k elems)
cnfast178,874 r/s ±2%(0.41 ms / 1k elems)
next-yak (css prop) foldStatic: false177,128 r/s ±3%(0.42 ms / 1k elems)
Bamboo146,621 r/s ±3%(0.51 ms / 1k elems)
Panda (css fn)133,176 r/s ±3%(0.56 ms / 1k elems)
tailwind-merge121,151 r/s ±3%(0.61 ms / 1k elems)
next-yak (styled API) foldStatic: false107,302 r/s ±3%(0.69 ms / 1k elems)
styled-components53,662 r/s ±5%(1.38 ms / 1k elems)
Goober49,920 r/s ±2%(1.48 ms / 1k elems)
Emotion30,937 r/s ±3%(2.39 ms / 1k elems)
Panda (style props)30,118 r/s ±3%(2.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 (css prop)253 req/s ±1%
vanilla (hand-written ceiling)238 req/s ±0%
next-yak (styled API)235 req/s ±0%
next-yak (css prop) foldStatic: false215 req/s ±2%
StyleX without CSS layers (:not() specificity hack)192 req/s
StyleX173 req/s ±2%
next-yak (styled API) foldStatic: false149 req/s ±3%
cnfast124 req/s ±2%
Bamboo122 req/s
tailwind-merge110 req/s ±0%
Panda (css fn)101 req/s
Goober95 req/s ±1%
styled-components93 req/s ±2%
Emotion73 req/s
Panda (style props)48 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 1.1.0): 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)2.13 ms
next-yak (css prop)2.16 ms
next-yak (styled API)2.50 ms
StyleX without CSS layers (:not() specificity hack)2.70 ms
cnfast2.77 ms
next-yak (css prop) foldStatic: false3.10 ms
StyleX3.29 ms
Panda (css fn)3.38 ms
Bamboo3.47 ms
next-yak (styled API) foldStatic: false4.01 ms
tailwind-merge6.10 ms
styled-components8.40 ms
Goober8.52 ms
Emotion13.90 ms
Panda (style props)18.63 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 1.1.0). Lower is better.

Panda (css fn)55 ms ±1%
Bamboo56 ms ±2%
vanilla (hand-written ceiling)56 ms ±2%
next-yak (css prop)56 ms ±2%
StyleX59 ms ±1%
next-yak (styled API)60 ms ±2%
StyleX without CSS layers (:not() specificity hack)60 ms ±2%
next-yak (css prop) foldStatic: false62 ms ±2%
cnfast66 ms ±2%
tailwind-merge66 ms ±1%
next-yak (styled API) foldStatic: false70 ms ±1%
Panda (style props)110 ms ±2%
Emotion128 ms ±1%
styled-components240 ms ±2%
Goober248 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (css prop)7.59 ms active(31.01 ms span)
StyleX without CSS layers (:not() specificity hack)8.01 ms active(28.43 ms span)
Bamboo8.09 ms active(28.55 ms span)
vanilla (hand-written ceiling)8.11 ms active(28.64 ms span)
Panda (css fn)8.47 ms active(26.56 ms span)
next-yak (styled API)8.48 ms active(34.68 ms span)
StyleX8.72 ms active(29.49 ms span)
next-yak (styled API) foldStatic: false9.81 ms active(32.10 ms span)
next-yak (css prop) foldStatic: false9.95 ms active(30.74 ms span)
cnfast11.95 ms active(33.45 ms span)
tailwind-merge13.39 ms active(33.45 ms span)
Panda (style props)14.73 ms active(31.67 ms span)
Emotion16.69 ms active(34.66 ms span)
Goober26.98 ms active(53.37 ms span)
styled-components27.22 ms active(42.73 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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.

cnfast9 ms ±16%
next-yak (css prop)9 ms ±11%
vanilla (hand-written ceiling)9 ms ±11%
StyleX without CSS layers (:not() specificity hack)9 ms ±11%
StyleX9 ms ±13%
tailwind-merge9 ms ±11%
Bamboo10 ms ±6%
Panda (css fn)10 ms ±11%
next-yak (styled API)10 ms ±8%
next-yak (css prop) foldStatic: false11 ms ±13%
next-yak (styled API) foldStatic: false18 ms ±15%
styled-components27 ms ±3%
Goober31 ms ±4%
Panda (style props)41 ms ±1%
Emotion46 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Panda (css fn)0.73 ms active(33.39 ms span)
next-yak (styled API)0.77 ms active(33.09 ms span)
vanilla (hand-written ceiling)0.84 ms active(33.08 ms span)
StyleX without CSS layers (:not() specificity hack)0.85 ms active(32.97 ms span)
cnfast0.89 ms active(30.53 ms span)
next-yak (css prop)0.90 ms active(32.88 ms span)
Bamboo0.90 ms active(33.98 ms span)
tailwind-merge0.94 ms active(34.69 ms span)
next-yak (css prop) foldStatic: false1.01 ms active(32.96 ms span)
StyleX1.42 ms active(31.82 ms span)
Goober1.69 ms active(33.40 ms span)
next-yak (styled API) foldStatic: false1.71 ms active(33.26 ms span)
Panda (style props)2.16 ms active(33.23 ms span)
styled-components2.57 ms active(32.85 ms span)
Emotion2.76 ms active(33.06 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)125 ms ±1%
next-yak (css prop)126 ms ±2%
Bamboo128 ms ±1%
Panda (css fn)130 ms ±1%
next-yak (styled API)131 ms ±2%
StyleX131 ms ±2%
StyleX without CSS layers (:not() specificity hack)132 ms ±2%
next-yak (css prop) foldStatic: false134 ms ±2%
cnfast140 ms ±2%
tailwind-merge141 ms ±2%
next-yak (styled API) foldStatic: false144 ms ±2%
Panda (style props)185 ms ±2%
styled-components201 ms ±2%
Goober209 ms ±1%
Emotion233 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (css prop)18.36 ms active(27.07 ms span)
StyleX19.02 ms active(28.41 ms span)
vanilla (hand-written ceiling)19.85 ms active(28.44 ms span)
Bamboo20.14 ms active(31.05 ms span)
next-yak (styled API)20.14 ms active(28.39 ms span)
next-yak (css prop) foldStatic: false20.61 ms active(26.16 ms span)
Panda (css fn)21.97 ms active(30.13 ms span)
tailwind-merge23.20 ms active(40.47 ms span)
next-yak (styled API) foldStatic: false23.48 ms active(30.24 ms span)
StyleX without CSS layers (:not() specificity hack)24.16 ms active(30.97 ms span)
cnfast24.55 ms active(44.18 ms span)
Goober26.01 ms active(32.16 ms span)
styled-components28.13 ms active(41.53 ms span)
Emotion30.03 ms active(54.83 ms span)
Panda (style props)45.48 ms active(52.53 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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 (styled API)Chrome9.6 ms1 recalc
Firefox9.0 ms
next-yak (css prop) foldStatic: falseChrome9.8 ms1 recalc
Firefox10.9 ms
StyleXChrome9.3 ms1 recalc
Firefox9.9 ms
vanilla (hand-written ceiling)Chrome10.5 ms1 recalc
Firefox11.1 ms
Panda (style props)Chrome29.4 ms1 recalc
Firefox11.4 ms
tailwind-mergeChrome9.2 ms1 recalc
Firefox9.8 ms
BambooChrome10.5 ms1 recalc
Firefox10.9 ms
cnfastChrome9.3 ms1 recalc
Firefox9.9 ms
StyleX without CSS layers (:not() specificity hack)Chrome14.0 ms1 recalc
Firefox9.0 ms
next-yak (css prop)Chrome9.0 ms1 recalc
Firefox10.0 ms
Panda (css fn)Chrome10.7 ms1 recalc
Firefox11.2 ms
GooberChrome10.7 ms1 recalc
Firefox14.1 ms
next-yak (styled API) foldStatic: falseChrome12.0 ms1 recalc
Firefox39.3 ms
styled-componentsChrome11.1 ms1 recalc
Firefox13.2 ms
EmotionChrome11.3 ms1 recalc
Firefox16.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) foldStatic: false7,459 B(0.3 / 0.9 / 6.1 kB)
next-yak (css prop)7,475 B(0.3 / 0.9 / 6.1 kB)
vanilla (hand-written ceiling)7,790 B(0.0 / 1.5 / 6.1 kB)
next-yak (styled API)8,291 B(1.1 / 0.9 / 6.1 kB)
next-yak (styled API) foldStatic: false8,359 B(1.2 / 0.9 / 6.1 kB)
Goober11,353 B(3.7 / 1.2 / 6.2 kB)
StyleX13,880 B(3.4 / 1.4 / 8.8 kB)
StyleX without CSS layers (:not() specificity hack)13,989 B(3.4 / 1.5 / 8.8 kB)
Bamboo17,267 B(1.9 / 1.5 / 13.5 kB)
Emotion20,495 B(12.7 / 1.2 / 6.1 kB)
styled-components21,345 B(13.1 / 1.4 / 6.3 kB)
Panda (css fn)22,365 B(4.8 / 1.5 / 15.6 kB)
Panda (style props)26,493 B(9.1 / 1.5 / 15.3 kB)
tailwind-merge26,755 B(10.4 / 1.7 / 14.0 kB)
cnfast27,718 B(11.3 / 1.7 / 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.039771161541005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
A whole shop pagethe same tiles, split across filesn = 400low cardinality

The EXACT product-grid workload — identical DOM, identical CSS, identical 400 tiles — laid out the way real code ships: the shared fragments in one module, the primitives split by role across several more, and every use site in index.tsx. A compiler can only replace a styled component with a plain tag when the declaration and the use site sit in the same module, so here it never can, and the shared fragments have to cross a module boundary to reach the components that use them. Runtime libraries do not care about file layout. The gap to product-grid is what the boundary costs.

Splitting the same page across modules costs next-yak's styled API its entire build-time fold and lands it on the lane that never folds, while the css prop resolves imported fragments at build time and holds its position.

This is the Example product-grid shown above with nothing changed but the file layout: same DOM, same CSS, same 400 products, verified pixel-identical, which is what makes the module boundary the only variable. Read it against the Example multifile-composition, which moves the styled primitives and their JSX use sites together into one imported module and therefore keeps folding. Moving a whole component across a file is free; shipping primitives for another module to use is not.

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)227,731 r/s ±4%(0.33 ms / 1k elems)
next-yak (css prop)223,204 r/s ±6%(0.33 ms / 1k elems)
cnfast175,734 r/s ±4%(0.42 ms / 1k elems)
next-yak (css prop) foldStatic: false175,227 r/s ±5%(0.42 ms / 1k elems)
StyleX158,270 r/s ±3%(0.47 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)156,991 r/s ±3%(0.47 ms / 1k elems)
Bamboo144,993 r/s ±3%(0.51 ms / 1k elems)
Panda (css fn)132,447 r/s ±2%(0.56 ms / 1k elems)
tailwind-merge121,240 r/s ±3%(0.61 ms / 1k elems)
next-yak (styled API)105,161 r/s ±3%(0.70 ms / 1k elems)
next-yak (styled API) foldStatic: false103,449 r/s ±4%(0.72 ms / 1k elems)
styled-components52,547 r/s ±3%(1.41 ms / 1k elems)
Goober45,773 r/s ±5%(1.62 ms / 1k elems)
Emotion31,949 r/s ±3%(2.32 ms / 1k elems)
Panda (style props)29,403 r/s ±4%(2.52 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)261 req/s
vanilla (hand-written ceiling)227 req/s ±2%
next-yak (css prop) foldStatic: false206 req/s ±3%
next-yak (styled API)165 req/s ±0%
StyleX without CSS layers (:not() specificity hack)164 req/s ±0%
StyleX163 req/s ±3%
next-yak (styled API) foldStatic: false148 req/s ±1%
cnfast123 req/s ±0%
Bamboo121 req/s ±0%
tailwind-merge104 req/s ±0%
Panda (css fn)92 req/s ±2%
Goober89 req/s ±1%
styled-components88 req/s ±1%
Emotion74 req/s ±1%
Panda (style props)50 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 1.1.0): 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)2.12 ms
vanilla (hand-written ceiling)2.16 ms
cnfast2.71 ms
next-yak (css prop) foldStatic: false2.93 ms
Panda (css fn)3.46 ms
Bamboo3.48 ms
StyleX without CSS layers (:not() specificity hack)3.62 ms
StyleX3.79 ms
next-yak (styled API) foldStatic: false3.92 ms
next-yak (styled API)4.09 ms
tailwind-merge6.12 ms
styled-components8.67 ms
Goober9.82 ms
Emotion13.68 ms
Panda (style props)14.22 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 1.1.0). Lower is better.

Panda (css fn)55 ms ±3%
next-yak (css prop)56 ms ±1%
Bamboo56 ms ±2%
vanilla (hand-written ceiling)56 ms ±2%
next-yak (css prop) foldStatic: false62 ms ±1%
StyleX63 ms ±1%
StyleX without CSS layers (:not() specificity hack)64 ms ±1%
cnfast67 ms ±1%
tailwind-merge67 ms ±1%
next-yak (styled API)70 ms ±2%
next-yak (styled API) foldStatic: false70 ms ±1%
Panda (style props)109 ms ±2%
Emotion128 ms ±1%
styled-components236 ms ±1%
Goober249 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)7.79 ms active(32.16 ms span)
next-yak (css prop)8.06 ms active(26.37 ms span)
Bamboo8.33 ms active(30.25 ms span)
Panda (css fn)8.69 ms active(34.52 ms span)
next-yak (css prop) foldStatic: false9.00 ms active(28.42 ms span)
StyleX without CSS layers (:not() specificity hack)9.17 ms active(35.37 ms span)
StyleX9.38 ms active(34.43 ms span)
next-yak (styled API) foldStatic: false9.94 ms active(30.47 ms span)
next-yak (styled API)10.14 ms active(34.03 ms span)
tailwind-merge11.92 ms active(35.93 ms span)
cnfast12.45 ms active(32.46 ms span)
Panda (style props)13.61 ms active(36.84 ms span)
Emotion16.13 ms active(32.74 ms span)
Goober27.64 ms active(54.01 ms span)
styled-components29.88 ms active(53.02 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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.

vanilla (hand-written ceiling)8 ms ±17%
Bamboo9 ms ±14%
cnfast9 ms ±8%
next-yak (css prop)9 ms ±7%
tailwind-merge10 ms ±21%
Panda (css fn)10 ms ±6%
next-yak (css prop) foldStatic: false11 ms ±29%
StyleX without CSS layers (:not() specificity hack)13 ms ±24%
StyleX14 ms ±17%
next-yak (styled API) foldStatic: false22 ms ±15%
next-yak (styled API)22 ms ±14%
styled-components28 ms ±7%
Goober36 ms ±3%
Panda (style props)41 ms ±3%
Emotion47 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)0.74 ms active(33.12 ms span)
next-yak (css prop)0.79 ms active(32.71 ms span)
cnfast0.84 ms active(33.57 ms span)
tailwind-merge0.87 ms active(35.16 ms span)
Bamboo0.90 ms active(33.22 ms span)
StyleX without CSS layers (:not() specificity hack)0.94 ms active(32.08 ms span)
Panda (css fn)0.98 ms active(33.63 ms span)
next-yak (css prop) foldStatic: false1.00 ms active(34.50 ms span)
next-yak (styled API)1.22 ms active(32.45 ms span)
next-yak (styled API) foldStatic: false1.35 ms active(33.58 ms span)
StyleX1.38 ms active(32.77 ms span)
Goober1.89 ms active(33.03 ms span)
Panda (style props)2.14 ms active(31.84 ms span)
Emotion2.30 ms active(33.23 ms span)
styled-components4.63 ms active(34.02 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)125 ms ±3%
next-yak (css prop)125 ms ±1%
Bamboo128 ms ±1%
Panda (css fn)129 ms ±2%
next-yak (css prop) foldStatic: false133 ms ±1%
StyleX138 ms ±3%
StyleX without CSS layers (:not() specificity hack)138 ms ±1%
tailwind-merge139 ms ±1%
cnfast142 ms ±1%
next-yak (styled API) foldStatic: false142 ms ±1%
next-yak (styled API)142 ms ±2%
Panda (style props)182 ms ±2%
styled-components201 ms ±2%
Goober209 ms ±1%
Emotion232 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)18.95 ms active(29.47 ms span)
next-yak (css prop) foldStatic: false19.56 ms active(29.32 ms span)
StyleX19.99 ms active(33.59 ms span)
StyleX without CSS layers (:not() specificity hack)20.26 ms active(29.83 ms span)
next-yak (styled API) foldStatic: false20.36 ms active(27.42 ms span)
next-yak (styled API)20.39 ms active(29.15 ms span)
next-yak (css prop)21.13 ms active(29.22 ms span)
tailwind-merge22.38 ms active(33.40 ms span)
Bamboo24.10 ms active(32.30 ms span)
Panda (style props)25.72 ms active(36.58 ms span)
Panda (css fn)27.70 ms active(33.61 ms span)
cnfast29.81 ms active(36.63 ms span)
Emotion30.20 ms active(34.54 ms span)
styled-components48.12 ms active(49.46 ms span)
Goober50.12 ms active(52.58 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)
cnfastChrome15.1 ms1 recalc
Firefox10.1 ms
next-yak (css prop) foldStatic: falseChrome9.2 ms1 recalc
Firefox10.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome9.3 ms1 recalc
Firefox9.4 ms
next-yak (styled API)Chrome8.9 ms1 recalc
Firefox11.2 ms
next-yak (styled API) foldStatic: falseChrome9.1 ms1 recalc
Firefox12.9 ms
BambooChrome14.4 ms1 recalc
Firefox9.8 ms
Panda (style props)Chrome9.3 ms1 recalc
Firefox10.0 ms
StyleXChrome9.0 ms1 recalc
Firefox10.0 ms
vanilla (hand-written ceiling)Chrome9.6 ms1 recalc
Firefox12.0 ms
tailwind-mergeChrome8.8 ms1 recalc
Firefox10.5 ms
next-yak (css prop)Chrome11.3 ms1 recalc
Firefox11.1 ms
styled-componentsChrome32.2 ms1 recalc
Firefox14.1 ms
Panda (css fn)Chrome16.2 ms1 recalc
Firefox11.3 ms
EmotionChrome10.6 ms1 recalc
Firefox51.4 ms
GooberChrome35.8 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) foldStatic: false7,460 B(0.3 / 0.9 / 6.1 kB)
next-yak (css prop)7,479 B(0.3 / 0.9 / 6.1 kB)
vanilla (hand-written ceiling)7,790 B(0.0 / 1.5 / 6.1 kB)
next-yak (styled API)8,425 B(1.1 / 0.9 / 6.2 kB)
next-yak (styled API) foldStatic: false8,480 B(1.2 / 0.9 / 6.2 kB)
Goober11,204 B(3.7 / 1.1 / 6.1 kB)
StyleX without CSS layers (:not() specificity hack)13,877 B(3.4 / 1.5 / 8.7 kB)
StyleX13,937 B(3.4 / 1.4 / 8.8 kB)
Bamboo17,265 B(1.9 / 1.5 / 13.5 kB)
Emotion20,489 B(12.7 / 1.2 / 6.1 kB)
styled-components21,385 B(13.1 / 1.4 / 6.4 kB)
Panda (css fn)22,365 B(4.8 / 1.5 / 15.6 kB)
Panda (style props)26,491 B(9.1 / 1.5 / 15.3 kB)
tailwind-merge26,755 B(10.4 / 1.7 / 14.0 kB)
cnfast27,718 B(11.3 / 1.7 / 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.039771161541005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
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.

A real component with a full pseudo-state matrix and a 992px flip rewards resolving that matrix during the build: four compile-time lanes finish above hand-written vanilla, and next-yak's own no-fold lane costs 1.97×.

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)899,315 r/s ±2%(0.44 ms / 1k elems)
StyleX894,988 r/s ±4%(0.45 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)875,241 r/s ±3%(0.46 ms / 1k elems)
next-yak (styled API)850,461 r/s ±3%(0.47 ms / 1k elems)
vanilla (hand-written ceiling)789,785 r/s ±3%(0.51 ms / 1k elems)
next-yak (css prop) foldStatic: false735,542 r/s ±4%(0.54 ms / 1k elems)
cnfast608,041 r/s ±5%(0.66 ms / 1k elems)
next-yak (styled API) foldStatic: false431,655 r/s ±2%(0.93 ms / 1k elems)
Bamboo357,079 r/s ±6%(1.12 ms / 1k elems)
tailwind-merge337,297 r/s ±2%(1.19 ms / 1k elems)
Panda (css fn)321,841 r/s ±3%(1.24 ms / 1k elems)
styled-components199,727 r/s ±3%(2.00 ms / 1k elems)
Goober196,786 r/s ±1%(2.03 ms / 1k elems)
Emotion144,500 r/s ±2%(2.77 ms / 1k elems)
Panda (style props)110,172 r/s ±3%(3.63 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 (styled API)629 req/s ±0%
next-yak (css prop)616 req/s ±0%
vanilla (hand-written ceiling)547 req/s ±1%
next-yak (css prop) foldStatic: false482 req/s ±2%
StyleX without CSS layers (:not() specificity hack)434 req/s ±1%
StyleX382 req/s ±3%
next-yak (styled API) foldStatic: false308 req/s ±1%
cnfast293 req/s ±0%
Bamboo207 req/s
tailwind-merge198 req/s ±3%
styled-components181 req/s ±1%
Goober170 req/s ±0%
Panda (css fn)154 req/s ±1%
Emotion136 req/s ±0%
Panda (style props)76 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 1.1.0): 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)1.38 ms
StyleX1.42 ms
vanilla (hand-written ceiling)1.56 ms
StyleX without CSS layers (:not() specificity hack)1.57 ms
next-yak (css prop) foldStatic: false1.79 ms
next-yak (styled API)1.80 ms
cnfast1.95 ms
next-yak (styled API) foldStatic: false2.57 ms
Bamboo3.12 ms
Panda (css fn)3.83 ms
tailwind-merge4.93 ms
styled-components5.45 ms
Goober5.79 ms
Emotion7.38 ms
Panda (style props)10.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 1.1.0). Lower is better.

Bamboo57 ms ±1%
next-yak (styled API)57 ms ±2%
vanilla (hand-written ceiling)57 ms ±1%
next-yak (css prop)58 ms ±2%
Panda (css fn)58 ms ±2%
StyleX58 ms ±1%
StyleX without CSS layers (:not() specificity hack)59 ms ±1%
next-yak (css prop) foldStatic: false62 ms ±1%
next-yak (styled API) foldStatic: false66 ms ±1%
tailwind-merge71 ms ±1%
cnfast72 ms ±1%
Panda (style props)95 ms ±2%
Emotion115 ms ±1%
styled-components158 ms ±1%
Goober158 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)5.99 ms active(27.38 ms span)
next-yak (styled API)6.08 ms active(32.16 ms span)
next-yak (css prop) foldStatic: false6.26 ms active(33.15 ms span)
Bamboo6.29 ms active(34.75 ms span)
next-yak (css prop)6.37 ms active(31.95 ms span)
StyleX6.40 ms active(27.97 ms span)
Panda (css fn)6.43 ms active(32.08 ms span)
StyleX without CSS layers (:not() specificity hack)6.96 ms active(29.68 ms span)
next-yak (styled API) foldStatic: false7.09 ms active(29.15 ms span)
Panda (style props)9.72 ms active(27.96 ms span)
Goober10.58 ms active(26.45 ms span)
cnfast11.01 ms active(33.33 ms span)
Emotion11.20 ms active(29.17 ms span)
tailwind-merge11.83 ms active(30.98 ms span)
styled-components12.42 ms active(32.76 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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 (styled API)7 ms ±16%
tailwind-merge7 ms ±7%
next-yak (css prop)7 ms ±17%
Bamboo7 ms ±6%
StyleX without CSS layers (:not() specificity hack)7 ms ±7%
StyleX7 ms ±4%
Panda (css fn)7 ms ±4%
vanilla (hand-written ceiling)8 ms ±10%
cnfast8 ms ±7%
next-yak (css prop) foldStatic: false8 ms ±8%
next-yak (styled API) foldStatic: false11 ms ±9%
styled-components15 ms ±12%
Goober17 ms ±13%
Panda (style props)26 ms ±5%
Emotion29 ms ±7%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (css prop)0.55 ms active(31.79 ms span)
StyleX without CSS layers (:not() specificity hack)0.66 ms active(34.30 ms span)
next-yak (css prop) foldStatic: false0.69 ms active(32.80 ms span)
cnfast0.80 ms active(32.48 ms span)
next-yak (styled API)0.81 ms active(33.38 ms span)
StyleX0.82 ms active(33.33 ms span)
vanilla (hand-written ceiling)0.86 ms active(32.13 ms span)
Bamboo0.97 ms active(33.06 ms span)
Goober1.01 ms active(32.14 ms span)
Panda (css fn)1.02 ms active(33.37 ms span)
tailwind-merge1.06 ms active(32.32 ms span)
styled-components1.11 ms active(32.26 ms span)
next-yak (styled API) foldStatic: false1.16 ms active(32.18 ms span)
Emotion1.22 ms active(32.76 ms span)
Panda (style props)1.35 ms active(32.72 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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 (styled API)107 ms ±1%
next-yak (css prop)107 ms ±2%
vanilla (hand-written ceiling)108 ms ±2%
StyleX without CSS layers (:not() specificity hack)108 ms ±2%
Bamboo109 ms ±2%
StyleX109 ms ±3%
Panda (css fn)109 ms ±2%
next-yak (css prop) foldStatic: false110 ms ±2%
next-yak (styled API) foldStatic: false117 ms ±3%
cnfast120 ms ±2%
tailwind-merge123 ms ±2%
Panda (style props)146 ms ±2%
Goober153 ms ±1%
styled-components156 ms ±1%
Emotion177 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (styled API)13.83 ms active(30.32 ms span)
next-yak (css prop)13.86 ms active(34.45 ms span)
StyleX13.91 ms active(39.85 ms span)
Bamboo14.45 ms active(28.82 ms span)
next-yak (styled API) foldStatic: false14.74 ms active(31.98 ms span)
Panda (css fn)15.47 ms active(26.29 ms span)
next-yak (css prop) foldStatic: false15.84 ms active(29.98 ms span)
StyleX without CSS layers (:not() specificity hack)16.02 ms active(27.99 ms span)
Goober16.13 ms active(32.86 ms span)
vanilla (hand-written ceiling)16.40 ms active(41.14 ms span)
Panda (style props)18.18 ms active(32.25 ms span)
tailwind-merge18.75 ms active(35.91 ms span)
cnfast18.83 ms active(49.56 ms span)
styled-components19.54 ms active(31.98 ms span)
Emotion19.71 ms active(33.21 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)
cnfastChrome6.7 ms1 recalc
Firefox6.8 ms
EmotionChrome7.6 ms1 recalc
Firefox9.1 ms
next-yak (styled API)Chrome6.6 ms1 recalc
Firefox6.0 ms
next-yak (css prop)Chrome7.0 ms1 recalc
Firefox6.0 ms
Panda (css fn)Chrome8.1 ms1 recalc
Firefox7.0 ms
Panda (style props)Chrome7.7 ms1 recalc
Firefox8.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome9.2 ms1 recalc
Firefox6.0 ms
StyleXChrome7.1 ms1 recalc
Firefox6.8 ms
tailwind-mergeChrome6.8 ms1 recalc
Firefox6.9 ms
next-yak (styled API) foldStatic: falseChrome7.1 ms1 recalc
Firefox8.2 ms
next-yak (css prop) foldStatic: falseChrome7.7 ms1 recalc
Firefox7.8 ms
vanilla (hand-written ceiling)Chrome8.8 ms1 recalc
Firefox7.0 ms
styled-componentsChrome8.0 ms1 recalc
Firefox7.7 ms
GooberChrome6.6 ms1 recalc
Firefox8.0 ms
BambooChrome6.9 ms1 recalc
Firefox8.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) foldStatic: false4,290 B(0.3 / 0.5 / 3.4 kB)
next-yak (css prop)4,303 B(0.3 / 0.5 / 3.4 kB)
vanilla (hand-written ceiling)4,444 B(0.0 / 0.9 / 3.5 kB)
next-yak (styled API)4,969 B(1.1 / 0.5 / 3.2 kB)
next-yak (styled API) foldStatic: false5,033 B(1.2 / 0.5 / 3.2 kB)
Goober7,840 B(3.7 / 0.6 / 3.4 kB)
StyleX9,947 B(3.4 / 0.9 / 5.4 kB)
StyleX without CSS layers (:not() specificity hack)10,004 B(3.4 / 1.0 / 5.4 kB)
Bamboo12,071 B(1.9 / 1.0 / 8.9 kB)
Emotion16,980 B(12.7 / 0.6 / 3.2 kB)
Panda (css fn)17,407 B(4.8 / 1.0 / 11.3 kB)
styled-components17,440 B(13.1 / 0.6 / 3.3 kB)
tailwind-merge21,381 B(10.4 / 1.0 / 9.5 kB)
Panda (style props)21,779 B(9.1 / 1.0 / 11.3 kB)
cnfast22,344 B(11.3 / 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.09.61929381005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
Button variantscomposed as style valuesn = 1,000low cardinality

A base button, a ghost override and a ghost-primary override, each exported from its own module as a style value and merged onto ONE element by the page. Every level overrides the last (background, border-colour, colour), so this measures conflict resolution, not concatenation: tailwind-merge re-parses the whole list, StyleX and the atomic compilers resolve it at build time, and the styled lanes serialise it once per class. Read against button-variants-nested, which ships the identical ladder as a JSX component API.

Three imported style values merged onto one element fold into a single class: next-yak emits className="yTV3SxX" and drops the wrapper component entirely, running 1.55× its own no-fold lane.

The Example button-variants-nested shown below is this exact ladder with one change: each module exports a JSX component instead of a style value. Same declarations, same three modules, same rendered element. Values fold across the module boundary and components do not, which drops next-yak from 2.23M to 1.28M renders/s and costs even hand-written vanilla 24%.

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 (styled API)2,227,792 r/s ±4%(0.45 ms / 1k elems)
next-yak (css prop)1,928,019 r/s ±4%(0.52 ms / 1k elems)
cnfast1,812,005 r/s ±2%(0.55 ms / 1k elems)
vanilla (hand-written ceiling)1,776,988 r/s ±2%(0.56 ms / 1k elems)
next-yak (css prop) foldStatic: false1,659,063 r/s ±4%(0.60 ms / 1k elems)
Bamboo1,643,950 r/s ±4%(0.61 ms / 1k elems)
next-yak (styled API) foldStatic: false1,436,092 r/s ±3%(0.70 ms / 1k elems)
StyleX1,325,090 r/s ±2%(0.75 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,265,890 r/s ±3%(0.79 ms / 1k elems)
Panda (css fn)1,243,008 r/s ±2%(0.80 ms / 1k elems)
tailwind-merge984,494 r/s ±3%(1.02 ms / 1k elems)
Panda (style props)964,204 r/s ±2%(1.04 ms / 1k elems)
Goober524,842 r/s ±1%(1.91 ms / 1k elems)
styled-components501,662 r/s ±1%(1.99 ms / 1k elems)
Emotion311,640 r/s ±2%(3.21 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 (styled API)1,825 req/s ±0%
next-yak (css prop)1,636 req/s ±0%
next-yak (css prop) foldStatic: false1,381 req/s ±0%
vanilla (hand-written ceiling)1,280 req/s ±1%
next-yak (styled API) foldStatic: false1,191 req/s ±3%
cnfast1,044 req/s ±0%
Bamboo894 req/s ±0%
StyleX without CSS layers (:not() specificity hack)858 req/s ±0%
StyleX856 req/s ±1%
tailwind-merge645 req/s ±1%
Panda (style props)606 req/s ±2%
Panda (css fn)581 req/s ±2%
Goober491 req/s ±0%
styled-components449 req/s ±0%
Emotion321 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 1.1.0): 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)0.66 ms
cnfast0.68 ms
Bamboo0.70 ms
next-yak (css prop) foldStatic: false0.71 ms
vanilla (hand-written ceiling)0.75 ms
next-yak (styled API)0.83 ms
StyleX0.90 ms
Panda (css fn)0.94 ms
next-yak (styled API) foldStatic: false0.95 ms
StyleX without CSS layers (:not() specificity hack)0.97 ms
Panda (style props)1.04 ms
tailwind-merge1.16 ms
Goober1.76 ms
Emotion3.24 ms
styled-components3.62 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 1.1.0). Lower is better.

next-yak (styled API)43 ms ±52%
Bamboo44 ms ±4%
next-yak (css prop)45 ms ±1%
Panda (css fn)46 ms ±3%
next-yak (styled API) foldStatic: false46 ms ±47%
StyleX without CSS layers (:not() specificity hack)47 ms ±2%
StyleX48 ms ±4%
next-yak (css prop) foldStatic: false51 ms ±44%
Panda (style props)54 ms ±1%
cnfast55 ms ±2%
tailwind-merge57 ms ±2%
Emotion79 ms ±27%
vanilla (hand-written ceiling)84 ms ±27%
Goober101 ms ±22%
styled-components136 ms ±17%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (styled API)5.57 ms active(31.37 ms span)
next-yak (styled API) foldStatic: false5.64 ms active(31.37 ms span)
next-yak (css prop)5.78 ms active(27.80 ms span)
StyleX5.97 ms active(33.32 ms span)
next-yak (css prop) foldStatic: false5.98 ms active(33.69 ms span)
vanilla (hand-written ceiling)6.01 ms active(33.31 ms span)
StyleX without CSS layers (:not() specificity hack)6.06 ms active(34.11 ms span)
Bamboo6.18 ms active(29.69 ms span)
Panda (css fn)6.59 ms active(36.00 ms span)
Panda (style props)7.31 ms active(34.36 ms span)
Goober8.96 ms active(29.15 ms span)
tailwind-merge9.56 ms active(28.14 ms span)
cnfast9.70 ms active(33.57 ms span)
Emotion10.88 ms active(32.50 ms span)
styled-components12.02 ms active(33.18 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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.

Bamboo6 ms ±10%
next-yak (css prop) foldStatic: false7 ms ±11%
Panda (style props)7 ms ±18%
Panda (css fn)7 ms ±15%
StyleX without CSS layers (:not() specificity hack)7 ms ±12%
Goober7 ms ±17%
styled-components8 ms ±7%
tailwind-merge8 ms ±5%
next-yak (css prop)8 ms ±6%
next-yak (styled API) foldStatic: false8 ms ±8%
StyleX8 ms ±11%
vanilla (hand-written ceiling)8 ms ±9%
cnfast8 ms ±8%
next-yak (styled API)8 ms ±18%
Emotion13 ms ±19%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (styled API)0.51 ms active(31.37 ms span)
next-yak (css prop) foldStatic: false0.60 ms active(32.10 ms span)
Bamboo0.61 ms active(33.26 ms span)
next-yak (styled API) foldStatic: false0.64 ms active(33.79 ms span)
StyleX0.65 ms active(34.09 ms span)
vanilla (hand-written ceiling)0.65 ms active(33.93 ms span)
next-yak (css prop)0.66 ms active(33.27 ms span)
styled-components0.74 ms active(34.31 ms span)
Panda (css fn)0.75 ms active(32.59 ms span)
Goober0.77 ms active(33.53 ms span)
tailwind-merge0.88 ms active(33.92 ms span)
Panda (style props)0.90 ms active(32.01 ms span)
cnfast0.90 ms active(32.98 ms span)
Emotion0.92 ms active(32.65 ms span)
StyleX without CSS layers (:not() specificity hack)1.13 ms active(31.83 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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 (styled API)25 ms ±85%
next-yak (css prop)67 ms ±33%
vanilla (hand-written ceiling)70 ms ±2%
Bamboo70 ms ±2%
Panda (css fn)70 ms ±2%
next-yak (css prop) foldStatic: false71 ms ±2%
next-yak (styled API) foldStatic: false73 ms ±1%
StyleX75 ms ±4%
StyleX without CSS layers (:not() specificity hack)75 ms ±2%
Panda (style props)82 ms ±1%
tailwind-merge82 ms ±4%
cnfast82 ms ±2%
Goober86 ms ±2%
styled-components88 ms ±2%
Emotion106 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (css prop)12.19 ms active(32.05 ms span)
next-yak (styled API)12.42 ms active(37.65 ms span)
next-yak (styled API) foldStatic: false12.54 ms active(37.40 ms span)
next-yak (css prop) foldStatic: false12.61 ms active(31.61 ms span)
Bamboo12.90 ms active(32.68 ms span)
vanilla (hand-written ceiling)12.91 ms active(31.26 ms span)
Panda (css fn)12.98 ms active(32.70 ms span)
StyleX13.17 ms active(32.25 ms span)
Panda (style props)13.97 ms active(33.90 ms span)
Goober14.12 ms active(30.31 ms span)
styled-components17.29 ms active(40.42 ms span)
Emotion17.29 ms active(33.52 ms span)
cnfast17.50 ms active(25.73 ms span)
tailwind-merge18.32 ms active(41.82 ms span)
StyleX without CSS layers (:not() specificity hack)19.63 ms active(30.91 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)
cnfastChrome6.6 ms1 recalc
Firefox7.0 ms
EmotionChrome7.3 ms1 recalc
Firefox8.1 ms
GooberChrome7.1 ms1 recalc
Firefox8.0 ms
next-yak (styled API)Chrome6.6 ms1 recalc
Firefox8.0 ms
next-yak (css prop) foldStatic: falseChrome6.0 ms1 recalc
Firefox9.3 ms
next-yak (styled API) foldStatic: falseChrome6.0 ms1 recalc
Firefox8.0 ms
Panda (css fn)Chrome6.5 ms1 recalc
Firefox8.0 ms
Panda (style props)Chrome6.3 ms1 recalc
Firefox8.1 ms
styled-componentsChrome8.6 ms1 recalc
Firefox7.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome12.8 ms1 recalc
Firefox8.0 ms
StyleXChrome6.7 ms1 recalc
Firefox7.0 ms
tailwind-mergeChrome8.0 ms1 recalc
Firefox30.0 ms
vanilla (hand-written ceiling)Chrome6.9 ms1 recalc
Firefox8.0 ms
next-yak (css prop)Chrome6.0 ms1 recalc
Firefox7.7 ms
BambooChrome6.6 ms1 recalc
Firefox9.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) foldStatic: false3,056 B(0.3 / 0.2 / 2.5 kB)
next-yak (css prop)3,078 B(0.3 / 0.2 / 2.5 kB)
vanilla (hand-written ceiling)3,164 B(0.0 / 0.5 / 2.6 kB)
next-yak (styled API)3,889 B(1.1 / 0.2 / 2.5 kB)
next-yak (styled API) foldStatic: false3,953 B(1.2 / 0.2 / 2.5 kB)
Bamboo5,578 B(1.9 / 0.4 / 3.2 kB)
Goober6,608 B(3.7 / 0.2 / 2.6 kB)
StyleX without CSS layers (:not() specificity hack)6,888 B(3.4 / 0.3 / 3.0 kB)
StyleX6,888 B(3.4 / 0.3 / 3.0 kB)
Panda (css fn)9,035 B(4.8 / 0.3 / 3.7 kB)
Panda (style props)13,055 B(9.1 / 0.3 / 3.4 kB)
tailwind-merge14,245 B(10.4 / 0.3 / 3.2 kB)
cnfast15,208 B(11.3 / 0.3 / 3.2 kB)
Emotion15,815 B(12.7 / 0.3 / 2.4 kB)
styled-components16,352 B(13.1 / 0.3 / 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.03.16.29.3121005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
Button variantscomposed as JSX componentsn = 1,000low cardinality

The IDENTICAL ladder as button-variants — same declarations, same three modules, same rendered element — but each module exports a component instead of a style value: GhostPrimaryButton wraps GhostButton wraps Button. next-yak collapses the chain at construction, so three levels stay one React element; the runtime and utility lanes render three components and re-merge at every level. The gap to button-variants is what the component API costs, with the file layout held constant. The next-yak css-prop lanes have no cell here: the css prop resolves only top-scope values, so a style fragment cannot reach a component through a prop — that API composes fragments at build time or not at all.

Exporting the same three modules as JSX components instead of style values costs every lane, hand-written vanilla included, and costs next-yak its fold outright: a styled component cannot be folded across a module boundary.

This is the Example button-variants shown above with each module exporting a JSX component instead of a style value, so the pair prices the API shape rather than the file layout — both are split three ways. The same asymmetry runs through the Example multifile-shop: exported values fold across a boundary, exported components do not. The css-prop lanes have no cell here at all, because the css prop cannot take a style fragment as a component prop and the case does not compile for them, which is a limitation worth knowing before choosing that API.

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,353,867 r/s ±5%(0.74 ms / 1k elems)
cnfast1,288,037 r/s ±2%(0.78 ms / 1k elems)
next-yak (styled API)1,277,547 r/s ±3%(0.78 ms / 1k elems)
Bamboo1,258,389 r/s ±4%(0.79 ms / 1k elems)
next-yak (styled API) foldStatic: false1,254,967 r/s ±2%(0.80 ms / 1k elems)
StyleX1,066,903 r/s ±4%(0.94 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,020,322 r/s ±2%(0.98 ms / 1k elems)
Panda (css fn)1,011,719 r/s ±3%(0.99 ms / 1k elems)
styled-components791,061 r/s ±1%(1.26 ms / 1k elems)
Panda (style props)767,141 r/s ±5%(1.30 ms / 1k elems)
tailwind-merge746,501 r/s ±2%(1.34 ms / 1k elems)
Goober554,490 r/s ±4%(1.80 ms / 1k elems)
Emotion364,625 r/s ±3%(2.74 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 (styled API)1,146 req/s ±0%
next-yak (styled API) foldStatic: false1,018 req/s ±1%
vanilla (hand-written ceiling)1,012 req/s ±0%
cnfast833 req/s ±0%
Bamboo765 req/s ±0%
StyleX without CSS layers (:not() specificity hack)738 req/s ±0%
StyleX729 req/s ±0%
styled-components676 req/s ±1%
tailwind-merge532 req/s ±2%
Panda (css fn)530 req/s ±1%
Panda (style props)484 req/s ±2%
Goober456 req/s ±0%
Emotion361 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 1.1.0): 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.80 ms
Bamboo0.81 ms
next-yak (styled API)0.88 ms
cnfast0.89 ms
next-yak (styled API) foldStatic: false0.89 ms
StyleX1.19 ms
StyleX without CSS layers (:not() specificity hack)1.23 ms
Panda (css fn)1.28 ms
Panda (style props)1.35 ms
styled-components1.57 ms
Goober2.00 ms
tailwind-merge2.11 ms
Emotion2.94 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 1.1.0). Lower is better.

next-yak (styled API) foldStatic: false46 ms ±3%
Panda (css fn)47 ms ±2%
Bamboo48 ms ±4%
StyleX without CSS layers (:not() specificity hack)49 ms ±1%
StyleX50 ms ±45%
Panda (style props)57 ms ±2%
cnfast57 ms ±3%
tailwind-merge57 ms ±1%
Emotion78 ms ±28%
next-yak (styled API)89 ms ±26%
vanilla (hand-written ceiling)91 ms ±25%
styled-components97 ms ±3%
Goober99 ms ±22%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Bamboo5.54 ms active(29.88 ms span)
next-yak (styled API)5.60 ms active(39.62 ms span)
next-yak (styled API) foldStatic: false5.74 ms active(35.21 ms span)
vanilla (hand-written ceiling)5.96 ms active(30.60 ms span)
Panda (css fn)6.30 ms active(27.02 ms span)
StyleX without CSS layers (:not() specificity hack)6.44 ms active(32.41 ms span)
StyleX6.48 ms active(28.84 ms span)
Panda (style props)7.53 ms active(28.82 ms span)
Emotion9.09 ms active(36.88 ms span)
styled-components9.78 ms active(32.36 ms span)
tailwind-merge10.12 ms active(26.95 ms span)
cnfast10.82 ms active(31.26 ms span)
Goober15.62 ms active(31.19 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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.

Panda (style props)7 ms ±8%
next-yak (styled API)7 ms ±5%
styled-components7 ms ±9%
next-yak (styled API) foldStatic: false7 ms ±17%
StyleX without CSS layers (:not() specificity hack)7 ms ±16%
tailwind-merge7 ms ±7%
cnfast8 ms ±4%
Panda (css fn)8 ms ±18%
Bamboo8 ms ±4%
StyleX8 ms ±16%
vanilla (hand-written ceiling)8 ms ±12%
Emotion11 ms ±4%
Goober11 ms ±26%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)0.61 ms active(33.20 ms span)
Bamboo0.67 ms active(33.34 ms span)
cnfast0.68 ms active(33.25 ms span)
next-yak (styled API)0.68 ms active(32.77 ms span)
Goober0.70 ms active(31.76 ms span)
StyleX0.78 ms active(31.88 ms span)
Emotion0.79 ms active(33.26 ms span)
styled-components0.81 ms active(32.45 ms span)
next-yak (styled API) foldStatic: false0.81 ms active(32.89 ms span)
Panda (css fn)0.91 ms active(32.70 ms span)
tailwind-merge0.92 ms active(31.52 ms span)
StyleX without CSS layers (:not() specificity hack)1.15 ms active(33.31 ms span)
Panda (style props)1.66 ms active(33.17 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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 (styled API)72 ms ±2%
vanilla (hand-written ceiling)72 ms ±2%
next-yak (styled API) foldStatic: false73 ms ±1%
Bamboo73 ms ±2%
Panda (css fn)74 ms ±3%
StyleX without CSS layers (:not() specificity hack)75 ms ±1%
StyleX76 ms ±2%
Panda (style props)83 ms ±2%
tailwind-merge84 ms ±2%
cnfast85 ms ±2%
styled-components88 ms ±1%
Goober93 ms ±3%
Emotion104 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)12.29 ms active(30.41 ms span)
next-yak (styled API)12.46 ms active(33.76 ms span)
Bamboo12.57 ms active(27.91 ms span)
StyleX without CSS layers (:not() specificity hack)12.81 ms active(29.17 ms span)
next-yak (styled API) foldStatic: false13.17 ms active(26.45 ms span)
StyleX13.72 ms active(39.76 ms span)
Goober13.95 ms active(31.38 ms span)
Panda (style props)14.02 ms active(32.15 ms span)
styled-components14.75 ms active(25.17 ms span)
Panda (css fn)14.83 ms active(26.74 ms span)
tailwind-merge17.19 ms active(33.39 ms span)
Emotion17.58 ms active(33.51 ms span)
cnfast23.93 ms active(41.74 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)
BambooChrome6.3 ms1 recalc
Firefox8.0 ms
cnfastChrome12.2 ms1 recalc
Firefox7.9 ms
GooberChrome6.8 ms1 recalc
Firefox8.6 ms
next-yak (styled API)Chrome6.1 ms1 recalc
Firefox8.0 ms
next-yak (styled API) foldStatic: falseChrome6.7 ms1 recalc
Firefox7.0 ms
Panda (css fn)Chrome8.1 ms1 recalc
Firefox7.0 ms
Panda (style props)Chrome6.2 ms1 recalc
Firefox8.1 ms
StyleXChrome7.1 ms1 recalc
Firefox12.6 ms
tailwind-mergeChrome6.7 ms1 recalc
Firefox8.1 ms
vanilla (hand-written ceiling)Chrome6.0 ms1 recalc
Firefox8.1 ms
StyleX without CSS layers (:not() specificity hack)Chrome6.2 ms1 recalc
Firefox12.0 ms
styled-componentsChrome6.8 ms1 recalc
Firefox8.1 ms
EmotionChrome7.6 ms1 recalc
Firefox8.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)3,164 B(0.0 / 0.5 / 2.6 kB)
next-yak (styled API)4,078 B(1.1 / 0.2 / 2.6 kB)
next-yak (styled API) foldStatic: false4,146 B(1.2 / 0.2 / 2.6 kB)
Bamboo5,578 B(1.9 / 0.4 / 3.2 kB)
Goober6,663 B(3.7 / 0.2 / 2.6 kB)
StyleX without CSS layers (:not() specificity hack)6,888 B(3.4 / 0.3 / 3.0 kB)
StyleX6,888 B(3.4 / 0.3 / 3.0 kB)
Panda (css fn)9,035 B(4.8 / 0.3 / 3.7 kB)
Panda (style props)13,055 B(9.1 / 0.3 / 3.4 kB)
tailwind-merge14,245 B(10.4 / 0.3 / 3.2 kB)
cnfast15,208 B(11.3 / 0.3 / 3.2 kB)
Emotion15,822 B(12.7 / 0.3 / 2.4 kB)
styled-components16,575 B(13.1 / 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.02.75.48.1111005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
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.

A full design-system Tabs is where a dynamic prop closure blocks folding: styled(TabInternal) keeps its runtime call, and next-yak's styled lane runs at half its own css-prop sibling.

The Example multifile-composition shown below is this exact workload with the styled primitives moved into an imported parts.tsx, and nothing meaningful moves: next-yak runs 80,800 here against 85,900 there, a difference inside the run-to-run spread, because that module keeps the primitives and their JSX use sites together, which is all folding requires. Contrast the Example multifile-shop, where primitives ship for another module's JSX to use and next-yak gives up 43% of its throughput.

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)160,650 r/s ±3%(0.78 ms / 1k elems)
vanilla (hand-written ceiling)157,604 r/s ±3%(0.79 ms / 1k elems)
StyleX144,121 r/s ±4%(0.87 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)139,475 r/s ±5%(0.90 ms / 1k elems)
next-yak (css prop) foldStatic: false123,148 r/s ±7%(1.02 ms / 1k elems)
next-yak (styled API)80,772 r/s ±2%(1.55 ms / 1k elems)
next-yak (styled API) foldStatic: false72,947 r/s ±2%(1.71 ms / 1k elems)
cnfast71,632 r/s ±2%(1.75 ms / 1k elems)
Bamboo60,263 r/s ±5%(2.07 ms / 1k elems)
Panda (css fn)52,692 r/s ±2%(2.37 ms / 1k elems)
tailwind-merge46,179 r/s ±2%(2.71 ms / 1k elems)
Goober36,788 r/s ±2%(3.40 ms / 1k elems)
styled-components32,398 r/s ±3%(3.86 ms / 1k elems)
Emotion22,787 r/s ±1%(5.49 ms / 1k elems)
Panda (style props)16,900 r/s ±3%(7.40 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)730 req/s ±1%
next-yak (css prop)684 req/s ±2%
next-yak (css prop) foldStatic: false516 req/s ±4%
StyleX without CSS layers (:not() specificity hack)487 req/s ±0%
StyleX449 req/s ±1%
next-yak (styled API)446 req/s ±0%
next-yak (styled API) foldStatic: false362 req/s ±2%
cnfast260 req/s ±1%
Bamboo235 req/s ±0%
Goober208 req/s ±0%
styled-components182 req/s ±0%
tailwind-merge179 req/s ±1%
Panda (css fn)175 req/s ±3%
Emotion143 req/s ±0%
Panda (style props)88 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 1.1.0): 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)1.28 ms
vanilla (hand-written ceiling)1.37 ms
StyleX1.49 ms
StyleX without CSS layers (:not() specificity hack)1.49 ms
next-yak (css prop) foldStatic: false2.19 ms
next-yak (styled API)2.24 ms
cnfast2.50 ms
next-yak (styled API) foldStatic: false2.83 ms
Bamboo2.95 ms
Panda (css fn)3.51 ms
tailwind-merge4.43 ms
Goober4.70 ms
styled-components5.44 ms
Emotion7.13 ms
Panda (style props)9.85 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 1.1.0). Lower is better.

vanilla (hand-written ceiling)45 ms ±1%
next-yak (css prop)45 ms ±2%
StyleX without CSS layers (:not() specificity hack)46 ms ±2%
StyleX46 ms ±2%
Panda (css fn)47 ms ±2%
Bamboo48 ms ±2%
next-yak (css prop) foldStatic: false50 ms ±1%
next-yak (styled API)52 ms ±2%
next-yak (styled API) foldStatic: false53 ms ±2%
tailwind-merge59 ms ±2%
cnfast59 ms ±1%
Panda (style props)82 ms ±2%
Emotion95 ms ±1%
Goober188 ms ±1%
styled-components190 ms ±0%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)7.67 ms active(28.16 ms span)
StyleX7.69 ms active(35.53 ms span)
next-yak (css prop)7.95 ms active(28.60 ms span)
Bamboo8.22 ms active(30.66 ms span)
Panda (css fn)8.49 ms active(36.29 ms span)
StyleX without CSS layers (:not() specificity hack)8.64 ms active(35.01 ms span)
next-yak (css prop) foldStatic: false8.95 ms active(32.99 ms span)
next-yak (styled API)9.30 ms active(32.57 ms span)
next-yak (styled API) foldStatic: false10.07 ms active(35.89 ms span)
tailwind-merge12.56 ms active(35.00 ms span)
cnfast13.03 ms active(30.81 ms span)
Panda (style props)14.09 ms active(31.70 ms span)
Emotion16.67 ms active(32.83 ms span)
Goober22.41 ms active(42.72 ms span)
styled-components22.62 ms active(29.55 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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) foldStatic: false7 ms ±11%
Panda (css fn)7 ms ±6%
cnfast7 ms ±5%
next-yak (css prop)7 ms ±5%
next-yak (styled API)8 ms ±14%
Bamboo8 ms ±8%
tailwind-merge8 ms ±14%
StyleX8 ms ±7%
vanilla (hand-written ceiling)8 ms ±11%
StyleX without CSS layers (:not() specificity hack)8 ms ±26%
next-yak (styled API) foldStatic: false9 ms ±9%
styled-components12 ms ±8%
Goober14 ms ±17%
Panda (style props)24 ms ±4%
Emotion27 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX0.73 ms active(32.85 ms span)
cnfast0.74 ms active(34.38 ms span)
StyleX without CSS layers (:not() specificity hack)0.86 ms active(32.61 ms span)
tailwind-merge0.92 ms active(31.25 ms span)
next-yak (css prop)1.04 ms active(34.80 ms span)
Bamboo1.11 ms active(33.23 ms span)
next-yak (css prop) foldStatic: false1.23 ms active(33.35 ms span)
vanilla (hand-written ceiling)1.32 ms active(32.98 ms span)
Panda (css fn)1.32 ms active(32.98 ms span)
next-yak (styled API)1.47 ms active(32.37 ms span)
Goober1.80 ms active(33.11 ms span)
next-yak (styled API) foldStatic: false1.80 ms active(33.73 ms span)
styled-components1.83 ms active(32.97 ms span)
Emotion2.88 ms active(34.90 ms span)
Panda (style props)3.13 ms active(32.59 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)88 ms ±1%
next-yak (css prop)88 ms ±2%
StyleX without CSS layers (:not() specificity hack)90 ms ±2%
StyleX91 ms ±2%
Bamboo92 ms ±2%
next-yak (css prop) foldStatic: false94 ms ±1%
Panda (css fn)94 ms ±3%
next-yak (styled API)97 ms ±3%
next-yak (styled API) foldStatic: false99 ms ±2%
cnfast104 ms ±2%
tailwind-merge105 ms ±2%
Panda (style props)128 ms ±1%
Goober188 ms ±2%
styled-components192 ms ±1%
Emotion215 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX18.76 ms active(33.89 ms span)
next-yak (css prop)18.92 ms active(29.87 ms span)
StyleX without CSS layers (:not() specificity hack)19.57 ms active(28.46 ms span)
Panda (css fn)19.94 ms active(28.34 ms span)
next-yak (styled API)19.96 ms active(38.26 ms span)
Bamboo20.40 ms active(45.26 ms span)
next-yak (styled API) foldStatic: false20.65 ms active(34.41 ms span)
next-yak (css prop) foldStatic: false21.08 ms active(26.46 ms span)
tailwind-merge23.91 ms active(33.78 ms span)
cnfast24.43 ms active(32.22 ms span)
vanilla (hand-written ceiling)25.10 ms active(36.90 ms span)
Panda (style props)25.51 ms active(45.81 ms span)
Goober28.84 ms active(33.40 ms span)
styled-components28.91 ms active(44.83 ms span)
Emotion32.61 ms active(35.77 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)
cnfastChrome9.6 ms1 recalc
Firefox9.1 ms
next-yak (styled API)Chrome9.1 ms1 recalc
Firefox9.9 ms
next-yak (css prop)Chrome9.9 ms1 recalc
Firefox9.0 ms
next-yak (css prop) foldStatic: falseChrome10.1 ms1 recalc
Firefox10.1 ms
StyleX without CSS layers (:not() specificity hack)Chrome10.1 ms1 recalc
Firefox10.0 ms
StyleXChrome9.4 ms1 recalc
Firefox9.9 ms
vanilla (hand-written ceiling)Chrome15.4 ms1 recalc
Firefox10.1 ms
tailwind-mergeChrome9.7 ms1 recalc
Firefox10.1 ms
EmotionChrome12.8 ms1 recalc
Firefox12.0 ms
Panda (css fn)Chrome9.5 ms1 recalc
Firefox11.0 ms
next-yak (styled API) foldStatic: falseChrome9.1 ms1 recalc
Firefox11.0 ms
BambooChrome9.5 ms1 recalc
Firefox10.3 ms
Panda (style props)Chrome9.5 ms1 recalc
Firefox11.4 ms
GooberChrome14.3 ms1 recalc
Firefox14.3 ms
styled-componentsChrome12.0 ms1 recalc
Firefox20.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
vanilla (hand-written ceiling)2,690 B(0.0 / 1.0 / 1.6 kB)
next-yak (css prop) foldStatic: false2,694 B(0.3 / 0.7 / 1.7 kB)
next-yak (css prop)2,707 B(0.3 / 0.6 / 1.7 kB)
next-yak (styled API)3,511 B(1.1 / 0.6 / 1.7 kB)
next-yak (styled API) foldStatic: false3,573 B(1.2 / 0.6 / 1.7 kB)
Goober6,248 B(3.7 / 0.7 / 1.7 kB)
StyleX8,969 B(3.4 / 0.9 / 4.5 kB)
StyleX without CSS layers (:not() specificity hack)9,043 B(3.4 / 1.0 / 4.5 kB)
Bamboo12,028 B(1.9 / 1.3 / 8.6 kB)
Emotion15,547 B(12.7 / 0.8 / 1.6 kB)
styled-components16,108 B(13.1 / 0.8 / 1.8 kB)
Panda (css fn)16,280 B(4.8 / 1.2 / 9.9 kB)
tailwind-merge21,051 B(10.4 / 1.3 / 8.9 kB)
Panda (style props)21,202 B(9.1 / 1.2 / 10.5 kB)
cnfast22,014 B(11.3 / 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.0701392092791005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
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.

Moving a component into its own module is free: the primitives and the JSX that uses them travel together, folding still sees both, and next-yak lands within the run-to-run spread of the single-file case.

This is the Example tabs shown above with the styled primitives moved into an imported parts.tsx, and nothing measurable changes. Read it against the Example multifile-shop, which asks the other half of the question: there the primitives ship for a different module's JSX to use, fold_jsx_usages never sees a declaration and its use site together, and next-yak gives up 43% of its throughput. Moving a whole component across a file is free; shipping primitives for someone else to use is not.

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)152,562 r/s ±3%(0.82 ms / 1k elems)
StyleX142,343 r/s ±5%(0.88 ms / 1k elems)
next-yak (css prop)138,462 r/s ±4%(0.90 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)136,815 r/s ±5%(0.91 ms / 1k elems)
next-yak (css prop) foldStatic: false108,170 r/s ±6%(1.16 ms / 1k elems)
next-yak (styled API)85,913 r/s ±3%(1.45 ms / 1k elems)
next-yak (styled API) foldStatic: false73,510 r/s ±4%(1.70 ms / 1k elems)
cnfast73,308 r/s ±2%(1.71 ms / 1k elems)
Bamboo58,780 r/s ±2%(2.13 ms / 1k elems)
Panda (css fn)52,332 r/s ±2%(2.39 ms / 1k elems)
tailwind-merge46,298 r/s ±1%(2.70 ms / 1k elems)
Goober35,096 r/s ±2%(3.56 ms / 1k elems)
styled-components31,895 r/s ±2%(3.92 ms / 1k elems)
Emotion22,023 r/s ±3%(5.68 ms / 1k elems)
Panda (style props)16,417 r/s ±3%(7.61 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)675 req/s ±0%
vanilla (hand-written ceiling)665 req/s ±2%
next-yak (css prop) foldStatic: false538 req/s ±0%
StyleX without CSS layers (:not() specificity hack)491 req/s ±1%
StyleX479 req/s ±3%
next-yak (styled API)449 req/s ±0%
next-yak (styled API) foldStatic: false392 req/s ±1%
cnfast262 req/s ±0%
Bamboo235 req/s ±0%
Goober209 req/s ±0%
Panda (css fn)185 req/s ±0%
tailwind-merge181 req/s ±2%
styled-components171 req/s
Emotion143 req/s
Panda (style props)90 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 1.1.0): 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.34 ms
StyleX1.45 ms
StyleX without CSS layers (:not() specificity hack)1.56 ms
next-yak (css prop)1.64 ms
next-yak (css prop) foldStatic: false1.74 ms
next-yak (styled API)2.09 ms
next-yak (styled API) foldStatic: false2.31 ms
cnfast2.46 ms
Bamboo3.06 ms
Panda (css fn)3.43 ms
tailwind-merge3.86 ms
Goober4.37 ms
styled-components5.58 ms
Emotion7.17 ms
Panda (style props)9.07 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 1.1.0). Lower is better.

next-yak (css prop)45 ms ±1%
vanilla (hand-written ceiling)45 ms ±4%
StyleX without CSS layers (:not() specificity hack)46 ms ±2%
Panda (css fn)47 ms ±1%
StyleX47 ms ±1%
Bamboo48 ms ±3%
next-yak (css prop) foldStatic: false49 ms ±2%
next-yak (styled API)52 ms ±3%
next-yak (styled API) foldStatic: false54 ms ±2%
cnfast58 ms ±2%
tailwind-merge59 ms ±2%
Panda (style props)82 ms ±2%
Emotion95 ms ±1%
Goober187 ms ±1%
styled-components192 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX without CSS layers (:not() specificity hack)7.44 ms active(32.81 ms span)
vanilla (hand-written ceiling)7.59 ms active(27.67 ms span)
next-yak (css prop)7.62 ms active(34.40 ms span)
Bamboo8.19 ms active(34.06 ms span)
StyleX8.20 ms active(35.30 ms span)
Panda (css fn)8.51 ms active(32.23 ms span)
next-yak (styled API)8.98 ms active(30.70 ms span)
next-yak (css prop) foldStatic: false9.11 ms active(27.94 ms span)
next-yak (styled API) foldStatic: false11.02 ms active(30.15 ms span)
tailwind-merge12.10 ms active(37.98 ms span)
cnfast12.79 ms active(31.97 ms span)
Panda (style props)14.74 ms active(39.37 ms span)
Emotion16.59 ms active(41.35 ms span)
Goober21.21 ms active(40.10 ms span)
styled-components23.01 ms active(33.72 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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.

StyleX7 ms ±14%
tailwind-merge7 ms ±17%
vanilla (hand-written ceiling)7 ms ±3%
Bamboo7 ms ±3%
cnfast7 ms ±10%
next-yak (styled API)7 ms ±7%
next-yak (css prop)7 ms ±9%
Panda (css fn)7 ms ±9%
next-yak (css prop) foldStatic: false8 ms ±7%
StyleX without CSS layers (:not() specificity hack)8 ms ±18%
next-yak (styled API) foldStatic: false9 ms ±8%
styled-components13 ms ±14%
Goober13 ms ±12%
Panda (style props)26 ms ±3%
Emotion26 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)0.68 ms active(33.08 ms span)
tailwind-merge0.80 ms active(32.91 ms span)
cnfast0.82 ms active(35.41 ms span)
StyleX without CSS layers (:not() specificity hack)0.85 ms active(32.49 ms span)
next-yak (css prop)0.85 ms active(33.77 ms span)
next-yak (css prop) foldStatic: false0.87 ms active(34.77 ms span)
StyleX0.88 ms active(31.74 ms span)
next-yak (styled API)1.10 ms active(32.43 ms span)
Bamboo1.12 ms active(32.77 ms span)
next-yak (styled API) foldStatic: false1.22 ms active(31.71 ms span)
Panda (css fn)1.36 ms active(32.93 ms span)
styled-components1.67 ms active(31.73 ms span)
Goober2.42 ms active(31.97 ms span)
Panda (style props)2.74 ms active(31.60 ms span)
Emotion3.27 ms active(34.56 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)88 ms ±1%
vanilla (hand-written ceiling)89 ms ±1%
StyleX without CSS layers (:not() specificity hack)90 ms ±2%
StyleX92 ms ±3%
Bamboo92 ms ±2%
next-yak (css prop) foldStatic: false93 ms ±3%
Panda (css fn)93 ms ±2%
next-yak (styled API)96 ms ±1%
next-yak (styled API) foldStatic: false98 ms ±2%
cnfast103 ms ±1%
tailwind-merge105 ms ±2%
Panda (style props)128 ms ±1%
Goober189 ms ±2%
styled-components193 ms ±1%
Emotion214 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)18.32 ms active(38.65 ms span)
next-yak (css prop)18.54 ms active(25.97 ms span)
StyleX without CSS layers (:not() specificity hack)18.71 ms active(25.69 ms span)
Bamboo19.40 ms active(33.12 ms span)
Panda (css fn)19.95 ms active(26.65 ms span)
StyleX20.56 ms active(34.77 ms span)
next-yak (styled API) foldStatic: false20.92 ms active(44.70 ms span)
next-yak (css prop) foldStatic: false23.23 ms active(38.89 ms span)
next-yak (styled API)24.21 ms active(26.36 ms span)
tailwind-merge24.95 ms active(48.33 ms span)
cnfast25.36 ms active(47.19 ms span)
Goober27.23 ms active(27.73 ms span)
styled-components29.23 ms active(45.64 ms span)
Panda (style props)33.30 ms active(38.00 ms span)
Emotion34.21 ms active(46.98 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)
BambooChrome9.5 ms1 recalc
Firefox9.0 ms
cnfastChrome10.0 ms1 recalc
Firefox37.0 ms
EmotionChrome13.6 ms1 recalc
Firefox12.5 ms
next-yak (styled API)Chrome13.2 ms1 recalc
Firefox9.0 ms
next-yak (css prop) foldStatic: falseChrome11.9 ms1 recalc
Firefox9.0 ms
Panda (css fn)Chrome9.2 ms1 recalc
Firefox9.0 ms
StyleXChrome9.9 ms1 recalc
Firefox10.0 ms
tailwind-mergeChrome10.2 ms1 recalc
Firefox10.1 ms
vanilla (hand-written ceiling)Chrome9.4 ms1 recalc
Firefox11.0 ms
styled-componentsChrome12.3 ms1 recalc
Firefox11.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome9.2 ms1 recalc
Firefox10.0 ms
next-yak (css prop)Chrome9.4 ms1 recalc
Firefox10.1 ms
next-yak (styled API) foldStatic: falseChrome8.8 ms1 recalc
Firefox11.1 ms
Panda (style props)Chrome16.6 ms1 recalc
Firefox12.3 ms
GooberChrome12.5 ms1 recalc
Firefox15.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) foldStatic: false2,690 B(0.3 / 0.6 / 1.7 kB)
vanilla (hand-written ceiling)2,690 B(0.0 / 1.0 / 1.6 kB)
next-yak (css prop)2,706 B(0.3 / 0.6 / 1.7 kB)
next-yak (styled API)3,512 B(1.1 / 0.6 / 1.7 kB)
next-yak (styled API) foldStatic: false3,575 B(1.2 / 0.6 / 1.7 kB)
Goober6,248 B(3.7 / 0.7 / 1.7 kB)
StyleX8,969 B(3.4 / 0.9 / 4.5 kB)
StyleX without CSS layers (:not() specificity hack)9,043 B(3.4 / 1.0 / 4.5 kB)
Bamboo12,028 B(1.9 / 1.3 / 8.6 kB)
Emotion15,547 B(12.7 / 0.8 / 1.6 kB)
styled-components16,091 B(13.1 / 0.8 / 1.7 kB)
Panda (css fn)16,280 B(4.8 / 1.2 / 9.9 kB)
tailwind-merge21,051 B(10.4 / 1.3 / 8.9 kB)
Panda (style props)21,202 B(9.1 / 1.2 / 10.5 kB)
cnfast22,014 B(11.3 / 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.0701392092781005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
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.

Baking a unique value into the class name mints 1,000 one-off classes: goober needs 1.27 s to hydrate the page and 149 ms at the largest size, while the lanes that keep the value out of the selector never notice.

The Examples dyn-fair and dyn-inline shown below take this workload two other ways. In dyn-fair every lane uses its own best practice, which for a library with no native dynamic-value support means a hand-written split of static class from inline style; that lifts goober from 166,000 to 945,000 renders/s and panda from 222,000 to 1.00M. next-yak ships that separation as a compiler feature, so nobody writes it; elsewhere the same result is developer discipline applied to every dynamic value, on every component, indefinitely. The naive pattern measured here is what a codebase gets when nobody pays that cost.

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.

Bamboo1,638,450 r/s ±2%(0.61 ms / 1k elems)
next-yak (css prop)1,590,141 r/s ±4%(0.63 ms / 1k elems)
vanilla (hand-written ceiling)1,580,613 r/s ±2%(0.63 ms / 1k elems)
next-yak (css prop) foldStatic: false1,439,454 r/s ±4%(0.69 ms / 1k elems)
StyleX1,171,932 r/s ±1%(0.85 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,144,383 r/s ±3%(0.87 ms / 1k elems)
next-yak (styled API)789,214 r/s ±4%(1.27 ms / 1k elems)
next-yak (styled API) foldStatic: false689,081 r/s ±9%(1.45 ms / 1k elems)
cnfast574,108 r/s ±3%(1.74 ms / 1k elems)
tailwind-merge504,796 r/s ±5%(1.98 ms / 1k elems)
Emotion353,243 r/s ±7%(2.83 ms / 1k elems)
styled-components228,652 r/s ±5%(4.37 ms / 1k elems)
Panda (css fn)221,965 r/s ±5%(4.51 ms / 1k elems)
Goober166,309 r/s ±4%(6.01 ms / 1k elems)
Panda (style props)157,116 r/s ±7%(6.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.

next-yak (css prop)1,349 req/s ±0%
Bamboo1,177 req/s ±0%
next-yak (css prop) foldStatic: false1,162 req/s ±0%
vanilla (hand-written ceiling)1,148 req/s ±4%
StyleX without CSS layers (:not() specificity hack)844 req/s ±2%
StyleX837 req/s ±2%
next-yak (styled API)709 req/s ±0%
next-yak (styled API) foldStatic: false681 req/s ±2%
cnfast519 req/s ±0%
tailwind-merge424 req/s ±0%
Emotion349 req/s ±0%
styled-components186 req/s ±2%
Panda (css fn)156 req/s ±2%
Goober152 req/s ±0%
Panda (style props)135 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 1.1.0): 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
Bamboo0.74 ms
next-yak (css prop)0.80 ms
vanilla (hand-written ceiling)0.80 ms
next-yak (css prop) foldStatic: false0.85 ms
StyleX without CSS layers (:not() specificity hack)1.12 ms
StyleX1.14 ms
next-yak (styled API) foldStatic: false1.34 ms
next-yak (styled API)1.39 ms
cnfast2.04 ms
tailwind-merge2.48 ms
Emotion3.09 ms
styled-components5.05 ms
Panda (css fn)5.67 ms
Goober6.97 ms
Panda (style props)8.74 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 1.1.0). Lower is better.

next-yak (css prop)45 ms ±33%
next-yak (css prop) foldStatic: false46 ms ±2%
Bamboo48 ms ±2%
StyleX49 ms ±4%
StyleX without CSS layers (:not() specificity hack)49 ms ±6%
next-yak (styled API)49 ms ±5%
next-yak (styled API) foldStatic: false50 ms ±7%
tailwind-merge64 ms ±24%
cnfast66 ms ±24%
Panda (css fn)66 ms ±2%
vanilla (hand-written ceiling)78 ms ±21%
Panda (style props)85 ms ±2%
Emotion89 ms ±18%
styled-components109 ms ±15%
Goober1,265 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Bamboo5.92 ms active(28.83 ms span)
next-yak (css prop)5.93 ms active(26.63 ms span)
next-yak (styled API) foldStatic: false5.95 ms active(29.03 ms span)
next-yak (css prop) foldStatic: false6.06 ms active(27.98 ms span)
next-yak (styled API)6.09 ms active(31.86 ms span)
StyleX6.15 ms active(28.96 ms span)
vanilla (hand-written ceiling)6.16 ms active(41.55 ms span)
StyleX without CSS layers (:not() specificity hack)6.28 ms active(35.40 ms span)
Panda (css fn)7.00 ms active(27.12 ms span)
Panda (style props)8.11 ms active(33.73 ms span)
Emotion9.15 ms active(29.87 ms span)
Goober9.24 ms active(32.66 ms span)
styled-components9.58 ms active(27.02 ms span)
cnfast10.01 ms active(35.43 ms span)
tailwind-merge10.67 ms active(33.00 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)7 ms ±8%
styled-components7 ms ±14%
StyleX without CSS layers (:not() specificity hack)7 ms ±13%
vanilla (hand-written ceiling)7 ms ±11%
next-yak (css prop) foldStatic: false8 ms ±14%
Bamboo8 ms ±3%
next-yak (styled API)8 ms ±13%
next-yak (styled API) foldStatic: false8 ms ±12%
StyleX8 ms ±16%
cnfast8 ms ±4%
tailwind-merge8 ms ±11%
Emotion9 ms ±7%
Goober22 ms ±12%
Panda (css fn)22 ms ±7%
Panda (style props)29 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
cnfast0.63 ms active(32.41 ms span)
Goober0.65 ms active(34.91 ms span)
tailwind-merge0.68 ms active(33.45 ms span)
next-yak (css prop) foldStatic: false0.68 ms active(34.09 ms span)
next-yak (styled API) foldStatic: false0.69 ms active(34.61 ms span)
Emotion0.71 ms active(33.06 ms span)
vanilla (hand-written ceiling)0.71 ms active(32.78 ms span)
Bamboo0.73 ms active(36.01 ms span)
Panda (css fn)0.73 ms active(33.12 ms span)
next-yak (css prop)0.80 ms active(32.91 ms span)
next-yak (styled API)0.84 ms active(33.85 ms span)
styled-components0.85 ms active(32.97 ms span)
StyleX1.09 ms active(34.89 ms span)
Panda (style props)1.13 ms active(36.09 ms span)
StyleX without CSS layers (:not() specificity hack)1.60 ms active(35.26 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)60 ms ±2%
next-yak (css prop) foldStatic: false61 ms ±1%
vanilla (hand-written ceiling)64 ms ±2%
Bamboo64 ms ±2%
next-yak (styled API)65 ms ±2%
StyleX65 ms ±4%
next-yak (styled API) foldStatic: false65 ms ±1%
StyleX without CSS layers (:not() specificity hack)66 ms ±2%
tailwind-merge79 ms ±2%
cnfast80 ms ±1%
Panda (css fn)82 ms ±2%
Panda (style props)100 ms ±1%
styled-components106 ms ±1%
Emotion111 ms ±2%
Goober1,241 ms ±0%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX11.12 ms active(33.41 ms span)
vanilla (hand-written ceiling)11.24 ms active(26.88 ms span)
Bamboo11.38 ms active(35.54 ms span)
next-yak (css prop) foldStatic: false11.44 ms active(28.48 ms span)
next-yak (styled API) foldStatic: false11.93 ms active(34.78 ms span)
Panda (css fn)12.72 ms active(30.72 ms span)
next-yak (css prop)13.26 ms active(32.66 ms span)
Goober13.38 ms active(32.17 ms span)
Panda (style props)14.21 ms active(34.36 ms span)
styled-components14.30 ms active(35.25 ms span)
next-yak (styled API)14.32 ms active(25.73 ms span)
Emotion14.71 ms active(26.72 ms span)
tailwind-merge15.68 ms active(33.23 ms span)
cnfast16.16 ms active(37.79 ms span)
StyleX without CSS layers (:not() specificity hack)16.96 ms active(29.09 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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.3 ms1 recalc
Firefox1.9 ms
EmotionChrome4.7 ms1 recalc
Firefox1.8 ms
next-yak (styled API)Chrome7.8 ms1 recalc
Firefox1.8 ms
next-yak (css prop) foldStatic: falseChrome4.5 ms1 recalc
Firefox1.9 ms
next-yak (styled API) foldStatic: falseChrome5.1 ms1 recalc
Firefox2.1 ms
Panda (css fn)Chrome4.7 ms1 recalc
Firefox1.8 ms
Panda (style props)Chrome4.7 ms1 recalc
Firefox2.0 ms
styled-componentsChrome5.1 ms1 recalc
Firefox1.9 ms
StyleX without CSS layers (:not() specificity hack)Chrome10.5 ms1 recalc
Firefox2.0 ms
StyleXChrome4.5 ms1 recalc
Firefox3.2 ms
tailwind-mergeChrome4.5 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome5.1 ms1 recalc
Firefox1.0 ms
next-yak (css prop)Chrome6.8 ms1 recalc
Firefox2.8 ms
BambooChrome4.7 ms1 recalc
Firefox3.3 ms
GooberChrome4.5 ms1 recalc
Firefox5.4 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,502 B(0.0 / 0.3 / 5.0 kB)
next-yak (css prop) foldStatic: false5,563 B(0.3 / 0.1 / 5.0 kB)
next-yak (css prop)5,586 B(0.3 / 0.1 / 5.0 kB)
next-yak (styled API)6,388 B(1.1 / 0.1 / 5.0 kB)
next-yak (styled API) foldStatic: false6,450 B(1.2 / 0.1 / 5.0 kB)
Bamboo7,503 B(1.9 / 0.1 / 5.4 kB)
StyleX without CSS layers (:not() specificity hack)9,120 B(3.4 / 0.1 / 5.4 kB)
StyleX9,125 B(3.4 / 0.1 / 5.4 kB)
Panda (css fn)10,567 B(4.8 / 0.1 / 5.4 kB)
Panda (style props)14,940 B(9.1 / 0.1 / 5.4 kB)
tailwind-merge21,015 B(10.4 / 5.1 / 5.0 kB)
Goober21,397 B(3.7 / 8.9 / 8.3 kB)
cnfast21,978 B(11.3 / 5.1 / 5.0 kB)
styled-components35,526 B(13.1 / 13.5 / 8.0 kB)
Emotion39,394 B(12.7 / 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.037741121491005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
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.

With every lane on its own best practice the field converges near 1.6M renders/s, and next-yak's automatic CSS-variable path costs 2.08× against the inline style the other lanes write by hand.

The trio reads as one argument. The Example dyn-translate shown above is the naive pattern, what a codebase produces when nobody optimizes: goober at 166,000 renders/s and a 1.27 s hydration. This case is every lane at its best, and for a library without native support that best is hand-written, the developer splitting static class from inline style per component, for every dynamic value, indefinitely. next-yak ships that separation as a compiler feature instead. The Example dyn-inline shown below removes the last variable by putting every lane, next-yak included, on a plain inline style, and next-yak renders 1.51M there against 724,000 here, which prices its CSS-variable path at 2.08×.

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.

cnfast1,618,233 r/s ±4%(0.62 ms / 1k elems)
Bamboo1,579,988 r/s ±3%(0.63 ms / 1k elems)
vanilla (hand-written ceiling)1,575,629 r/s ±6%(0.63 ms / 1k elems)
tailwind-merge1,571,299 r/s ±5%(0.64 ms / 1k elems)
next-yak (css prop)1,543,410 r/s ±4%(0.65 ms / 1k elems)
next-yak (css prop) foldStatic: false1,390,256 r/s ±2%(0.72 ms / 1k elems)
StyleX1,158,749 r/s ±2%(0.86 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,091,554 r/s ±5%(0.92 ms / 1k elems)
Panda (css fn)999,625 r/s ±3%(1.00 ms / 1k elems)
Goober945,254 r/s ±2%(1.06 ms / 1k elems)
next-yak (styled API) foldStatic: false779,499 r/s ±3%(1.28 ms / 1k elems)
styled-components765,574 r/s ±2%(1.31 ms / 1k elems)
next-yak (styled API)723,742 r/s ±8%(1.38 ms / 1k elems)
Panda (style props)587,573 r/s ±2%(1.70 ms / 1k elems)
Emotion423,400 r/s ±3%(2.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.

next-yak (css prop)1,347 req/s ±0%
cnfast1,221 req/s ±0%
vanilla (hand-written ceiling)1,207 req/s ±2%
Bamboo1,175 req/s ±0%
next-yak (css prop) foldStatic: false1,099 req/s ±2%
tailwind-merge1,067 req/s ±0%
StyleX850 req/s ±0%
StyleX without CSS layers (:not() specificity hack)821 req/s ±3%
next-yak (styled API)713 req/s ±0%
next-yak (styled API) foldStatic: false698 req/s ±0%
Goober674 req/s ±0%
Panda (css fn)622 req/s ±1%
styled-components561 req/s ±1%
Panda (style props)493 req/s ±1%
Emotion411 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 1.1.0): 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)0.76 ms
Bamboo0.76 ms
cnfast0.79 ms
vanilla (hand-written ceiling)0.83 ms
next-yak (css prop) foldStatic: false0.92 ms
tailwind-merge0.93 ms
StyleX without CSS layers (:not() specificity hack)1.15 ms
StyleX1.21 ms
next-yak (styled API) foldStatic: false1.25 ms
next-yak (styled API)1.33 ms
Panda (css fn)1.37 ms
Goober1.45 ms
styled-components1.61 ms
Panda (style props)1.80 ms
Emotion2.46 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 1.1.0). Lower is better.

next-yak (css prop) foldStatic: false47 ms ±2%
next-yak (css prop)47 ms ±34%
vanilla (hand-written ceiling)48 ms ±33%
Bamboo48 ms ±33%
StyleX without CSS layers (:not() specificity hack)49 ms ±33%
StyleX49 ms ±3%
next-yak (styled API)50 ms ±31%
next-yak (styled API) foldStatic: false50 ms ±12%
Panda (css fn)52 ms ±30%
tailwind-merge56 ms ±3%
cnfast58 ms ±28%
Panda (style props)59 ms ±2%
Emotion71 ms ±1%
Goober81 ms ±2%
styled-components82 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (css prop)5.71 ms active(30.07 ms span)
vanilla (hand-written ceiling)5.78 ms active(35.20 ms span)
StyleX5.79 ms active(25.29 ms span)
next-yak (css prop) foldStatic: false5.93 ms active(31.99 ms span)
Panda (css fn)5.93 ms active(32.69 ms span)
StyleX without CSS layers (:not() specificity hack)6.00 ms active(34.11 ms span)
Bamboo6.03 ms active(32.36 ms span)
next-yak (styled API) foldStatic: false6.03 ms active(31.71 ms span)
next-yak (styled API)6.36 ms active(30.14 ms span)
Panda (style props)7.37 ms active(28.27 ms span)
Goober7.39 ms active(28.40 ms span)
styled-components7.63 ms active(32.98 ms span)
Emotion8.62 ms active(28.39 ms span)
cnfast9.57 ms active(33.56 ms span)
tailwind-merge10.45 ms active(34.17 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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.

Goober7 ms ±12%
next-yak (styled API) foldStatic: false7 ms ±11%
Panda (css fn)7 ms ±19%
Panda (style props)7 ms ±10%
styled-components7 ms ±21%
next-yak (css prop)7 ms ±12%
next-yak (css prop) foldStatic: false7 ms ±13%
StyleX7 ms ±17%
Bamboo8 ms ±2%
next-yak (styled API)8 ms ±3%
StyleX without CSS layers (:not() specificity hack)8 ms ±9%
tailwind-merge8 ms ±9%
cnfast8 ms ±6%
vanilla (hand-written ceiling)8 ms ±8%
Emotion10 ms ±8%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (css prop)0.55 ms active(32.78 ms span)
next-yak (styled API)0.67 ms active(32.97 ms span)
Bamboo0.67 ms active(32.90 ms span)
StyleX0.67 ms active(33.30 ms span)
Emotion0.71 ms active(33.02 ms span)
cnfast0.72 ms active(32.68 ms span)
styled-components0.74 ms active(32.90 ms span)
vanilla (hand-written ceiling)0.76 ms active(33.59 ms span)
Panda (style props)0.77 ms active(31.66 ms span)
next-yak (styled API) foldStatic: false0.78 ms active(32.64 ms span)
next-yak (css prop) foldStatic: false0.79 ms active(32.44 ms span)
Panda (css fn)0.85 ms active(34.45 ms span)
Goober0.86 ms active(33.86 ms span)
tailwind-merge0.87 ms active(33.22 ms span)
StyleX without CSS layers (:not() specificity hack)0.87 ms active(32.90 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)60 ms ±2%
Bamboo63 ms ±3%
next-yak (css prop) foldStatic: false63 ms ±1%
vanilla (hand-written ceiling)63 ms ±3%
next-yak (styled API)65 ms ±3%
StyleX without CSS layers (:not() specificity hack)66 ms ±2%
next-yak (styled API) foldStatic: false66 ms ±3%
StyleX66 ms ±3%
Panda (css fn)67 ms ±2%
tailwind-merge74 ms ±1%
cnfast74 ms ±2%
Panda (style props)76 ms ±2%
Goober79 ms ±2%
styled-components81 ms ±2%
Emotion91 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (css prop)10.86 ms active(28.88 ms span)
vanilla (hand-written ceiling)11.02 ms active(27.00 ms span)
Bamboo11.06 ms active(27.36 ms span)
StyleX without CSS layers (:not() specificity hack)11.12 ms active(29.57 ms span)
next-yak (styled API) foldStatic: false11.29 ms active(30.07 ms span)
StyleX11.40 ms active(29.07 ms span)
next-yak (styled API)11.80 ms active(29.13 ms span)
Panda (css fn)11.85 ms active(28.46 ms span)
Goober11.86 ms active(28.10 ms span)
styled-components12.56 ms active(29.47 ms span)
next-yak (css prop) foldStatic: false12.76 ms active(28.24 ms span)
Panda (style props)12.95 ms active(29.56 ms span)
Emotion13.75 ms active(43.02 ms span)
cnfast14.85 ms active(29.34 ms span)
tailwind-merge15.26 ms active(35.82 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)
BambooChrome4.9 ms1 recalc
Firefox2.0 ms
cnfastChrome4.7 ms1 recalc
Firefox2.0 ms
EmotionChrome4.5 ms1 recalc
Firefox2.0 ms
GooberChrome4.7 ms1 recalc
Firefox2.1 ms
next-yak (css prop)Chrome4.6 ms1 recalc
Firefox2.1 ms
next-yak (css prop) foldStatic: falseChrome5.8 ms1 recalc
Firefox7.5 ms
next-yak (styled API) foldStatic: falseChrome4.5 ms1 recalc
Firefox1.9 ms
Panda (css fn)Chrome4.9 ms1 recalc
Firefox1.2 ms
Panda (style props)Chrome5.1 ms1 recalc
Firefox2.0 ms
styled-componentsChrome4.6 ms1 recalc
Firefox2.2 ms
StyleX without CSS layers (:not() specificity hack)Chrome4.8 ms1 recalc
Firefox2.0 ms
tailwind-mergeChrome4.9 ms1 recalc
Firefox2.0 ms
StyleXChrome4.9 ms1 recalc
Firefox1.9 ms
next-yak (styled API)Chrome5.4 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome4.9 ms1 recalc
Firefox2.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
vanilla (hand-written ceiling)5,502 B(0.0 / 0.3 / 5.0 kB)
next-yak (css prop) foldStatic: false5,565 B(0.3 / 0.1 / 5.0 kB)
next-yak (css prop)5,583 B(0.3 / 0.1 / 5.0 kB)
next-yak (styled API)6,387 B(1.1 / 0.1 / 5.0 kB)
next-yak (styled API) foldStatic: false6,451 B(1.2 / 0.1 / 5.0 kB)
Bamboo7,503 B(1.9 / 0.1 / 5.4 kB)
StyleX without CSS layers (:not() specificity hack)9,120 B(3.4 / 0.1 / 5.4 kB)
StyleX9,125 B(3.4 / 0.1 / 5.4 kB)
Goober9,164 B(3.7 / 0.1 / 5.2 kB)
Panda (css fn)10,517 B(4.8 / 0.1 / 5.4 kB)
Panda (style props)15,050 B(9.1 / 0.1 / 5.5 kB)
tailwind-merge16,013 B(10.4 / 0.1 / 5.2 kB)
cnfast16,976 B(11.3 / 0.1 / 5.2 kB)
Emotion18,420 B(12.7 / 0.1 / 5.2 kB)
styled-components18,896 B(13.1 / 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.02.44.87.29.61005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
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.

With every lane on a plain inline style, next-yak included, only the element-level fold is left to measure: next-yak and hand-written vanilla finish within 4%, and next-yak's own no-fold lane costs 1.41×.

The control for the Examples dyn-translate and dyn-fair shown above. Holding the technique constant across every lane isolates one number: next-yak renders 1.51M here against 724,000 in dyn-fair, so its CSS custom-property path costs 2.08× against the inline style other lanes write by hand. That is the honest price of the feature, and it stands against what the feature removes — the naive pattern in dyn-translate, where goober falls to 166,000 renders/s and hydration takes 1.27 s because nobody wrote the split.

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,565,251 r/s ±3%(0.64 ms / 1k elems)
next-yak (styled API)1,507,727 r/s ±5%(0.66 ms / 1k elems)
next-yak (styled API) foldStatic: false1,068,708 r/s ±4%(0.94 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 (styled API)1,238 req/s ±0%
vanilla (hand-written ceiling)1,203 req/s ±3%
next-yak (styled API) foldStatic: false926 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 1.1.0): 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 (styled API)0.79 ms
vanilla (hand-written ceiling)0.81 ms
next-yak (styled API) foldStatic: false1.02 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 1.1.0). Lower is better.

next-yak (styled API)47 ms ±4%
vanilla (hand-written ceiling)49 ms ±32%
next-yak (styled API) foldStatic: false50 ms ±9%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (styled API)5.63 ms active(29.87 ms span)
vanilla (hand-written ceiling)5.73 ms active(33.88 ms span)
next-yak (styled API) foldStatic: false5.86 ms active(27.61 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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.

vanilla (hand-written ceiling)7 ms ±9%
next-yak (styled API) foldStatic: false8 ms ±5%
next-yak (styled API)8 ms ±18%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)0.64 ms active(33.61 ms span)
next-yak (styled API)0.72 ms active(32.83 ms span)
next-yak (styled API) foldStatic: false0.91 ms active(33.40 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)62 ms ±26%
next-yak (styled API)62 ms ±2%
next-yak (styled API) foldStatic: false67 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (styled API)10.84 ms active(32.05 ms span)
vanilla (hand-written ceiling)11.05 ms active(31.72 ms span)
next-yak (styled API) foldStatic: false11.36 ms active(33.76 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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 (styled API) foldStatic: falseChrome5.1 ms1 recalc
Firefox0.8 ms
vanilla (hand-written ceiling)Chrome4.4 ms1 recalc
Firefox2.0 ms
next-yak (styled API)Chrome4.5 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
vanilla (hand-written ceiling)5,502 B(0.0 / 0.3 / 5.0 kB)
next-yak (styled API)6,581 B(1.1 / 0.1 / 5.2 kB)
next-yak (styled API) foldStatic: false6,643 B(1.2 / 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.00.91.92.83.71005001,0002,0004,000
next-yak (styled API)next-yak (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.

A thousand buttons cycling twelve class strings put eight lanes within 20% of each other, because almost every lookup is a cache hit; the split is between lanes that cache and lanes that recompute regardless.

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 (styled API)1,784,917 r/s ±13%(0.56 ms / 1k elems)
next-yak (css prop)1,735,734 r/s ±11%(0.58 ms / 1k elems)
cnfast1,658,603 r/s ±9%(0.60 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,649,033 r/s ±7%(0.61 ms / 1k elems)
StyleX1,642,373 r/s ±9%(0.61 ms / 1k elems)
vanilla (hand-written ceiling)1,522,552 r/s ±11%(0.66 ms / 1k elems)
Bamboo1,457,726 r/s ±10%(0.69 ms / 1k elems)
Panda (recipe)1,298,351 r/s ±37%(0.77 ms / 1k elems)
next-yak (css prop) foldStatic: false1,254,508 r/s ±11%(0.80 ms / 1k elems)
tailwind-merge1,188,413 r/s ±8%(0.84 ms / 1k elems)
Panda (css fn)768,172 r/s ±8%(1.30 ms / 1k elems)
next-yak (styled API) foldStatic: false743,609 r/s ±6%(1.34 ms / 1k elems)
styled-components562,707 r/s ±7%(1.78 ms / 1k elems)
Goober508,184 r/s ±3%(1.97 ms / 1k elems)
Panda (style props)478,593 r/s ±4%(2.09 ms / 1k elems)
Emotion368,386 r/s ±4%(2.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 (styled API)1,593 req/s ±0%
next-yak (css prop)1,447 req/s ±0%
vanilla (hand-written ceiling)1,276 req/s ±2%
StyleX without CSS layers (:not() specificity hack)1,237 req/s ±0%
StyleX1,225 req/s ±0%
cnfast1,108 req/s ±0%
Bamboo1,027 req/s ±1%
next-yak (css prop) foldStatic: false1,014 req/s ±2%
Panda (recipe)893 req/s ±2%
tailwind-merge768 req/s ±0%
next-yak (styled API) foldStatic: false629 req/s ±1%
Panda (css fn)567 req/s ±1%
styled-components520 req/s ±1%
Goober463 req/s
Panda (style props)369 req/s ±0%
Emotion352 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 1.1.0): 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 (styled API)0.67 ms
cnfast0.68 ms
Bamboo0.71 ms
next-yak (css prop)0.73 ms
StyleX without CSS layers (:not() specificity hack)0.74 ms
StyleX0.76 ms
vanilla (hand-written ceiling)0.77 ms
Panda (recipe)0.85 ms
next-yak (css prop) foldStatic: false0.92 ms
tailwind-merge1.32 ms
Panda (css fn)1.36 ms
next-yak (styled API) foldStatic: false1.50 ms
Panda (style props)1.94 ms
styled-components2.19 ms
Goober2.24 ms
Emotion2.87 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 1.1.0). Lower is better.

Bamboo45 ms ±11%
StyleX46 ms ±1%
next-yak (css prop)46 ms ±48%
vanilla (hand-written ceiling)47 ms ±48%
StyleX without CSS layers (:not() specificity hack)47 ms ±48%
next-yak (css prop) foldStatic: false48 ms ±45%
Panda (recipe)51 ms ±5%
next-yak (styled API) foldStatic: false52 ms ±42%
Panda (css fn)55 ms ±1%
tailwind-merge58 ms ±4%
cnfast59 ms ±37%
Panda (style props)61 ms ±1%
Emotion75 ms ±30%
next-yak (styled API)85 ms ±26%
Goober90 ms ±24%
styled-components92 ms ±24%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (css prop)5.55 ms active(31.61 ms span)
next-yak (styled API)5.73 ms active(43.21 ms span)
StyleX without CSS layers (:not() specificity hack)5.81 ms active(39.89 ms span)
vanilla (hand-written ceiling)5.87 ms active(30.94 ms span)
next-yak (css prop) foldStatic: false5.98 ms active(29.59 ms span)
StyleX6.01 ms active(31.09 ms span)
Bamboo6.03 ms active(31.29 ms span)
next-yak (styled API) foldStatic: false6.20 ms active(28.46 ms span)
Panda (recipe)6.60 ms active(30.34 ms span)
Panda (css fn)6.72 ms active(29.79 ms span)
Panda (style props)7.72 ms active(35.44 ms span)
Emotion9.17 ms active(33.49 ms span)
tailwind-merge9.62 ms active(27.66 ms span)
Goober9.85 ms active(27.53 ms span)
styled-components10.17 ms active(41.84 ms span)
cnfast10.32 ms active(31.34 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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 ±16%
next-yak (styled API) foldStatic: false7 ms ±13%
Goober7 ms ±6%
next-yak (styled API)7 ms ±10%
StyleX7 ms ±5%
Panda (recipe)7 ms ±19%
next-yak (css prop)8 ms ±18%
StyleX without CSS layers (:not() specificity hack)8 ms ±11%
Panda (style props)8 ms ±5%
tailwind-merge8 ms ±1%
Bamboo8 ms ±12%
cnfast8 ms ±10%
Panda (css fn)8 ms ±6%
vanilla (hand-written ceiling)8 ms ±10%
next-yak (css prop) foldStatic: false8 ms ±13%
Emotion11 ms ±22%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Panda (recipe)0.59 ms active(34.02 ms span)
StyleX0.60 ms active(31.80 ms span)
next-yak (css prop)0.62 ms active(33.22 ms span)
tailwind-merge0.63 ms active(33.22 ms span)
vanilla (hand-written ceiling)0.63 ms active(33.18 ms span)
next-yak (css prop) foldStatic: false0.67 ms active(32.81 ms span)
cnfast0.71 ms active(32.81 ms span)
next-yak (styled API)0.71 ms active(35.17 ms span)
Bamboo0.72 ms active(32.57 ms span)
styled-components0.75 ms active(32.71 ms span)
Goober0.75 ms active(33.53 ms span)
Panda (style props)0.76 ms active(33.82 ms span)
Emotion0.77 ms active(33.48 ms span)
Panda (css fn)0.84 ms active(32.59 ms span)
StyleX without CSS layers (:not() specificity hack)0.90 ms active(32.21 ms span)
next-yak (styled API) foldStatic: false0.93 ms active(31.73 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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 (styled API)69 ms ±33%
next-yak (css prop)70 ms ±31%
StyleX without CSS layers (:not() specificity hack)71 ms ±2%
StyleX72 ms ±3%
vanilla (hand-written ceiling)72 ms ±32%
Bamboo72 ms ±32%
next-yak (css prop) foldStatic: false77 ms ±3%
Panda (recipe)78 ms ±1%
next-yak (styled API) foldStatic: false80 ms ±3%
cnfast82 ms ±2%
tailwind-merge83 ms ±2%
Panda (css fn)83 ms ±2%
Goober85 ms ±2%
styled-components86 ms ±2%
Panda (style props)89 ms ±2%
Emotion99 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (css prop) foldStatic: false12.29 ms active(29.08 ms span)
vanilla (hand-written ceiling)12.31 ms active(30.44 ms span)
StyleX12.55 ms active(40.78 ms span)
next-yak (styled API)12.72 ms active(28.51 ms span)
next-yak (styled API) foldStatic: false12.75 ms active(33.20 ms span)
StyleX without CSS layers (:not() specificity hack)13.35 ms active(33.40 ms span)
Panda (css fn)13.91 ms active(32.63 ms span)
next-yak (css prop)14.28 ms active(33.80 ms span)
Panda (style props)15.15 ms active(29.34 ms span)
Goober15.34 ms active(27.63 ms span)
Bamboo15.38 ms active(30.48 ms span)
tailwind-merge17.02 ms active(30.34 ms span)
Panda (recipe)17.20 ms active(27.96 ms span)
Emotion17.26 ms active(29.64 ms span)
cnfast17.43 ms active(26.17 ms span)
styled-components20.81 ms active(42.16 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)
BambooChrome7.9 ms1 recalc
Firefox9.3 ms
cnfastChrome6.1 ms1 recalc
Firefox8.0 ms
next-yak (css prop)Chrome8.0 ms1 recalc
Firefox8.1 ms
next-yak (css prop) foldStatic: falseChrome5.7 ms1 recalc
Firefox8.0 ms
Panda (css fn)Chrome6.5 ms1 recalc
Firefox9.0 ms
Panda (style props)Chrome6.9 ms1 recalc
Firefox8.3 ms
Panda (recipe)Chrome9.7 ms1 recalc
Firefox7.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome6.6 ms1 recalc
Firefox11.8 ms
StyleXChrome6.1 ms1 recalc
Firefox8.9 ms
vanilla (hand-written ceiling)Chrome6.3 ms1 recalc
Firefox7.8 ms
EmotionChrome7.1 ms1 recalc
Firefox8.6 ms
next-yak (styled API)Chrome6.6 ms1 recalc
Firefox8.0 ms
styled-componentsChrome11.9 ms1 recalc
Firefox9.0 ms
GooberChrome7.9 ms1 recalc
Firefox8.0 ms
tailwind-mergeChrome6.7 ms1 recalc
Firefox7.9 ms
next-yak (styled API) foldStatic: falseChrome6.3 ms1 recalc
Firefox9.2 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) foldStatic: false3,718 B(0.3 / 0.2 / 3.1 kB)
next-yak (css prop)3,737 B(0.3 / 0.2 / 3.1 kB)
vanilla (hand-written ceiling)3,789 B(0.0 / 0.5 / 3.2 kB)
next-yak (styled API)4,540 B(1.1 / 0.2 / 3.1 kB)
next-yak (styled API) foldStatic: false4,609 B(1.2 / 0.2 / 3.1 kB)
Panda (recipe)6,477 B(1.4 / 0.3 / 4.6 kB)
Bamboo6,736 B(1.9 / 0.3 / 4.4 kB)
Goober7,198 B(3.7 / 0.3 / 3.1 kB)
StyleX7,862 B(3.4 / 0.3 / 4.0 kB)
StyleX without CSS layers (:not() specificity hack)7,871 B(3.4 / 0.3 / 4.0 kB)
Panda (css fn)9,938 B(4.8 / 0.3 / 4.6 kB)
Panda (style props)14,311 B(9.1 / 0.3 / 4.6 kB)
tailwind-merge15,130 B(10.4 / 0.4 / 4.0 kB)
cnfast16,093 B(11.3 / 0.4 / 4.0 kB)
Emotion16,517 B(12.7 / 0.3 / 3.1 kB)
styled-components17,015 B(13.1 / 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.03.06.19.1121005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)Panda (recipe)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
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 depth one there is no chain to collapse, so the case prices each library's floor cost per element: next-yak compiles to a bare tagged element and leads, while the runtime lanes already give up 2 to 5× before any wrapping begins.

Read with the Examples compose-3 and compose-6 shown below, which wrap this same button two and five more times. next-yak runs the three depths at 2.24M, 1.40M and 1.48M renders/s: the drop from one to three buys back a __yak_button component that depth one does not need, and depth six is free because six levels concatenate into that same single component. vanilla pays once per level instead, 1.92M to 1.37M to 919,000.

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 (styled API)2,237,552 r/s ±3%(0.45 ms / 1k elems)
StyleX2,008,871 r/s ±2%(0.50 ms / 1k elems)
vanilla (hand-written ceiling)1,918,465 r/s ±4%(0.52 ms / 1k elems)
Bamboo1,908,244 r/s ±3%(0.52 ms / 1k elems)
next-yak (css prop)1,907,640 r/s ±1%(0.52 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,872,221 r/s ±2%(0.53 ms / 1k elems)
cnfast1,857,441 r/s ±2%(0.54 ms / 1k elems)
tailwind-merge1,853,139 r/s ±3%(0.54 ms / 1k elems)
next-yak (css prop) foldStatic: false1,669,566 r/s ±2%(0.60 ms / 1k elems)
next-yak (styled API) foldStatic: false1,467,261 r/s ±1%(0.68 ms / 1k elems)
Panda (css fn)1,115,449 r/s ±4%(0.90 ms / 1k elems)
Goober1,103,144 r/s ±1%(0.91 ms / 1k elems)
styled-components787,221 r/s ±6%(1.27 ms / 1k elems)
Panda (style props)656,204 r/s ±2%(1.52 ms / 1k elems)
Emotion459,286 r/s ±1%(2.18 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 (styled API)1,831 req/s ±0%
next-yak (css prop)1,639 req/s ±0%
vanilla (hand-written ceiling)1,604 req/s ±1%
StyleX without CSS layers (:not() specificity hack)1,434 req/s ±0%
StyleX1,418 req/s ±1%
Bamboo1,380 req/s ±0%
cnfast1,356 req/s ±0%
next-yak (styled API) foldStatic: false1,239 req/s ±0%
next-yak (css prop) foldStatic: false1,235 req/s ±4%
tailwind-merge1,234 req/s ±1%
Goober916 req/s ±0%
styled-components726 req/s ±1%
Panda (css fn)639 req/s ±1%
Panda (style props)498 req/s ±1%
Emotion460 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 1.1.0): 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 (styled API)0.51 ms
Bamboo0.60 ms
next-yak (css prop)0.64 ms
StyleX0.65 ms
StyleX without CSS layers (:not() specificity hack)0.66 ms
cnfast0.70 ms
vanilla (hand-written ceiling)0.70 ms
tailwind-merge0.73 ms
next-yak (css prop) foldStatic: false0.76 ms
next-yak (styled API) foldStatic: false0.81 ms
Goober0.96 ms
Panda (css fn)1.06 ms
styled-components1.23 ms
Panda (style props)1.48 ms
Emotion2.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 1.1.0). Lower is better.

next-yak (styled API)43 ms ±51%
next-yak (css prop)44 ms ±4%
StyleX45 ms ±50%
Bamboo46 ms ±49%
StyleX without CSS layers (:not() specificity hack)46 ms ±48%
next-yak (css prop) foldStatic: false47 ms ±47%
next-yak (styled API) foldStatic: false48 ms ±47%
Panda (css fn)51 ms ±45%
cnfast56 ms ±38%
Panda (style props)59 ms ±2%
vanilla (hand-written ceiling)88 ms ±26%
Goober95 ms ±23%
styled-components97 ms ±23%
tailwind-merge98 ms ±24%
Emotion110 ms ±20%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (styled API)5.41 ms active(30.84 ms span)
next-yak (styled API) foldStatic: false5.62 ms active(28.16 ms span)
next-yak (css prop)5.78 ms active(32.57 ms span)
StyleX without CSS layers (:not() specificity hack)5.84 ms active(33.38 ms span)
StyleX5.95 ms active(34.42 ms span)
next-yak (css prop) foldStatic: false6.09 ms active(27.72 ms span)
vanilla (hand-written ceiling)6.11 ms active(39.30 ms span)
Bamboo6.17 ms active(27.93 ms span)
Panda (css fn)6.54 ms active(32.28 ms span)
Panda (style props)7.13 ms active(31.04 ms span)
Goober7.53 ms active(27.70 ms span)
Emotion7.75 ms active(29.81 ms span)
styled-components8.94 ms active(28.93 ms span)
cnfast9.67 ms active(28.71 ms span)
tailwind-merge9.67 ms active(26.69 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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.

StyleX7 ms ±7%
Panda (style props)7 ms ±9%
next-yak (css prop)7 ms ±11%
cnfast8 ms ±5%
next-yak (styled API)8 ms ±3%
next-yak (css prop) foldStatic: false8 ms ±18%
Panda (css fn)8 ms ±8%
Bamboo8 ms ±7%
styled-components8 ms ±11%
tailwind-merge8 ms ±11%
StyleX without CSS layers (:not() specificity hack)8 ms ±18%
Goober8 ms ±6%
vanilla (hand-written ceiling)8 ms ±13%
next-yak (styled API) foldStatic: false8 ms ±14%
Emotion9 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (styled API) foldStatic: false0.57 ms active(33.56 ms span)
cnfast0.61 ms active(33.57 ms span)
vanilla (hand-written ceiling)0.63 ms active(31.61 ms span)
Bamboo0.66 ms active(33.54 ms span)
StyleX0.68 ms active(33.27 ms span)
next-yak (css prop)0.68 ms active(31.95 ms span)
next-yak (css prop) foldStatic: false0.70 ms active(32.88 ms span)
next-yak (styled API)0.70 ms active(32.97 ms span)
Emotion0.70 ms active(34.09 ms span)
StyleX without CSS layers (:not() specificity hack)0.74 ms active(33.61 ms span)
Panda (style props)0.74 ms active(32.49 ms span)
Panda (css fn)0.75 ms active(32.07 ms span)
tailwind-merge0.84 ms active(33.19 ms span)
Goober0.88 ms active(32.89 ms span)
styled-components1.27 ms active(34.47 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)26 ms ±89%
next-yak (styled API)67 ms ±2%
Bamboo68 ms ±32%
StyleX without CSS layers (:not() specificity hack)68 ms ±33%
next-yak (css prop)68 ms ±1%
StyleX69 ms ±2%
next-yak (css prop) foldStatic: false71 ms ±5%
next-yak (styled API) foldStatic: false72 ms ±2%
Panda (css fn)76 ms ±2%
tailwind-merge81 ms ±3%
cnfast82 ms ±2%
Panda (style props)85 ms ±1%
Goober92 ms ±3%
styled-components93 ms ±1%
Emotion101 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)11.89 ms active(27.84 ms span)
StyleX without CSS layers (:not() specificity hack)12.22 ms active(25.84 ms span)
StyleX12.23 ms active(32.54 ms span)
next-yak (styled API)12.51 ms active(32.37 ms span)
Bamboo12.70 ms active(32.42 ms span)
Panda (css fn)12.77 ms active(30.35 ms span)
next-yak (styled API) foldStatic: false13.02 ms active(30.54 ms span)
Panda (style props)14.09 ms active(31.40 ms span)
Goober14.11 ms active(31.21 ms span)
Emotion14.71 ms active(33.81 ms span)
styled-components14.79 ms active(30.10 ms span)
next-yak (css prop) foldStatic: false15.72 ms active(31.64 ms span)
tailwind-merge16.38 ms active(31.93 ms span)
next-yak (css prop)18.04 ms active(34.63 ms span)
cnfast19.49 ms active(35.71 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)
BambooChrome6.4 ms1 recalc
Firefox9.0 ms
cnfastChrome7.9 ms1 recalc
Firefox8.0 ms
EmotionChrome6.6 ms1 recalc
Firefox8.0 ms
GooberChrome6.9 ms1 recalc
Firefox9.0 ms
next-yak (css prop)Chrome11.6 ms1 recalc
Firefox7.0 ms
Panda (css fn)Chrome6.3 ms1 recalc
Firefox8.1 ms
Panda (style props)Chrome6.4 ms1 recalc
Firefox8.0 ms
styled-componentsChrome7.3 ms1 recalc
Firefox8.9 ms
tailwind-mergeChrome6.4 ms1 recalc
Firefox28.1 ms
vanilla (hand-written ceiling)Chrome6.0 ms1 recalc
Firefox8.0 ms
StyleXChrome6.1 ms1 recalc
Firefox9.8 ms
next-yak (css prop) foldStatic: falseChrome9.3 ms1 recalc
Firefox8.7 ms
StyleX without CSS layers (:not() specificity hack)Chrome6.2 ms1 recalc
Firefox8.0 ms
next-yak (styled API) foldStatic: falseChrome6.2 ms1 recalc
Firefox9.0 ms
next-yak (styled API)Chrome6.5 ms1 recalc
Firefox10.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
vanilla (hand-written ceiling)2,700 B(0.0 / 0.3 / 2.4 kB)
next-yak (css prop) foldStatic: false3,005 B(0.3 / 0.1 / 2.5 kB)
next-yak (css prop)3,032 B(0.3 / 0.1 / 2.5 kB)
next-yak (styled API)3,833 B(1.1 / 0.1 / 2.5 kB)
next-yak (styled API) foldStatic: false3,895 B(1.2 / 0.1 / 2.5 kB)
Bamboo4,981 B(1.9 / 0.2 / 2.8 kB)
StyleX without CSS layers (:not() specificity hack)6,441 B(3.4 / 0.2 / 2.7 kB)
StyleX6,444 B(3.4 / 0.2 / 2.7 kB)
Goober6,533 B(3.7 / 0.1 / 2.6 kB)
Panda (css fn)8,231 B(4.8 / 0.2 / 3.1 kB)
Panda (style props)12,604 B(9.1 / 0.2 / 3.1 kB)
tailwind-merge13,797 B(10.4 / 0.2 / 2.9 kB)
cnfast14,760 B(11.3 / 0.2 / 2.9 kB)
Emotion15,701 B(12.7 / 0.2 / 2.4 kB)
styled-components16,256 B(13.1 / 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.02.14.36.48.61005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
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.

next-yak concatenates the three-level chain into one __yak_button at build time and leads the field, while the css prop keeps a merge at every level and finishes below hand-written vanilla.

Bracketed by the Examples compose-1 and compose-6. next-yak runs the three depths at 2.24M, 1.40M and 1.48M renders/s, so the cost lands entirely on the first wrapper and everything after it is free. vanilla pays once per level, 1.92M to 1.37M to 919,000, and next-yak-css pays once per level too, 1.91M to 983,000 to 600,000.

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 (styled API)1,402,359 r/s ±2%(0.71 ms / 1k elems)
vanilla (hand-written ceiling)1,367,445 r/s ±5%(0.73 ms / 1k elems)
cnfast1,288,590 r/s ±2%(0.78 ms / 1k elems)
next-yak (styled API) foldStatic: false1,283,285 r/s ±1%(0.78 ms / 1k elems)
Bamboo1,251,760 r/s ±3%(0.80 ms / 1k elems)
StyleX1,134,377 r/s ±2%(0.88 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,066,903 r/s ±5%(0.94 ms / 1k elems)
next-yak (css prop)983,163 r/s ±3%(1.02 ms / 1k elems)
next-yak (css prop) foldStatic: false961,269 r/s ±2%(1.04 ms / 1k elems)
styled-components793,283 r/s ±4%(1.26 ms / 1k elems)
Panda (style props)782,064 r/s ±2%(1.28 ms / 1k elems)
tailwind-merge765,038 r/s ±2%(1.31 ms / 1k elems)
Goober549,275 r/s ±3%(1.82 ms / 1k elems)
Panda (css fn)539,532 r/s ±3%(1.85 ms / 1k elems)
Emotion436,713 r/s ±3%(2.29 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 (styled API)1,246 req/s ±0%
vanilla (hand-written ceiling)1,149 req/s ±0%
next-yak (styled API) foldStatic: false1,095 req/s ±0%
cnfast887 req/s ±0%
next-yak (css prop)865 req/s ±0%
Bamboo860 req/s ±0%
StyleX845 req/s ±1%
StyleX without CSS layers (:not() specificity hack)839 req/s ±0%
next-yak (css prop) foldStatic: false791 req/s ±3%
styled-components650 req/s ±2%
Panda (style props)525 req/s ±1%
tailwind-merge519 req/s ±3%
Goober460 req/s ±0%
Emotion428 req/s ±0%
Panda (css fn)293 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 1.1.0): 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 (styled API)0.80 ms
vanilla (hand-written ceiling)0.83 ms
Bamboo0.84 ms
next-yak (styled API) foldStatic: false0.84 ms
cnfast0.85 ms
StyleX without CSS layers (:not() specificity hack)1.00 ms
StyleX1.08 ms
next-yak (css prop) foldStatic: false1.27 ms
styled-components1.42 ms
next-yak (css prop)1.49 ms
Panda (style props)1.51 ms
Goober1.86 ms
Panda (css fn)2.05 ms
tailwind-merge2.15 ms
Emotion2.42 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 1.1.0). Lower is better.

Bamboo47 ms ±2%
next-yak (styled API)47 ms ±48%
StyleX50 ms ±43%
StyleX without CSS layers (:not() specificity hack)51 ms ±43%
vanilla (hand-written ceiling)52 ms ±44%
Panda (css fn)55 ms ±2%
Panda (style props)57 ms ±2%
tailwind-merge58 ms ±3%
cnfast58 ms ±3%
Emotion72 ms ±31%
next-yak (styled API) foldStatic: false90 ms ±23%
next-yak (css prop)92 ms ±24%
next-yak (css prop) foldStatic: false93 ms ±25%
Goober98 ms ±23%
styled-components98 ms ±22%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (css prop) foldStatic: false5.82 ms active(31.55 ms span)
vanilla (hand-written ceiling)5.83 ms active(29.55 ms span)
Bamboo5.84 ms active(28.75 ms span)
next-yak (styled API)5.87 ms active(35.88 ms span)
next-yak (styled API) foldStatic: false5.87 ms active(28.79 ms span)
StyleX without CSS layers (:not() specificity hack)6.19 ms active(28.39 ms span)
next-yak (css prop)6.28 ms active(30.82 ms span)
Panda (css fn)6.37 ms active(32.89 ms span)
StyleX6.40 ms active(31.38 ms span)
Panda (style props)7.42 ms active(32.45 ms span)
Emotion8.27 ms active(25.55 ms span)
Goober8.30 ms active(36.22 ms span)
styled-components8.49 ms active(31.21 ms span)
cnfast9.77 ms active(31.71 ms span)
tailwind-merge10.36 ms active(33.61 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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) foldStatic: false7 ms ±11%
Bamboo7 ms ±10%
next-yak (css prop)7 ms ±8%
next-yak (styled API) foldStatic: false7 ms ±13%
StyleX7 ms ±8%
StyleX without CSS layers (:not() specificity hack)8 ms ±18%
next-yak (styled API)8 ms ±12%
Panda (css fn)8 ms ±7%
styled-components8 ms ±9%
vanilla (hand-written ceiling)8 ms ±22%
cnfast8 ms ±5%
tailwind-merge8 ms ±9%
Panda (style props)8 ms ±11%
Emotion9 ms ±10%
Goober10 ms ±18%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)0.59 ms active(32.81 ms span)
next-yak (styled API)0.65 ms active(33.99 ms span)
Emotion0.66 ms active(32.22 ms span)
Bamboo0.67 ms active(33.27 ms span)
next-yak (css prop) foldStatic: false0.73 ms active(33.92 ms span)
next-yak (styled API) foldStatic: false0.74 ms active(32.20 ms span)
styled-components0.76 ms active(32.78 ms span)
StyleX0.77 ms active(32.55 ms span)
cnfast0.80 ms active(32.91 ms span)
StyleX without CSS layers (:not() specificity hack)0.80 ms active(33.49 ms span)
Panda (css fn)0.99 ms active(33.43 ms span)
tailwind-merge1.13 ms active(33.58 ms span)
next-yak (css prop)1.24 ms active(32.94 ms span)
Goober1.28 ms active(33.28 ms span)
Panda (style props)1.49 ms active(32.83 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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 (styled API)72 ms ±2%
next-yak (styled API) foldStatic: false73 ms ±3%
vanilla (hand-written ceiling)73 ms ±2%
Bamboo74 ms ±3%
next-yak (css prop) foldStatic: false75 ms ±1%
StyleX without CSS layers (:not() specificity hack)76 ms ±3%
next-yak (css prop)76 ms ±2%
StyleX76 ms ±3%
Panda (css fn)82 ms ±4%
Panda (style props)82 ms ±3%
cnfast84 ms ±1%
tailwind-merge84 ms ±1%
styled-components95 ms ±1%
Goober98 ms ±1%
Emotion105 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (styled API) foldStatic: false12.17 ms active(30.49 ms span)
next-yak (css prop) foldStatic: false12.27 ms active(33.51 ms span)
next-yak (styled API)12.82 ms active(34.18 ms span)
StyleX12.98 ms active(30.29 ms span)
vanilla (hand-written ceiling)13.10 ms active(30.95 ms span)
Panda (css fn)13.25 ms active(34.17 ms span)
StyleX without CSS layers (:not() specificity hack)13.32 ms active(31.51 ms span)
Bamboo13.60 ms active(29.51 ms span)
styled-components13.81 ms active(32.12 ms span)
next-yak (css prop)13.84 ms active(29.21 ms span)
Goober14.20 ms active(30.03 ms span)
Panda (style props)15.15 ms active(29.46 ms span)
Emotion15.55 ms active(27.80 ms span)
tailwind-merge16.63 ms active(25.91 ms span)
cnfast17.59 ms active(31.87 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)
cnfastChrome6.6 ms1 recalc
Firefox8.8 ms
EmotionChrome6.7 ms1 recalc
Firefox8.1 ms
GooberChrome7.2 ms1 recalc
Firefox8.1 ms
next-yak (styled API)Chrome6.7 ms1 recalc
Firefox7.8 ms
next-yak (css prop)Chrome6.9 ms1 recalc
Firefox8.0 ms
next-yak (css prop) foldStatic: falseChrome5.8 ms1 recalc
Firefox8.1 ms
next-yak (styled API) foldStatic: falseChrome5.9 ms1 recalc
Firefox8.1 ms
Panda (css fn)Chrome6.5 ms1 recalc
Firefox7.9 ms
styled-componentsChrome6.4 ms1 recalc
Firefox8.3 ms
tailwind-mergeChrome6.4 ms1 recalc
Firefox8.0 ms
BambooChrome7.0 ms1 recalc
Firefox9.3 ms
Panda (style props)Chrome7.5 ms1 recalc
Firefox10.9 ms
StyleX without CSS layers (:not() specificity hack)Chrome6.6 ms1 recalc
Firefox8.1 ms
StyleXChrome6.5 ms1 recalc
Firefox8.9 ms
vanilla (hand-written ceiling)Chrome6.4 ms1 recalc
Firefox9.3 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)3,044 B(0.0 / 0.4 / 2.6 kB)
next-yak (css prop) foldStatic: false3,129 B(0.3 / 0.2 / 2.6 kB)
next-yak (css prop)3,151 B(0.3 / 0.2 / 2.6 kB)
next-yak (styled API)3,958 B(1.1 / 0.2 / 2.6 kB)
next-yak (styled API) foldStatic: false4,012 B(1.2 / 0.2 / 2.6 kB)
Bamboo5,375 B(1.9 / 0.2 / 3.1 kB)
Goober6,613 B(3.7 / 0.2 / 2.6 kB)
StyleX6,666 B(3.4 / 0.3 / 2.9 kB)
StyleX without CSS layers (:not() specificity hack)6,669 B(3.4 / 0.3 / 2.9 kB)
Panda (css fn)8,450 B(4.8 / 0.3 / 3.2 kB)
Panda (style props)12,770 B(9.1 / 0.2 / 3.2 kB)
tailwind-merge14,080 B(10.4 / 0.3 / 3.1 kB)
cnfast15,043 B(11.3 / 0.3 / 3.1 kB)
Emotion15,753 B(12.7 / 0.2 / 2.4 kB)
styled-components16,514 B(13.1 / 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.02.44.97.39.81005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)
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.

Six levels cost next-yak nothing over three, because the whole chain compiles to one __yak_button holding six classes in a single string, while every lane that merges at render pays six times.

The deep end of the trio, with the Examples compose-1 and compose-3. next-yak costs 2.24M, 1.40M and 1.48M renders/s across the three depths, so everything after the first wrapper is free and the chain concatenates into one component whatever its depth. Every other lane pays per level, most steeply panda at 1.12M, 540,000 and 310,000.

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 (styled API)1,475,925 r/s ±2%(0.68 ms / 1k elems)
next-yak (styled API) foldStatic: false1,181,392 r/s ±2%(0.85 ms / 1k elems)
vanilla (hand-written ceiling)919,188 r/s ±3%(1.09 ms / 1k elems)
cnfast903,071 r/s ±2%(1.11 ms / 1k elems)
Bamboo886,165 r/s ±3%(1.13 ms / 1k elems)
StyleX799,254 r/s ±2%(1.25 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)764,380 r/s ±3%(1.31 ms / 1k elems)
styled-components720,937 r/s ±2%(1.39 ms / 1k elems)
next-yak (css prop)600,075 r/s ±2%(1.67 ms / 1k elems)
next-yak (css prop) foldStatic: false590,217 r/s ±2%(1.69 ms / 1k elems)
Panda (style props)580,327 r/s ±3%(1.72 ms / 1k elems)
tailwind-merge419,133 r/s ±3%(2.39 ms / 1k elems)
Emotion334,658 r/s ±3%(2.99 ms / 1k elems)
Goober328,035 r/s ±2%(3.05 ms / 1k elems)
Panda (css fn)309,993 r/s ±8%(3.23 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 (styled API)1,206 req/s ±0%
next-yak (styled API) foldStatic: false980 req/s ±0%
vanilla (hand-written ceiling)840 req/s ±0%
cnfast658 req/s ±0%
StyleX631 req/s ±0%
StyleX without CSS layers (:not() specificity hack)628 req/s ±3%
styled-components583 req/s ±3%
Bamboo581 req/s ±0%
next-yak (css prop)551 req/s ±0%
next-yak (css prop) foldStatic: false453 req/s ±3%
Panda (style props)428 req/s ±1%
Emotion335 req/s ±0%
tailwind-merge323 req/s ±1%
Goober261 req/s ±0%
Panda (css fn)191 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 1.1.0): 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 (styled API)0.81 ms
next-yak (styled API) foldStatic: false0.89 ms
vanilla (hand-written ceiling)1.24 ms
Bamboo1.25 ms
cnfast1.25 ms
StyleX1.52 ms
StyleX without CSS layers (:not() specificity hack)1.58 ms
styled-components1.66 ms
next-yak (css prop) foldStatic: false1.74 ms
Panda (style props)1.83 ms
tailwind-merge2.62 ms
next-yak (css prop)2.84 ms
Emotion3.06 ms
Panda (css fn)3.48 ms
Goober4.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 1.1.0). Lower is better.

vanilla (hand-written ceiling)50 ms ±44%
Bamboo50 ms ±2%
next-yak (css prop)54 ms ±40%
next-yak (css prop) foldStatic: false54 ms ±39%
StyleX54 ms ±42%
StyleX without CSS layers (:not() specificity hack)55 ms ±41%
cnfast60 ms ±36%
Panda (style props)60 ms ±3%
tailwind-merge62 ms ±2%
Panda (css fn)62 ms ±2%
Emotion75 ms ±29%
next-yak (styled API)88 ms ±25%
next-yak (styled API) foldStatic: false89 ms ±25%
styled-components96 ms ±2%
Goober107 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (styled API) foldStatic: false5.79 ms active(28.14 ms span)
next-yak (styled API)6.00 ms active(34.91 ms span)
vanilla (hand-written ceiling)6.05 ms active(30.52 ms span)
StyleX6.17 ms active(32.44 ms span)
next-yak (css prop) foldStatic: false6.25 ms active(36.00 ms span)
next-yak (css prop)6.38 ms active(26.84 ms span)
StyleX without CSS layers (:not() specificity hack)6.39 ms active(28.50 ms span)
Bamboo6.41 ms active(28.45 ms span)
Panda (css fn)6.84 ms active(32.39 ms span)
Panda (style props)7.56 ms active(35.55 ms span)
Emotion8.25 ms active(32.13 ms span)
styled-components8.33 ms active(32.44 ms span)
Goober8.38 ms active(33.51 ms span)
cnfast10.00 ms active(28.64 ms span)
tailwind-merge10.47 ms active(28.14 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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.

Bamboo7 ms ±8%
vanilla (hand-written ceiling)7 ms ±5%
cnfast7 ms ±8%
StyleX8 ms ±9%
next-yak (styled API)8 ms ±9%
next-yak (styled API) foldStatic: false8 ms ±14%
StyleX without CSS layers (:not() specificity hack)8 ms ±11%
tailwind-merge8 ms ±12%
styled-components8 ms ±12%
next-yak (css prop) foldStatic: false9 ms ±14%
next-yak (css prop)9 ms ±13%
Panda (style props)9 ms ±7%
Emotion11 ms ±14%
Panda (css fn)15 ms ±14%
Goober23 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
cnfast0.45 ms active(32.05 ms span)
Panda (css fn)0.55 ms active(35.13 ms span)
next-yak (css prop) foldStatic: false0.61 ms active(32.94 ms span)
Bamboo0.63 ms active(32.95 ms span)
next-yak (styled API)0.64 ms active(32.45 ms span)
StyleX without CSS layers (:not() specificity hack)0.64 ms active(32.78 ms span)
next-yak (styled API) foldStatic: false0.69 ms active(37.69 ms span)
StyleX0.74 ms active(34.54 ms span)
Emotion0.76 ms active(33.36 ms span)
vanilla (hand-written ceiling)0.77 ms active(33.78 ms span)
Panda (style props)0.77 ms active(32.45 ms span)
Goober1.04 ms active(33.52 ms span)
next-yak (css prop)1.11 ms active(32.97 ms span)
styled-components1.22 ms active(33.45 ms span)
tailwind-merge1.35 ms active(31.93 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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 (styled API)71 ms ±33%
next-yak (styled API) foldStatic: false73 ms ±2%
vanilla (hand-written ceiling)76 ms ±3%
Bamboo76 ms ±3%
StyleX78 ms ±2%
StyleX without CSS layers (:not() specificity hack)79 ms ±2%
next-yak (css prop)79 ms ±2%
next-yak (css prop) foldStatic: false81 ms ±2%
cnfast87 ms ±2%
Panda (style props)87 ms ±3%
tailwind-merge88 ms ±2%
Panda (css fn)89 ms ±3%
styled-components96 ms ±2%
Goober108 ms ±1%
Emotion109 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)12.60 ms active(33.51 ms span)
next-yak (styled API) foldStatic: false12.68 ms active(30.56 ms span)
Bamboo12.94 ms active(30.96 ms span)
StyleX without CSS layers (:not() specificity hack)13.03 ms active(33.48 ms span)
next-yak (css prop) foldStatic: false13.09 ms active(30.34 ms span)
Panda (css fn)13.34 ms active(26.41 ms span)
next-yak (css prop)13.47 ms active(35.86 ms span)
next-yak (styled API)13.70 ms active(31.12 ms span)
StyleX14.52 ms active(26.83 ms span)
Panda (style props)14.96 ms active(27.54 ms span)
Goober15.49 ms active(33.52 ms span)
Emotion15.93 ms active(28.59 ms span)
styled-components16.16 ms active(34.06 ms span)
tailwind-merge17.08 ms active(40.24 ms span)
cnfast17.88 ms active(32.03 ms span)

Chrome-profiled first-span anatomy (web-performance-debugger 1.1.0); 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)
cnfastChrome6.3 ms1 recalc
Firefox8.0 ms
EmotionChrome7.1 ms1 recalc
Firefox9.0 ms
GooberChrome7.5 ms1 recalc
Firefox7.9 ms
next-yak (css prop)Chrome6.6 ms1 recalc
Firefox8.0 ms
next-yak (css prop) foldStatic: falseChrome6.4 ms1 recalc
Firefox10.9 ms
next-yak (styled API) foldStatic: falseChrome6.1 ms1 recalc
Firefox8.0 ms
Panda (css fn)Chrome6.3 ms1 recalc
Firefox7.0 ms
Panda (style props)Chrome6.7 ms1 recalc
Firefox7.8 ms
styled-componentsChrome8.0 ms1 recalc
Firefox7.9 ms
StyleXChrome7.7 ms1 recalc
Firefox7.9 ms
tailwind-mergeChrome6.2 ms1 recalc
Firefox8.1 ms
BambooChrome6.0 ms1 recalc
Firefox8.7 ms
StyleX without CSS layers (:not() specificity hack)Chrome6.2 ms1 recalc
Firefox9.0 ms
next-yak (styled API)Chrome6.7 ms1 recalc
Firefox8.9 ms
vanilla (hand-written ceiling)Chrome6.0 ms1 recalc
Firefox10.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
vanilla (hand-written ceiling)3,075 B(0.0 / 0.4 / 2.6 kB)
next-yak (css prop) foldStatic: false3,286 B(0.3 / 0.2 / 2.7 kB)
next-yak (css prop)3,304 B(0.3 / 0.2 / 2.7 kB)
next-yak (styled API)4,112 B(1.1 / 0.2 / 2.7 kB)
next-yak (styled API) foldStatic: false4,174 B(1.2 / 0.2 / 2.7 kB)
Bamboo5,656 B(1.9 / 0.3 / 3.4 kB)
StyleX6,690 B(3.4 / 0.3 / 2.9 kB)
StyleX without CSS layers (:not() specificity hack)6,692 B(3.4 / 0.3 / 2.9 kB)
Goober6,926 B(3.7 / 0.2 / 2.8 kB)
Panda (css fn)9,204 B(4.8 / 0.3 / 3.9 kB)
Panda (style props)12,770 B(9.1 / 0.2 / 3.2 kB)
tailwind-merge14,079 B(10.4 / 0.3 / 3.1 kB)
cnfast15,042 B(11.3 / 0.3 / 3.1 kB)
Emotion15,773 B(12.7 / 0.2 / 2.4 kB)
styled-components16,787 B(13.1 / 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.04.59.114181005001,0002,0004,000
BamboocnfastEmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXtailwind-mergevanilla (hand-written ceiling)

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)71 msnone · warm 72 ms
cnfast73 msutility · warm 75 ms
Goober73 msruntime · warm 72 ms
Emotion74 msruntime · warm 73 ms
styled-components74 msruntime · warm 77 ms
tailwind-merge87 msutility · warm 74 ms
next-yak (css prop)109 msextracted · warm 111 ms
next-yak (styled API)110 msextracted · warm 107 ms
next-yak (styled API) foldStatic: false111 msextracted · warm 110 ms
next-yak (css prop) foldStatic: false113 msextracted · warm 110 ms
StyleX without CSS layers (:not() specificity hack)321 msatomic · warm 335 ms
Panda (recipe)328 msatomic · warm 304 ms
StyleX332 msatomic · warm 329 ms
Panda (css fn)351 msatomic · warm 340 ms
Panda (style props)395 msatomic · warm 335 ms
Bamboo2,029 msatomic · warm 1,707 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.