CSS-in-JS benchmarks

One set of components, built 23 different ways and measured on identical workloads. Every version renders the same pixels, so the numbers compare by construction, not by claim.

23 styling techniques13 workloadsproduction builds · median of repeated runs
Show source and preview
Technologies23 / 23 shown·
Benchmarks10 / 10 shown·
Server
Browser
Size & build
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.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

vanilla Solid (hand-written ceiling)531,362 r/s ±3%(0.14 ms / 1k elems)
Plumeria on Solid487,363 r/s ±17%(0.15 ms / 1k elems)
cn (shadcn-ui) on Solid450,942 r/s ±12%(0.16 ms / 1k elems)
@yak/solid (styled API, Solid 2)351,862 r/s ±13%(0.21 ms / 1k elems)
StyleX on Solid217,594 r/s ±4%(0.34 ms / 1k elems)
vanilla (hand-written ceiling)143,949 r/s ±2%(0.51 ms / 1k elems)
Plumeria (classStyle prop)141,013 r/s ±1%(0.53 ms / 1k elems)
StyleX128,766 r/s ±3%(0.58 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)128,651 r/s ±3%(0.58 ms / 1k elems)
cn (shadcn-ui)121,252 r/s ±1%(0.61 ms / 1k elems)
next-yak (styled API)119,660 r/s ±1%(0.62 ms / 1k elems)
next-yak (css prop) foldStatic: false111,863 r/s ±3%(0.66 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false105,047 r/s ±2%(0.71 ms / 1k elems)
Bamboo103,789 r/s ±1%(0.71 ms / 1k elems)
next-yak (css prop)94,474 r/s ±2%(0.78 ms / 1k elems)
tailwind-merge88,012 r/s ±1%(0.84 ms / 1k elems)
next-yak (styled API) foldStatic: false64,916 r/s ±1%(1.14 ms / 1k elems)
Panda (css fn)54,461 r/s ±1%(1.36 ms / 1k elems)
Goober38,495 r/s ±1%(1.92 ms / 1k elems)
styled-components38,251 r/s ±1%(1.94 ms / 1k elems)
Panda (style props)24,599 r/s ±1%(3.01 ms / 1k elems)
Emotion21,076 r/s ±2%(3.51 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

vanilla Solid (hand-written ceiling)494 req/s ±0%
@yak/solid (styled API, Solid 2)402 req/s ±1%
Plumeria on Solid285 req/s ±0%
vanilla (hand-written ceiling)224 req/s ±0%
next-yak (css prop)216 req/s ±0%
StyleX on Solid213 req/s ±1%
next-yak (styled API)202 req/s ±0%
next-yak (css prop) foldStatic: false183 req/s ±2%
cn (shadcn-ui) on Solid182 req/s ±0%
@yak/solid (styled API, Solid 2) foldStatic: false181 req/s ±0%
StyleX without CSS layers (:not() specificity hack)165 req/s ±0%
StyleX164 req/s ±2%
Plumeria (classStyle prop)162 req/s ±3%
next-yak (styled API) foldStatic: false131 req/s ±0%
cn (shadcn-ui)116 req/s ±1%
Bamboo106 req/s ±1%
tailwind-merge103 req/s ±0%
Goober83 req/s ±0%
styled-components77 req/s ±2%
Panda (css fn)74 req/s ±1%
Emotion57 req/s ±0%
Panda (style props)45 req/s ±0%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
Plumeria on Solid1.54 ms
vanilla Solid (hand-written ceiling)1.61 ms
cn (shadcn-ui) on Solid1.73 ms
@yak/solid (styled API, Solid 2)2.39 ms
StyleX on Solid2.67 ms
vanilla (hand-written ceiling)3.24 ms
Plumeria (classStyle prop)3.39 ms
next-yak (css prop)3.84 ms
StyleX without CSS layers (:not() specificity hack)4.23 ms
next-yak (styled API)4.49 ms
StyleX4.62 ms
@yak/solid (styled API, Solid 2) foldStatic: false4.64 ms
cn (shadcn-ui)4.98 ms
next-yak (css prop) foldStatic: false5.60 ms
tailwind-merge6.10 ms
Bamboo6.10 ms
next-yak (styled API) foldStatic: false8.02 ms
Panda (css fn)9.78 ms
Goober12.40 ms
styled-components12.96 ms
Panda (style props)20.38 ms
Emotion21.93 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

Plumeria (classStyle prop)86 ms ±3%
vanilla (hand-written ceiling)89 ms ±4%
Bamboo90 ms ±4%
next-yak (css prop)94 ms ±5%
StyleX97 ms ±2%
StyleX without CSS layers (:not() specificity hack)97 ms ±6%
cn (shadcn-ui)99 ms ±3%
next-yak (styled API)101 ms ±3%
next-yak (css prop) foldStatic: false101 ms ±4%
Panda (css fn)113 ms ±2%
tailwind-merge113 ms ±4%
next-yak (styled API) foldStatic: false122 ms ±4%
Plumeria on Solid133 ms ±4%
vanilla Solid (hand-written ceiling)136 ms ±4%
styled-components147 ms ±4%
cn (shadcn-ui) on Solid156 ms ±4%
Goober162 ms ±3%
@yak/solid (styled API, Solid 2)186 ms ±2%
StyleX on Solid188 ms ±4%
Emotion209 ms ±4%
Panda (style props)211 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false323 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Bamboo16.39 ms active(37.98 ms span)
next-yak (css prop)17.26 ms active(37.27 ms span)
StyleX17.28 ms active(29.90 ms span)
next-yak (styled API)17.97 ms active(34.91 ms span)
vanilla (hand-written ceiling)18.74 ms active(31.39 ms span)
Plumeria (classStyle prop)18.83 ms active(41.00 ms span)
StyleX without CSS layers (:not() specificity hack)18.99 ms active(43.47 ms span)
next-yak (styled API) foldStatic: false20.58 ms active(38.22 ms span)
@yak/solid (styled API, Solid 2)21.22 ms active(21.26 ms span)
Panda (css fn)21.57 ms active(27.85 ms span)
next-yak (css prop) foldStatic: false21.73 ms active(30.24 ms span)
cn (shadcn-ui)22.00 ms active(33.38 ms span)
vanilla Solid (hand-written ceiling)22.13 ms active(30.64 ms span)
Plumeria on Solid22.29 ms active(22.32 ms span)
cn (shadcn-ui) on Solid25.05 ms active(38.32 ms span)
StyleX on Solid26.21 ms active(26.74 ms span)
tailwind-merge27.59 ms active(57.29 ms span)
styled-components28.26 ms active(56.37 ms span)
Panda (style props)31.34 ms active(60.60 ms span)
Goober31.61 ms active(37.88 ms span)
Emotion32.28 ms active(54.68 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false36.81 ms active(45.33 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

vanilla Solid (hand-written ceiling)249 ms ±3%
Plumeria on Solid252 ms ±9%
@yak/solid (styled API, Solid 2)269 ms ±5%
next-yak (css prop)269 ms ±3%
next-yak (styled API)275 ms ±3%
Bamboo277 ms ±4%
vanilla (hand-written ceiling)279 ms ±6%
next-yak (styled API) foldStatic: false281 ms ±6%
Panda (css fn)284 ms ±3%
StyleX on Solid286 ms ±4%
StyleX without CSS layers (:not() specificity hack)287 ms ±9%
StyleX293 ms ±5%
Goober301 ms ±3%
cn (shadcn-ui) on Solid306 ms ±11%
styled-components309 ms ±3%
Plumeria (classStyle prop)309 ms ±5%
Emotion322 ms ±2%
next-yak (css prop) foldStatic: false322 ms ±7%
@yak/solid (styled API, Solid 2) foldStatic: false331 ms ±5%
Panda (style props)335 ms ±5%
cn (shadcn-ui)345 ms ±9%
tailwind-merge402 ms ±7%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)10.41 ms active(33.25 ms span · 33.25 ms profiled median (n=5))
vanilla (hand-written ceiling)11.11 ms active(33.94 ms span · 33.94 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false11.23 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
next-yak (css prop) foldStatic: false11.35 ms active(33.98 ms span · 33.98 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)11.54 ms active(33.20 ms span · 33.20 ms profiled median (n=5))
StyleX on Solid11.84 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
Plumeria on Solid12.06 ms active(33.20 ms span · 33.20 ms profiled median (n=5))
StyleX12.17 ms active(34.02 ms span · 34.02 ms profiled median (n=5))
next-yak (styled API)12.82 ms active(32.40 ms span · 32.40 ms profiled median (n=5))
Goober12.84 ms active(33.05 ms span · 33.05 ms profiled median (n=5))
Bamboo13.00 ms active(32.44 ms span · 32.44 ms profiled median (n=5))
next-yak (css prop)13.11 ms active(32.46 ms span · 32.46 ms profiled median (n=5))
Panda (css fn)13.12 ms active(32.41 ms span · 32.41 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)13.15 ms active(32.41 ms span · 32.41 ms profiled median (n=5))
next-yak (styled API) foldStatic: false14.44 ms active(32.42 ms span · 32.42 ms profiled median (n=5))
cn (shadcn-ui) on Solid14.51 ms active(33.20 ms span · 33.20 ms profiled median (n=5))
styled-components16.18 ms active(34.01 ms span · 34.01 ms profiled median (n=5))
Plumeria (classStyle prop)16.29 ms active(33.55 ms span · 33.55 ms profiled median (n=5))
Panda (style props)16.48 ms active(32.62 ms span · 32.62 ms profiled median (n=5))
Emotion17.90 ms active(33.97 ms span · 33.97 ms profiled median (n=5))
cn (shadcn-ui)19.40 ms active(30.85 ms span · 30.85 ms profiled median (n=5))
tailwind-merge19.78 ms active(30.73 ms span · 30.73 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

Plumeria on Solid109 ms ±4%
vanilla Solid (hand-written ceiling)111 ms ±5%
vanilla (hand-written ceiling)121 ms ±3%
Bamboo122 ms ±3%
Plumeria (classStyle prop)122 ms ±5%
cn (shadcn-ui) on Solid129 ms ±2%
StyleX without CSS layers (:not() specificity hack)129 ms ±2%
next-yak (css prop)129 ms ±4%
StyleX131 ms ±6%
cn (shadcn-ui)132 ms ±4%
next-yak (css prop) foldStatic: false135 ms ±3%
next-yak (styled API)137 ms ±4%
tailwind-merge146 ms ±5%
Panda (css fn)153 ms ±4%
@yak/solid (styled API, Solid 2)154 ms ±3%
next-yak (styled API) foldStatic: false157 ms ±7%
StyleX on Solid166 ms ±4%
styled-components195 ms ±3%
Goober204 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false230 ms ±4%
Panda (style props)245 ms ±2%
Emotion253 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)31.00 ms active(31.06 ms span)
@yak/solid (styled API, Solid 2)36.00 ms active(36.04 ms span)
next-yak (styled API)37.20 ms active(43.34 ms span)
next-yak (css prop)37.38 ms active(47.89 ms span)
cn (shadcn-ui) on Solid38.07 ms active(38.12 ms span)
StyleX without CSS layers (:not() specificity hack)38.39 ms active(39.20 ms span)
Plumeria (classStyle prop)39.26 ms active(46.21 ms span)
Bamboo39.98 ms active(55.46 ms span)
StyleX40.98 ms active(56.77 ms span)
Plumeria on Solid41.15 ms active(41.19 ms span)
vanilla (hand-written ceiling)41.73 ms active(56.02 ms span)
Panda (css fn)41.77 ms active(51.74 ms span)
StyleX on Solid43.88 ms active(43.93 ms span)
cn (shadcn-ui)45.27 ms active(55.83 ms span)
next-yak (styled API) foldStatic: false45.95 ms active(50.99 ms span)
next-yak (css prop) foldStatic: false46.05 ms active(50.49 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false48.83 ms active(48.90 ms span)
tailwind-merge52.52 ms active(62.10 ms span)
styled-components54.60 ms active(59.42 ms span)
Panda (style props)54.66 ms active(60.63 ms span)
Goober55.27 ms active(61.38 ms span)
Emotion58.12 ms active(64.30 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
EmotionChrome16.7 ms1 recalc
Firefox8.0 ms
styled-componentsChrome16.9 ms1 recalc
Firefox9.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome16.7 ms1 recalc
Firefox10.0 ms
StyleXChrome17.6 ms1 recalc
Firefox9.0 ms
next-yak (css prop)Chrome16.9 ms1 recalc
Firefox10.0 ms
next-yak (css prop) foldStatic: falseChrome18.8 ms1 recalc
Firefox11.0 ms
@yak/solid (styled API, Solid 2)Chrome16.7 ms1 recalc
Firefox11.2 ms
next-yak (styled API)Chrome16.2 ms1 recalc
Firefox12.0 ms
vanilla (hand-written ceiling)Chrome16.6 ms1 recalc
Firefox10.0 ms
Panda (style props)Chrome17.4 ms1 recalc
Firefox10.0 ms
vanilla Solid (hand-written ceiling)Chrome16.6 ms1 recalc
Firefox11.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome18.2 ms1 recalc
Firefox14.0 ms
Plumeria on SolidChrome21.5 ms1 recalc
Firefox12.0 ms
Panda (css fn)Chrome17.7 ms1 recalc
Firefox11.0 ms
Plumeria (classStyle prop)Chrome18.8 ms1 recalc
Firefox10.0 ms
BambooChrome18.2 ms1 recalc
Firefox11.0 ms
next-yak (styled API) foldStatic: falseChrome17.8 ms1 recalc
Firefox13.0 ms
StyleX on SolidChrome17.5 ms1 recalc
Firefox11.0 ms
cn (shadcn-ui) on SolidChrome18.5 ms1 recalc
Firefox12.8 ms
GooberChrome16.2 ms1 recalc
Firefox11.0 ms
cn (shadcn-ui)Chrome18.4 ms1 recalc
Firefox18.0 ms
tailwind-mergeChrome19.2 ms1 recalc
Firefox18.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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
next-yak (css prop) foldStatic: false7,622 B(0.4 / 0.9 / 6.1 kB)
next-yak (css prop)7,629 B(0.5 / 0.9 / 6.1 kB)
vanilla (hand-written ceiling)7,790 B(0.0 / 1.5 / 6.1 kB)
next-yak (styled API)8,308 B(1.1 / 0.9 / 6.1 kB)
next-yak (styled API) foldStatic: false8,444 B(1.3 / 0.9 / 6.1 kB)
vanilla Solid (hand-written ceiling)9,703 B(0.0 / 1.5 / 8.0 kB)
Plumeria (classStyle prop)11,285 B(1.1 / 1.4 / 8.5 kB)
Goober11,551 B(3.9 / 1.2 / 6.2 kB)
Plumeria on Solid12,600 B(0.0 / 1.4 / 10.9 kB)
StyleX14,007 B(3.5 / 1.4 / 8.8 kB)
StyleX without CSS layers (:not() specificity hack)14,115 B(3.5 / 1.5 / 8.8 kB)
Bamboo17,628 B(2.0 / 1.5 / 13.7 kB)
StyleX on Solid17,697 B(5.1 / 1.4 / 10.8 kB)
@yak/solid (styled API, Solid 2)18,068 B(6.3 / 0.9 / 10.5 kB)
Emotion20,671 B(12.9 / 1.2 / 6.1 kB)
styled-components21,421 B(13.2 / 1.4 / 6.3 kB)
Panda (css fn)22,232 B(4.9 / 1.5 / 15.3 kB)
Panda (style props)26,645 B(9.2 / 1.5 / 15.3 kB)
tailwind-merge27,085 B(10.4 / 2.0 / 14.0 kB)
cn (shadcn-ui)29,241 B(12.5 / 2.0 / 14.0 kB)
@yak/solid (styled API, Solid 2) foldStatic: false29,731 B(5.7 / 0.9 / 22.5 kB)
cn (shadcn-ui) on Solid31,473 B(12.1 / 2.0 / 16.7 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.048971451941005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-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)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: falsePlumeria (classStyle prop)Plumeria on Solid
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.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

vanilla Solid (hand-written ceiling)458,847 r/s ±4%(0.16 ms / 1k elems)
Plumeria on Solid439,484 r/s ±17%(0.17 ms / 1k elems)
cn (shadcn-ui) on Solid350,796 r/s ±21%(0.21 ms / 1k elems)
vanilla (hand-written ceiling)143,362 r/s ±1%(0.52 ms / 1k elems)
Plumeria (classStyle prop)142,066 r/s ±1%(0.52 ms / 1k elems)
cn (shadcn-ui)120,305 r/s ±5%(0.62 ms / 1k elems)
next-yak (css prop) foldStatic: false111,374 r/s ±4%(0.67 ms / 1k elems)
@yak/solid (styled API, Solid 2)107,300 r/s ±10%(0.69 ms / 1k elems)
StyleX on Solid103,053 r/s ±5%(0.72 ms / 1k elems)
Bamboo102,979 r/s ±1%(0.72 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false102,818 r/s ±5%(0.72 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)100,003 r/s ±3%(0.74 ms / 1k elems)
StyleX98,745 r/s ±4%(0.75 ms / 1k elems)
next-yak (css prop)94,721 r/s ±3%(0.78 ms / 1k elems)
tailwind-merge87,348 r/s ±1%(0.85 ms / 1k elems)
next-yak (styled API)65,663 r/s ±1%(1.13 ms / 1k elems)
next-yak (styled API) foldStatic: false65,262 r/s ±2%(1.14 ms / 1k elems)
Panda (css fn)54,582 r/s ±1%(1.36 ms / 1k elems)
styled-components37,751 r/s ±1%(1.96 ms / 1k elems)
Goober35,263 r/s ±1%(2.10 ms / 1k elems)
Panda (style props)24,610 r/s ±2%(3.01 ms / 1k elems)
Emotion21,079 r/s ±1%(3.51 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

vanilla Solid (hand-written ceiling)452 req/s ±0%
Plumeria on Solid285 req/s ±1%
vanilla (hand-written ceiling)225 req/s ±0%
next-yak (css prop)214 req/s ±0%
next-yak (css prop) foldStatic: false187 req/s ±0%
@yak/solid (styled API, Solid 2) foldStatic: false178 req/s ±0%
@yak/solid (styled API, Solid 2)178 req/s ±1%
cn (shadcn-ui) on Solid173 req/s ±1%
Plumeria (classStyle prop)166 req/s ±0%
StyleX without CSS layers (:not() specificity hack)140 req/s ±0%
StyleX138 req/s ±0%
StyleX on Solid132 req/s ±0%
next-yak (styled API)130 req/s ±1%
next-yak (styled API) foldStatic: false129 req/s ±1%
cn (shadcn-ui)116 req/s ±0%
Bamboo105 req/s ±1%
tailwind-merge103 req/s ±0%
styled-components76 req/s ±1%
Goober76 req/s ±0%
Panda (css fn)75 req/s ±0%
Emotion57 req/s ±0%
Panda (style props)44 req/s ±0%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
vanilla Solid (hand-written ceiling)1.59 ms
Plumeria on Solid1.85 ms
cn (shadcn-ui) on Solid1.89 ms
next-yak (css prop)3.94 ms
Plumeria (classStyle prop)4.00 ms
vanilla (hand-written ceiling)4.17 ms
cn (shadcn-ui)4.29 ms
@yak/solid (styled API, Solid 2)4.54 ms
@yak/solid (styled API, Solid 2) foldStatic: false4.60 ms
next-yak (css prop) foldStatic: false4.89 ms
Bamboo4.98 ms
StyleX on Solid5.35 ms
StyleX without CSS layers (:not() specificity hack)5.39 ms
StyleX5.68 ms
tailwind-merge5.80 ms
next-yak (styled API) foldStatic: false7.05 ms
next-yak (styled API)7.21 ms
Panda (css fn)9.69 ms
styled-components13.10 ms
Goober13.82 ms
Emotion19.79 ms
Panda (style props)22.50 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

Plumeria (classStyle prop)87 ms ±3%
Bamboo89 ms ±6%
vanilla (hand-written ceiling)90 ms ±4%
next-yak (css prop)94 ms ±2%
cn (shadcn-ui)100 ms ±4%
next-yak (css prop) foldStatic: false103 ms ±3%
StyleX without CSS layers (:not() specificity hack)103 ms ±4%
StyleX105 ms ±5%
Panda (css fn)114 ms ±3%
tailwind-merge114 ms ±2%
next-yak (styled API) foldStatic: false124 ms ±4%
next-yak (styled API)125 ms ±3%
Plumeria on Solid138 ms ±4%
vanilla Solid (hand-written ceiling)142 ms ±2%
styled-components147 ms ±5%
cn (shadcn-ui) on Solid155 ms ±3%
Goober170 ms ±2%
Panda (style props)204 ms ±3%
Emotion207 ms ±2%
StyleX on Solid235 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false318 ms ±3%
@yak/solid (styled API, Solid 2)319 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)15.37 ms active(39.21 ms span)
next-yak (css prop)17.85 ms active(34.02 ms span)
Bamboo18.07 ms active(28.56 ms span)
Plumeria (classStyle prop)18.28 ms active(44.69 ms span)
Plumeria on Solid18.46 ms active(20.80 ms span)
next-yak (css prop) foldStatic: false19.23 ms active(41.32 ms span)
vanilla Solid (hand-written ceiling)20.31 ms active(20.33 ms span)
Panda (css fn)21.90 ms active(28.06 ms span)
StyleX without CSS layers (:not() specificity hack)22.54 ms active(54.25 ms span)
StyleX23.89 ms active(43.90 ms span)
cn (shadcn-ui)25.25 ms active(55.71 ms span)
styled-components25.36 ms active(26.79 ms span)
cn (shadcn-ui) on Solid25.41 ms active(25.58 ms span)
next-yak (styled API) foldStatic: false25.94 ms active(43.41 ms span)
next-yak (styled API)29.36 ms active(43.22 ms span)
Goober29.87 ms active(34.56 ms span)
StyleX on Solid31.82 ms active(31.86 ms span)
Emotion32.56 ms active(59.22 ms span)
tailwind-merge35.58 ms active(59.62 ms span)
@yak/solid (styled API, Solid 2)37.22 ms active(49.84 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false39.30 ms active(48.05 ms span)
Panda (style props)39.43 ms active(57.48 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

Plumeria on Solid253 ms ±9%
vanilla Solid (hand-written ceiling)257 ms ±4%
Bamboo263 ms ±6%
next-yak (css prop)265 ms ±6%
vanilla (hand-written ceiling)274 ms ±6%
Plumeria (classStyle prop)277 ms ±3%
next-yak (css prop) foldStatic: false283 ms ±4%
next-yak (styled API)292 ms ±4%
Panda (css fn)292 ms ±6%
next-yak (styled API) foldStatic: false295 ms ±5%
StyleX302 ms ±6%
StyleX on Solid311 ms ±6%
StyleX without CSS layers (:not() specificity hack)313 ms ±4%
styled-components315 ms ±3%
Goober324 ms ±6%
Emotion332 ms ±4%
@yak/solid (styled API, Solid 2)333 ms ±5%
cn (shadcn-ui)349 ms ±5%
Panda (style props)358 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false361 ms ±6%
tailwind-merge370 ms ±8%
cn (shadcn-ui) on Solid405 ms ±11%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid10.98 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)11.21 ms active(33.11 ms span · 33.11 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false11.21 ms active(33.41 ms span · 33.41 ms profiled median (n=5))
Plumeria (classStyle prop)11.23 ms active(33.84 ms span · 33.84 ms profiled median (n=5))
vanilla (hand-written ceiling)11.88 ms active(32.57 ms span · 32.57 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)12.00 ms active(33.40 ms span · 33.40 ms profiled median (n=5))
Panda (css fn)12.09 ms active(32.48 ms span · 32.48 ms profiled median (n=5))
Bamboo12.13 ms active(32.44 ms span · 32.44 ms profiled median (n=5))
StyleX on Solid12.36 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
Emotion12.68 ms active(33.94 ms span · 33.94 ms profiled median (n=5))
next-yak (css prop)13.19 ms active(32.43 ms span · 32.43 ms profiled median (n=5))
StyleX13.70 ms active(32.17 ms span · 32.17 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)13.74 ms active(32.45 ms span · 32.45 ms profiled median (n=5))
next-yak (css prop) foldStatic: false13.89 ms active(32.37 ms span · 32.37 ms profiled median (n=5))
next-yak (styled API)14.44 ms active(33.04 ms span · 33.04 ms profiled median (n=5))
styled-components14.47 ms active(32.46 ms span · 32.46 ms profiled median (n=5))
next-yak (styled API) foldStatic: false14.76 ms active(32.36 ms span · 32.36 ms profiled median (n=5))
Goober15.67 ms active(32.40 ms span · 32.40 ms profiled median (n=5))
Panda (style props)15.81 ms active(33.87 ms span · 33.87 ms profiled median (n=5))
cn (shadcn-ui) on Solid17.84 ms active(33.05 ms span · 33.05 ms profiled median (n=5))
cn (shadcn-ui)19.61 ms active(30.66 ms span · 30.66 ms profiled median (n=5))
tailwind-merge19.73 ms active(30.91 ms span · 30.91 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

Plumeria on Solid117 ms ±5%
vanilla (hand-written ceiling)120 ms ±3%
vanilla Solid (hand-written ceiling)120 ms ±3%
Plumeria (classStyle prop)122 ms ±4%
Bamboo126 ms ±5%
next-yak (css prop)126 ms ±2%
cn (shadcn-ui)128 ms ±4%
cn (shadcn-ui) on Solid131 ms ±2%
next-yak (css prop) foldStatic: false135 ms ±4%
StyleX without CSS layers (:not() specificity hack)139 ms ±4%
StyleX140 ms ±6%
Panda (css fn)148 ms ±6%
tailwind-merge149 ms ±5%
next-yak (styled API)159 ms ±4%
next-yak (styled API) foldStatic: false160 ms ±3%
styled-components197 ms ±3%
Goober210 ms ±5%
StyleX on Solid216 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false242 ms ±2%
Panda (style props)244 ms ±3%
@yak/solid (styled API, Solid 2)249 ms ±4%
Emotion251 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid36.78 ms active(36.85 ms span)
vanilla Solid (hand-written ceiling)36.97 ms active(41.69 ms span)
next-yak (css prop)37.24 ms active(43.80 ms span)
vanilla (hand-written ceiling)41.09 ms active(47.87 ms span)
Plumeria (classStyle prop)41.87 ms active(48.13 ms span)
StyleX without CSS layers (:not() specificity hack)42.44 ms active(55.31 ms span)
next-yak (css prop) foldStatic: false42.58 ms active(44.60 ms span)
StyleX on Solid43.29 ms active(43.37 ms span)
Bamboo44.21 ms active(56.52 ms span)
cn (shadcn-ui) on Solid44.49 ms active(44.56 ms span)
StyleX45.00 ms active(53.29 ms span)
Panda (css fn)46.37 ms active(52.69 ms span)
cn (shadcn-ui)46.37 ms active(51.68 ms span)
@yak/solid (styled API, Solid 2)46.90 ms active(46.98 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false48.48 ms active(48.52 ms span)
next-yak (styled API) foldStatic: false49.18 ms active(60.70 ms span)
next-yak (styled API)50.87 ms active(56.64 ms span)
Goober51.27 ms active(57.46 ms span)
tailwind-merge52.52 ms active(68.18 ms span)
styled-components53.51 ms active(63.20 ms span)
Emotion56.83 ms active(63.44 ms span)
Panda (style props)64.95 ms active(71.33 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
next-yak (css prop)Chrome16.9 ms1 recalc
Firefox11.0 ms
vanilla (hand-written ceiling)Chrome16.4 ms1 recalc
Firefox10.0 ms
styled-componentsChrome16.9 ms1 recalc
Firefox9.0 ms
next-yak (css prop) foldStatic: falseChrome16.7 ms1 recalc
Firefox9.0 ms
StyleXChrome17.9 ms1 recalc
Firefox9.0 ms
BambooChrome17.7 ms1 recalc
Firefox9.0 ms
vanilla Solid (hand-written ceiling)Chrome17.2 ms1 recalc
Firefox9.0 ms
Panda (css fn)Chrome17.1 ms1 recalc
Firefox10.0 ms
@yak/solid (styled API, Solid 2)Chrome16.9 ms1 recalc
Firefox10.0 ms
next-yak (styled API) foldStatic: falseChrome19.4 ms1 recalc
Firefox10.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome16.5 ms1 recalc
Firefox11.0 ms
EmotionChrome16.7 ms1 recalc
Firefox11.0 ms
Plumeria (classStyle prop)Chrome19.2 ms1 recalc
Firefox10.0 ms
next-yak (styled API)Chrome16.6 ms1 recalc
Firefox10.0 ms
StyleX on SolidChrome16.6 ms1 recalc
Firefox11.0 ms
Panda (style props)Chrome17.5 ms1 recalc
Firefox12.0 ms
Plumeria on SolidChrome21.4 ms1 recalc
Firefox11.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome18.5 ms1 recalc
Firefox16.0 ms
cn (shadcn-ui)Chrome19.6 ms1 recalc
Firefox13.0 ms
GooberChrome16.8 ms1 recalc
Firefox17.0 ms
cn (shadcn-ui) on SolidChrome19.3 ms1 recalc
Firefox17.1 ms
tailwind-mergeChrome19.4 ms1 recalc
Firefox16.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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
next-yak (css prop) foldStatic: false7,614 B(0.4 / 0.9 / 6.1 kB)
next-yak (css prop)7,628 B(0.5 / 0.9 / 6.1 kB)
vanilla (hand-written ceiling)7,790 B(0.0 / 1.5 / 6.1 kB)
next-yak (styled API)8,442 B(1.1 / 0.9 / 6.2 kB)
next-yak (styled API) foldStatic: false8,565 B(1.3 / 0.9 / 6.2 kB)
vanilla Solid (hand-written ceiling)9,703 B(0.0 / 1.5 / 8.0 kB)
Plumeria (classStyle prop)11,285 B(1.1 / 1.4 / 8.5 kB)
Goober11,402 B(3.9 / 1.1 / 6.1 kB)
Plumeria on Solid12,602 B(0.0 / 1.4 / 10.9 kB)
StyleX without CSS layers (:not() specificity hack)14,003 B(3.5 / 1.5 / 8.7 kB)
StyleX14,064 B(3.5 / 1.4 / 8.8 kB)
Bamboo17,623 B(2.0 / 1.5 / 13.7 kB)
StyleX on Solid17,803 B(5.1 / 1.5 / 10.7 kB)
Emotion20,665 B(12.9 / 1.2 / 6.1 kB)
styled-components21,492 B(13.2 / 1.4 / 6.4 kB)
Panda (css fn)22,230 B(4.9 / 1.5 / 15.3 kB)
Panda (style props)26,643 B(9.2 / 1.5 / 15.3 kB)
tailwind-merge27,083 B(10.4 / 2.0 / 14.0 kB)
cn (shadcn-ui)29,239 B(12.5 / 2.0 / 14.0 kB)
@yak/solid (styled API, Solid 2) foldStatic: false29,716 B(5.7 / 0.9 / 22.5 kB)
@yak/solid (styled API, Solid 2)30,364 B(6.3 / 0.9 / 22.5 kB)
cn (shadcn-ui) on Solid31,471 B(12.1 / 2.0 / 16.7 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.049971461951005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-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)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: falsePlumeria (classStyle prop)Plumeria on Solid
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.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

vanilla Solid (hand-written ceiling)1,580,298 r/s ±11%(0.25 ms / 1k elems)
@yak/solid (styled API, Solid 2)1,432,501 r/s ±9%(0.28 ms / 1k elems)
cn (shadcn-ui) on Solid1,415,584 r/s ±8%(0.28 ms / 1k elems)
Plumeria on Solid1,409,968 r/s ±8%(0.28 ms / 1k elems)
StyleX on Solid816,953 r/s ±5%(0.49 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)629,153 r/s ±5%(0.64 ms / 1k elems)
StyleX621,041 r/s ±5%(0.64 ms / 1k elems)
next-yak (styled API)609,059 r/s ±1%(0.66 ms / 1k elems)
Plumeria (classStyle prop)514,453 r/s ±1%(0.78 ms / 1k elems)
vanilla (hand-written ceiling)506,975 r/s ±2%(0.79 ms / 1k elems)
next-yak (css prop) foldStatic: false495,805 r/s ±6%(0.81 ms / 1k elems)
cn (shadcn-ui)491,288 r/s ±9%(0.81 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false414,362 r/s ±3%(0.97 ms / 1k elems)
next-yak (css prop)404,391 r/s ±2%(0.99 ms / 1k elems)
Bamboo347,104 r/s ±1%(1.15 ms / 1k elems)
tailwind-merge291,188 r/s ±4%(1.37 ms / 1k elems)
next-yak (styled API) foldStatic: false265,499 r/s ±1%(1.51 ms / 1k elems)
Panda (css fn)265,483 r/s ±2%(1.51 ms / 1k elems)
styled-components156,049 r/s ±1%(2.56 ms / 1k elems)
Goober147,936 r/s ±1%(2.70 ms / 1k elems)
Emotion103,435 r/s ±2%(3.87 ms / 1k elems)
Panda (style props)89,130 r/s ±1%(4.49 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

@yak/solid (styled API, Solid 2)791 req/s ±1%
vanilla Solid (hand-written ceiling)781 req/s ±1%
Plumeria on Solid543 req/s ±1%
next-yak (styled API)445 req/s ±1%
next-yak (css prop)442 req/s ±1%
cn (shadcn-ui) on Solid433 req/s ±0%
StyleX on Solid417 req/s ±0%
next-yak (css prop) foldStatic: false382 req/s ±1%
vanilla (hand-written ceiling)371 req/s ±0%
StyleX364 req/s ±0%
StyleX without CSS layers (:not() specificity hack)361 req/s ±3%
@yak/solid (styled API, Solid 2) foldStatic: false351 req/s ±1%
Plumeria (classStyle prop)315 req/s ±1%
cn (shadcn-ui)262 req/s ±0%
next-yak (styled API) foldStatic: false243 req/s ±0%
tailwind-merge204 req/s ±0%
Bamboo181 req/s ±0%
Panda (css fn)153 req/s ±0%
Goober143 req/s ±1%
styled-components141 req/s ±1%
Emotion112 req/s ±0%
Panda (style props)73 req/s ±0%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
vanilla Solid (hand-written ceiling)1.12 ms
@yak/solid (styled API, Solid 2)1.18 ms
cn (shadcn-ui) on Solid1.24 ms
Plumeria on Solid1.42 ms
StyleX on Solid1.50 ms
StyleX1.84 ms
StyleX without CSS layers (:not() specificity hack)1.87 ms
next-yak (styled API)1.91 ms
next-yak (css prop)1.98 ms
vanilla (hand-written ceiling)2.41 ms
cn (shadcn-ui)2.52 ms
next-yak (css prop) foldStatic: false2.62 ms
@yak/solid (styled API, Solid 2) foldStatic: false2.71 ms
Plumeria (classStyle prop)3.54 ms
tailwind-merge3.78 ms
Bamboo4.24 ms
next-yak (styled API) foldStatic: false4.57 ms
Panda (css fn)4.77 ms
styled-components7.69 ms
Goober7.99 ms
Emotion9.70 ms
Panda (style props)13.01 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

Panda (css fn)79 ms ±4%
next-yak (css prop)80 ms ±4%
next-yak (styled API)80 ms ±2%
StyleX without CSS layers (:not() specificity hack)80 ms ±5%
StyleX81 ms ±4%
Plumeria (classStyle prop)81 ms ±2%
vanilla (hand-written ceiling)83 ms ±3%
Bamboo83 ms ±5%
next-yak (css prop) foldStatic: false92 ms ±5%
cn (shadcn-ui)95 ms ±3%
next-yak (styled API) foldStatic: false100 ms ±5%
tailwind-merge103 ms ±2%
styled-components115 ms ±4%
Goober118 ms ±4%
Plumeria on Solid128 ms ±3%
vanilla Solid (hand-written ceiling)132 ms ±2%
@yak/solid (styled API, Solid 2)135 ms ±5%
Emotion145 ms ±3%
cn (shadcn-ui) on Solid148 ms ±4%
StyleX on Solid153 ms ±4%
Panda (style props)154 ms ±6%
@yak/solid (styled API, Solid 2) foldStatic: false233 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)10.86 ms active(24.73 ms span)
Bamboo11.54 ms active(26.32 ms span)
next-yak (css prop)11.54 ms active(29.42 ms span)
@yak/solid (styled API, Solid 2)11.68 ms active(12.54 ms span)
Panda (css fn)11.82 ms active(26.08 ms span)
StyleX without CSS layers (:not() specificity hack)11.89 ms active(32.56 ms span)
StyleX12.16 ms active(35.18 ms span)
Plumeria (classStyle prop)12.25 ms active(32.65 ms span)
next-yak (styled API)12.47 ms active(40.60 ms span)
vanilla (hand-written ceiling)12.96 ms active(36.47 ms span)
next-yak (styled API) foldStatic: false13.75 ms active(25.87 ms span)
styled-components13.95 ms active(32.50 ms span)
StyleX on Solid14.35 ms active(21.94 ms span)
Plumeria on Solid14.51 ms active(14.54 ms span)
next-yak (css prop) foldStatic: false15.73 ms active(35.00 ms span)
cn (shadcn-ui)17.11 ms active(23.17 ms span)
Emotion17.25 ms active(37.79 ms span)
Goober18.02 ms active(35.18 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false19.50 ms active(19.55 ms span)
Panda (style props)19.65 ms active(30.64 ms span)
cn (shadcn-ui) on Solid20.35 ms active(32.21 ms span)
tailwind-merge21.77 ms active(37.31 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

next-yak (styled API)237 ms ±9%
vanilla (hand-written ceiling)252 ms ±6%
styled-components264 ms ±5%
next-yak (css prop)265 ms ±5%
cn (shadcn-ui) on Solid265 ms ±5%
next-yak (css prop) foldStatic: false265 ms ±5%
Goober265 ms ±4%
cn (shadcn-ui)266 ms ±4%
vanilla Solid (hand-written ceiling)268 ms ±4%
StyleX269 ms ±3%
next-yak (styled API) foldStatic: false269 ms ±6%
Bamboo272 ms ±4%
@yak/solid (styled API, Solid 2)273 ms ±7%
Plumeria on Solid275 ms ±4%
Panda (css fn)282 ms ±4%
Emotion288 ms ±7%
StyleX on Solid291 ms ±4%
Plumeria (classStyle prop)299 ms ±7%
@yak/solid (styled API, Solid 2) foldStatic: false303 ms ±3%
Panda (style props)312 ms ±6%
tailwind-merge317 ms ±6%
StyleX without CSS layers (:not() specificity hack)320 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)4.37 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
Goober4.42 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)4.52 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
tailwind-merge4.82 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
cn (shadcn-ui) on Solid4.83 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
next-yak (styled API) foldStatic: false5.12 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
cn (shadcn-ui)5.14 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
Panda (style props)5.17 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
next-yak (styled API)5.20 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
StyleX on Solid5.28 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
styled-components5.32 ms active(33.39 ms span · 33.39 ms profiled median (n=5))
next-yak (css prop)5.53 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
vanilla (hand-written ceiling)5.63 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false5.71 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)5.92 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
Panda (css fn)6.14 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
next-yak (css prop) foldStatic: false6.50 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Plumeria on Solid6.95 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Bamboo6.97 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX7.08 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Plumeria (classStyle prop)7.42 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Emotion7.48 ms active(33.31 ms span · 33.31 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

next-yak (styled API)98 ms ±3%
Plumeria on Solid99 ms ±2%
next-yak (css prop)100 ms ±4%
StyleX101 ms ±3%
Bamboo101 ms ±5%
@yak/solid (styled API, Solid 2)101 ms ±5%
vanilla Solid (hand-written ceiling)102 ms ±4%
vanilla (hand-written ceiling)102 ms ±4%
Plumeria (classStyle prop)103 ms ±2%
StyleX without CSS layers (:not() specificity hack)103 ms ±8%
Panda (css fn)108 ms ±3%
next-yak (css prop) foldStatic: false111 ms ±4%
cn (shadcn-ui)117 ms ±3%
cn (shadcn-ui) on Solid118 ms ±4%
StyleX on Solid120 ms ±5%
tailwind-merge133 ms ±4%
next-yak (styled API) foldStatic: false136 ms ±4%
Goober143 ms ±3%
styled-components151 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false166 ms ±5%
Emotion183 ms ±3%
Panda (style props)183 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid15.84 ms active(30.20 ms span)
@yak/solid (styled API, Solid 2)17.50 ms active(26.47 ms span)
vanilla Solid (hand-written ceiling)18.60 ms active(27.33 ms span)
vanilla (hand-written ceiling)19.48 ms active(32.55 ms span)
StyleX on Solid19.57 ms active(21.39 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false19.67 ms active(21.29 ms span)
next-yak (styled API)19.89 ms active(34.47 ms span)
next-yak (css prop)20.67 ms active(40.75 ms span)
cn (shadcn-ui) on Solid20.69 ms active(22.96 ms span)
Bamboo20.81 ms active(33.14 ms span)
Plumeria (classStyle prop)21.10 ms active(29.68 ms span)
StyleX21.36 ms active(44.59 ms span)
StyleX without CSS layers (:not() specificity hack)21.53 ms active(36.24 ms span)
next-yak (css prop) foldStatic: false21.57 ms active(35.03 ms span)
next-yak (styled API) foldStatic: false21.74 ms active(41.01 ms span)
Panda (css fn)22.54 ms active(36.77 ms span)
cn (shadcn-ui)26.54 ms active(39.42 ms span)
Panda (style props)26.95 ms active(36.02 ms span)
Emotion28.97 ms active(49.61 ms span)
Goober30.53 ms active(40.10 ms span)
styled-components32.05 ms active(37.50 ms span)
tailwind-merge36.04 ms active(49.47 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
BambooChrome6.2 ms1 recalc
Firefox2.0 ms
next-yak (css prop) foldStatic: falseChrome6.4 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome7.1 ms1 recalc
Firefox2.0 ms
styled-componentsChrome6.1 ms1 recalc
Firefox2.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome6.9 ms1 recalc
Firefox2.0 ms
StyleXChrome6.2 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome5.8 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2)Chrome5.6 ms1 recalc
Firefox3.0 ms
Plumeria (classStyle prop)Chrome6.9 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome5.7 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome6.5 ms1 recalc
Firefox2.0 ms
next-yak (css prop)Chrome6.1 ms1 recalc
Firefox3.0 ms
next-yak (styled API) foldStatic: falseChrome5.9 ms1 recalc
Firefox3.0 ms
GooberChrome6.6 ms1 recalc
Firefox3.0 ms
EmotionChrome6.2 ms1 recalc
Firefox4.0 ms
tailwind-mergeChrome6.2 ms1 recalc
Firefox3.0 ms
StyleX on SolidChrome7.0 ms1 recalc
Firefox2.0 ms
Plumeria on SolidChrome6.4 ms1 recalc
Firefox3.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome5.9 ms1 recalc
Firefox2.0 ms
next-yak (styled API)Chrome6.1 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui) on SolidChrome6.2 ms1 recalc
Firefox3.0 ms
cn (shadcn-ui)Chrome6.4 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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
next-yak (css prop) foldStatic: false4,422 B(0.4 / 0.5 / 3.4 kB)
next-yak (css prop)4,431 B(0.5 / 0.5 / 3.4 kB)
vanilla (hand-written ceiling)4,444 B(0.0 / 0.9 / 3.5 kB)
next-yak (styled API)4,986 B(1.1 / 0.5 / 3.2 kB)
next-yak (styled API) foldStatic: false5,118 B(1.3 / 0.5 / 3.2 kB)
Plumeria (classStyle prop)7,559 B(1.1 / 0.8 / 5.4 kB)
Goober8,038 B(3.9 / 0.6 / 3.4 kB)
StyleX10,074 B(3.5 / 0.9 / 5.4 kB)
StyleX without CSS layers (:not() specificity hack)10,130 B(3.5 / 1.0 / 5.4 kB)
vanilla Solid (hand-written ceiling)10,730 B(0.0 / 0.9 / 9.6 kB)
Bamboo12,215 B(2.0 / 1.0 / 8.9 kB)
Plumeria on Solid12,896 B(0.0 / 0.8 / 11.7 kB)
@yak/solid (styled API, Solid 2) foldStatic: false15,859 B(5.7 / 0.5 / 9.3 kB)
@yak/solid (styled API, Solid 2)16,680 B(6.3 / 0.5 / 9.5 kB)
Emotion17,156 B(12.9 / 0.6 / 3.2 kB)
Panda (css fn)17,523 B(4.9 / 1.0 / 11.3 kB)
styled-components17,538 B(13.2 / 0.6 / 3.3 kB)
StyleX on Solid18,180 B(5.1 / 0.9 / 11.7 kB)
tailwind-merge21,734 B(10.4 / 1.3 / 9.5 kB)
Panda (style props)21,931 B(9.2 / 1.0 / 11.3 kB)
cn (shadcn-ui)23,890 B(12.5 / 1.3 / 9.5 kB)
cn (shadcn-ui) on Solid28,801 B(12.1 / 1.4 / 14.7 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.0122435471005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-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)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: falsePlumeria (classStyle prop)Plumeria on Solid
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.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

@yak/solid (styled API, Solid 2)3,064,185 r/s ±49%(0.33 ms / 1k elems)
Plumeria on Solid2,504,132 r/s ±27%(0.40 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false2,243,158 r/s ±36%(0.45 ms / 1k elems)
vanilla Solid (hand-written ceiling)2,075,787 r/s ±6%(0.48 ms / 1k elems)
next-yak (styled API)1,980,084 r/s ±3%(0.51 ms / 1k elems)
cn (shadcn-ui) on Solid1,952,671 r/s ±25%(0.51 ms / 1k elems)
Plumeria (classStyle prop)1,632,890 r/s ±2%(0.61 ms / 1k elems)
Bamboo1,603,883 r/s ±3%(0.62 ms / 1k elems)
cn (shadcn-ui)1,564,576 r/s ±27%(0.64 ms / 1k elems)
vanilla (hand-written ceiling)1,434,148 r/s ±2%(0.70 ms / 1k elems)
next-yak (css prop) foldStatic: false1,322,424 r/s ±2%(0.76 ms / 1k elems)
next-yak (styled API) foldStatic: false1,101,268 r/s ±1%(0.91 ms / 1k elems)
StyleX on Solid1,022,810 r/s ±2%(0.98 ms / 1k elems)
StyleX1,017,327 r/s ±3%(0.98 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)993,055 r/s ±2%(1.01 ms / 1k elems)
tailwind-merge891,355 r/s ±3%(1.12 ms / 1k elems)
next-yak (css prop)708,953 r/s ±4%(1.41 ms / 1k elems)
Panda (style props)657,520 r/s ±1%(1.52 ms / 1k elems)
Panda (css fn)556,071 r/s ±1%(1.80 ms / 1k elems)
Goober475,115 r/s ±1%(2.10 ms / 1k elems)
styled-components402,628 r/s ±1%(2.48 ms / 1k elems)
Emotion225,170 r/s ±1%(4.44 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

@yak/solid (styled API, Solid 2)3,402 req/s ±0%
@yak/solid (styled API, Solid 2) foldStatic: false2,574 req/s ±1%
next-yak (styled API)1,749 req/s ±0%
vanilla Solid (hand-written ceiling)1,565 req/s ±1%
next-yak (css prop)1,510 req/s ±0%
Plumeria on Solid1,414 req/s ±2%
vanilla (hand-written ceiling)1,294 req/s ±3%
next-yak (css prop) foldStatic: false1,272 req/s ±1%
next-yak (styled API) foldStatic: false1,144 req/s ±0%
cn (shadcn-ui) on Solid1,126 req/s ±1%
Plumeria (classStyle prop)1,054 req/s ±1%
Bamboo1,010 req/s ±1%
cn (shadcn-ui)960 req/s ±1%
StyleX766 req/s ±2%
StyleX without CSS layers (:not() specificity hack)765 req/s ±1%
StyleX on Solid710 req/s ±2%
tailwind-merge662 req/s ±1%
Panda (style props)561 req/s ±2%
Goober487 req/s ±0%
Panda (css fn)445 req/s ±1%
styled-components411 req/s ±1%
Emotion291 req/s ±1%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
@yak/solid (styled API, Solid 2) foldStatic: false0.46 ms
Plumeria on Solid0.70 ms
@yak/solid (styled API, Solid 2)0.78 ms
Plumeria (classStyle prop)0.95 ms
next-yak (styled API)1.01 ms
cn (shadcn-ui) on Solid1.01 ms
vanilla Solid (hand-written ceiling)1.09 ms
next-yak (css prop) foldStatic: false1.13 ms
Bamboo1.30 ms
next-yak (styled API) foldStatic: false1.32 ms
next-yak (css prop)1.34 ms
cn (shadcn-ui)1.43 ms
vanilla (hand-written ceiling)1.53 ms
Panda (style props)1.57 ms
StyleX without CSS layers (:not() specificity hack)1.58 ms
StyleX1.69 ms
tailwind-merge1.85 ms
StyleX on Solid1.90 ms
Goober2.35 ms
styled-components2.80 ms
Panda (css fn)2.82 ms
Emotion4.10 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

next-yak (styled API)48 ms ±4%
Bamboo53 ms ±4%
next-yak (css prop)54 ms ±3%
Plumeria (classStyle prop)54 ms ±3%
vanilla (hand-written ceiling)63 ms ±4%
next-yak (css prop) foldStatic: false65 ms ±3%
next-yak (styled API) foldStatic: false66 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false66 ms ±4%
@yak/solid (styled API, Solid 2)67 ms ±6%
cn (shadcn-ui)67 ms ±3%
Plumeria on Solid67 ms ±5%
StyleX without CSS layers (:not() specificity hack)69 ms ±3%
StyleX70 ms ±5%
vanilla Solid (hand-written ceiling)73 ms ±4%
Panda (css fn)74 ms ±3%
styled-components78 ms ±3%
Goober78 ms ±3%
cn (shadcn-ui) on Solid79 ms ±5%
Panda (style props)85 ms ±3%
StyleX on Solid85 ms ±4%
tailwind-merge89 ms ±4%
Emotion100 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid6.81 ms active(16.07 ms span)
@yak/solid (styled API, Solid 2)6.92 ms active(28.69 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false7.02 ms active(22.11 ms span)
vanilla Solid (hand-written ceiling)7.26 ms active(24.67 ms span)
StyleX on Solid9.58 ms active(21.45 ms span)
cn (shadcn-ui) on Solid9.89 ms active(22.58 ms span)
next-yak (css prop)11.09 ms active(31.90 ms span)
Bamboo11.22 ms active(37.28 ms span)
next-yak (css prop) foldStatic: false11.46 ms active(31.15 ms span)
Plumeria (classStyle prop)11.62 ms active(28.36 ms span)
next-yak (styled API) foldStatic: false11.67 ms active(36.44 ms span)
StyleX11.97 ms active(35.26 ms span)
Goober12.36 ms active(19.80 ms span)
next-yak (styled API)12.54 ms active(31.18 ms span)
styled-components12.93 ms active(25.37 ms span)
cn (shadcn-ui)12.96 ms active(30.23 ms span)
Panda (css fn)13.12 ms active(25.64 ms span)
vanilla (hand-written ceiling)13.57 ms active(23.99 ms span)
Panda (style props)13.76 ms active(24.61 ms span)
StyleX without CSS layers (:not() specificity hack)14.21 ms active(30.91 ms span)
Emotion14.50 ms active(27.69 ms span)
tailwind-merge21.65 ms active(32.13 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

cn (shadcn-ui) on Solid57 ms ±4%
next-yak (styled API) foldStatic: false58 ms ±9%
@yak/solid (styled API, Solid 2) foldStatic: false58 ms ±1%
@yak/solid (styled API, Solid 2)58 ms ±5%
Bamboo59 ms ±2%
Plumeria on Solid59 ms ±5%
Plumeria (classStyle prop)59 ms ±1%
StyleX on Solid60 ms ±3%
next-yak (css prop)60 ms ±3%
cn (shadcn-ui)60 ms ±3%
next-yak (styled API)61 ms ±5%
vanilla Solid (hand-written ceiling)61 ms ±3%
vanilla (hand-written ceiling)61 ms ±3%
StyleX62 ms ±3%
Panda (css fn)63 ms ±2%
Panda (style props)63 ms ±7%
tailwind-merge63 ms ±3%
next-yak (css prop) foldStatic: false64 ms ±7%
Goober65 ms ±3%
StyleX without CSS layers (:not() specificity hack)65 ms ±3%
styled-components67 ms ±3%
Emotion77 ms ±10%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (css prop) foldStatic: false1.82 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX on Solid1.87 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
cn (shadcn-ui)1.89 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)1.94 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
tailwind-merge2.01 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
vanilla (hand-written ceiling)2.19 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Plumeria (classStyle prop)2.20 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
styled-components2.20 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Panda (css fn)2.21 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.24 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Bamboo2.31 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
next-yak (css prop)2.34 ms active(33.19 ms span · 33.19 ms profiled median (n=5))
next-yak (styled API) foldStatic: false2.43 ms active(33.41 ms span · 33.41 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.45 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
Plumeria on Solid2.53 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Panda (style props)2.65 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)2.66 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
cn (shadcn-ui) on Solid2.67 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Goober2.67 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Emotion2.76 ms active(33.21 ms span · 33.21 ms profiled median (n=5))
StyleX2.84 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
next-yak (styled API)2.94 ms active(33.40 ms span · 33.40 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

@yak/solid (styled API, Solid 2)33 ms ±4%
Plumeria on Solid43 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false45 ms ±3%
vanilla Solid (hand-written ceiling)52 ms ±18%
next-yak (styled API)53 ms ±2%
Plumeria (classStyle prop)57 ms ±4%
next-yak (css prop)57 ms ±3%
cn (shadcn-ui) on Solid58 ms ±2%
Bamboo58 ms ±4%
StyleX on Solid61 ms ±6%
next-yak (styled API) foldStatic: false68 ms ±3%
next-yak (css prop) foldStatic: false69 ms ±3%
vanilla (hand-written ceiling)70 ms ±5%
StyleX without CSS layers (:not() specificity hack)73 ms ±2%
StyleX73 ms ±3%
cn (shadcn-ui)75 ms ±3%
Panda (css fn)81 ms ±3%
Goober82 ms ±3%
tailwind-merge90 ms ±2%
styled-components91 ms ±4%
Panda (style props)92 ms ±4%
Emotion110 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)9.93 ms active(22.37 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false10.27 ms active(23.36 ms span)
vanilla Solid (hand-written ceiling)10.31 ms active(20.25 ms span)
Plumeria on Solid11.45 ms active(32.01 ms span)
StyleX on Solid12.31 ms active(22.75 ms span)
cn (shadcn-ui) on Solid13.18 ms active(27.49 ms span)
next-yak (css prop)16.46 ms active(31.01 ms span)
StyleX without CSS layers (:not() specificity hack)17.35 ms active(39.27 ms span)
Plumeria (classStyle prop)17.42 ms active(41.61 ms span)
next-yak (css prop) foldStatic: false17.80 ms active(29.75 ms span)
next-yak (styled API) foldStatic: false17.83 ms active(34.39 ms span)
vanilla (hand-written ceiling)18.02 ms active(34.54 ms span)
Goober18.10 ms active(31.89 ms span)
Bamboo18.21 ms active(41.23 ms span)
Panda (css fn)18.61 ms active(32.88 ms span)
next-yak (styled API)19.25 ms active(33.47 ms span)
StyleX19.47 ms active(41.01 ms span)
cn (shadcn-ui)20.02 ms active(33.09 ms span)
Panda (style props)21.65 ms active(39.90 ms span)
styled-components22.29 ms active(47.11 ms span)
Emotion23.28 ms active(34.14 ms span)
tailwind-merge27.75 ms active(34.84 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
cn (shadcn-ui)Chrome5.4 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome4.6 ms1 recalc
Firefox1.0 ms
EmotionChrome4.9 ms1 recalc
Firefox1.0 ms
GooberChrome4.5 ms1 recalc
Firefox2.0 ms
next-yak (styled API)Chrome5.0 ms1 recalc
Firefox2.0 ms
next-yak (css prop)Chrome4.7 ms1 recalc
Firefox1.0 ms
next-yak (css prop) foldStatic: falseChrome4.5 ms1 recalc
Firefox1.0 ms
next-yak (styled API) foldStatic: falseChrome5.3 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome4.8 ms1 recalc
Firefox2.0 ms
Panda (style props)Chrome5.6 ms1 recalc
Firefox2.0 ms
styled-componentsChrome4.3 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome4.6 ms1 recalc
Firefox1.0 ms
StyleXChrome5.0 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome5.1 ms1 recalc
Firefox3.0 ms
vanilla (hand-written ceiling)Chrome5.1 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome4.5 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome4.5 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.7 ms1 recalc
Firefox1.0 ms
Plumeria (classStyle prop)Chrome4.7 ms1 recalc
Firefox1.0 ms
Plumeria on SolidChrome5.1 ms1 recalc
Firefox1.0 ms
BambooChrome4.8 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome5.1 ms1 recalc
Firefox2.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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)3,164 B(0.0 / 0.5 / 2.6 kB)
next-yak (css prop) foldStatic: false3,188 B(0.4 / 0.2 / 2.5 kB)
next-yak (css prop)3,206 B(0.5 / 0.2 / 2.5 kB)
next-yak (styled API)3,906 B(1.1 / 0.2 / 2.5 kB)
next-yak (styled API) foldStatic: false4,038 B(1.3 / 0.2 / 2.5 kB)
Plumeria (classStyle prop)4,556 B(1.1 / 0.3 / 3.1 kB)
Bamboo5,622 B(2.0 / 0.3 / 3.2 kB)
vanilla Solid (hand-written ceiling)5,784 B(0.0 / 0.5 / 5.2 kB)
Plumeria on Solid6,227 B(0.0 / 0.3 / 5.8 kB)
Goober6,806 B(3.9 / 0.2 / 2.6 kB)
StyleX without CSS layers (:not() specificity hack)7,014 B(3.5 / 0.3 / 3.0 kB)
StyleX7,015 B(3.5 / 0.3 / 3.0 kB)
Panda (css fn)8,798 B(4.9 / 0.3 / 3.4 kB)
@yak/solid (styled API, Solid 2) foldStatic: false11,035 B(5.7 / 0.2 / 4.9 kB)
StyleX on Solid11,464 B(5.1 / 0.3 / 5.8 kB)
@yak/solid (styled API, Solid 2)11,824 B(6.3 / 0.2 / 5.1 kB)
Panda (style props)13,207 B(9.2 / 0.3 / 3.4 kB)
tailwind-merge14,646 B(10.4 / 0.7 / 3.2 kB)
Emotion15,991 B(12.9 / 0.3 / 2.4 kB)
styled-components16,438 B(13.2 / 0.3 / 2.6 kB)
cn (shadcn-ui)16,802 B(12.5 / 0.7 / 3.2 kB)
cn (shadcn-ui) on Solid18,985 B(12.1 / 0.7 / 5.8 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.04.69.114181005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-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)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: falsePlumeria (classStyle prop)Plumeria on Solid
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.

Source · generated HTML · generated CSS · rendered preview

rendered preview

The css prop takes styles written in place. From the next-yak docs: "Its value has to be styles written in place: a css template or an atoms() call and logical combinations of them" (Features, CSS Prop). The compiler folds those fragments onto the element where they are written and collapses them into one class on one host tag.

This case asks each module to export a component that extends the one below: GhostPrimaryButton wraps GhostButton wraps Button. A fragment cannot travel to another component through a prop, and one component cannot extend another, so the css prop has nothing to compose against. The styled API does that with styled(Button), which is the next-yak lane's cell. Merging class names at runtime would be a workaround rather than the primitive.

The same three declarations as style values are in button-variants, where this API composes the ladder onto one element.

The css prop takes styles written in place. From the next-yak docs: "Its value has to be styles written in place: a css template or an atoms() call and logical combinations of them" (Features, CSS Prop). The compiler folds those fragments onto the element where they are written and collapses them into one class on one host tag.

This case asks each module to export a component that extends the one below: GhostPrimaryButton wraps GhostButton wraps Button. A fragment cannot travel to another component through a prop, and one component cannot extend another, so the css prop has nothing to compose against. The styled API does that with styled(Button), which is the next-yak lane's cell. Merging class names at runtime would be a workaround rather than the primitive.

The same three declarations as style values are in button-variants, where this API composes the ladder onto one element.

This case asks each module to export a component that extends the one below: GhostPrimaryButton wraps GhostButton wraps Button, each adding its own style and passing the accumulated styles down. Plumeria lets a style value cross one component boundary, but the receiving component has to apply it to an element; relaying a received classStyle to another component is a build error, because composition is resolved where the styles are written.

The same three declarations as style values are in button-variants, where classStyle composes the ladder onto one element. Joining class strings at runtime here would bypass Plumeria's conflict resolution, so the lane has no cell. Source: the lane author's note on the pull request.

This case asks each module to export a component that extends the one below: GhostPrimaryButton wraps GhostButton wraps Button, each adding its own style and passing the accumulated styles down. Plumeria lets a style value cross one component boundary, but the receiving component has to apply it to an element; relaying a received classStyle to another component is a build error, because composition is resolved where the styles are written.

The same three declarations as style values are in button-variants, where classStyle composes the ladder onto one element. Joining class strings at runtime here would bypass Plumeria's conflict resolution, so the lane has no cell. Source: the lane author's note on the pull request.

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

@yak/solid (styled API, Solid 2) foldStatic: false3,623,412 r/s ±9%(0.28 ms / 1k elems)
@yak/solid (styled API, Solid 2)3,227,087 r/s ±21%(0.31 ms / 1k elems)
cn (shadcn-ui)1,089,873 r/s ±10%(0.92 ms / 1k elems)
vanilla (hand-written ceiling)1,078,792 r/s ±3%(0.93 ms / 1k elems)
cn (shadcn-ui) on Solid1,061,475 r/s ±20%(0.94 ms / 1k elems)
next-yak (styled API)913,612 r/s ±23%(1.09 ms / 1k elems)
next-yak (styled API) foldStatic: false871,685 r/s ±3%(1.15 ms / 1k elems)
vanilla Solid (hand-written ceiling)850,023 r/s ±12%(1.18 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)808,251 r/s ±5%(1.24 ms / 1k elems)
Bamboo783,627 r/s ±2%(1.28 ms / 1k elems)
StyleX776,026 r/s ±5%(1.29 ms / 1k elems)
StyleX on Solid728,638 r/s ±5%(1.37 ms / 1k elems)
tailwind-merge640,182 r/s ±3%(1.56 ms / 1k elems)
styled-components569,564 r/s ±1%(1.76 ms / 1k elems)
Panda (style props)540,093 r/s ±3%(1.85 ms / 1k elems)
Goober370,724 r/s ±1%(2.70 ms / 1k elems)
Panda (css fn)268,156 r/s ±2%(3.73 ms / 1k elems)
Emotion252,268 r/s ±1%(3.96 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

@yak/solid (styled API, Solid 2) foldStatic: false2,525 req/s ±1%
@yak/solid (styled API, Solid 2)2,478 req/s ±4%
vanilla (hand-written ceiling)1,059 req/s ±0%
next-yak (styled API)959 req/s ±0%
next-yak (styled API) foldStatic: false954 req/s ±0%
cn (shadcn-ui)798 req/s ±1%
vanilla Solid (hand-written ceiling)788 req/s ±1%
cn (shadcn-ui) on Solid726 req/s ±0%
StyleX without CSS layers (:not() specificity hack)660 req/s ±1%
StyleX654 req/s ±2%
styled-components587 req/s ±2%
StyleX on Solid547 req/s ±0%
tailwind-merge524 req/s ±0%
Bamboo518 req/s ±1%
Panda (style props)477 req/s ±0%
Goober410 req/s ±0%
Emotion308 req/s ±1%
Panda (css fn)245 req/s ±0%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
@yak/solid (styled API, Solid 2) foldStatic: false0.68 ms
@yak/solid (styled API, Solid 2)0.72 ms
cn (shadcn-ui) on Solid1.37 ms
StyleX without CSS layers (:not() specificity hack)1.37 ms
next-yak (styled API)1.55 ms
StyleX1.60 ms
cn (shadcn-ui)1.65 ms
vanilla Solid (hand-written ceiling)1.68 ms
vanilla (hand-written ceiling)1.71 ms
Bamboo1.78 ms
StyleX on Solid1.83 ms
tailwind-merge1.96 ms
Panda (style props)2.02 ms
next-yak (styled API) foldStatic: false2.05 ms
styled-components2.53 ms
Goober2.73 ms
Emotion3.46 ms
Panda (css fn)4.04 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

@yak/solid (styled API, Solid 2) foldStatic: false65 ms ±4%
next-yak (styled API) foldStatic: false67 ms ±5%
@yak/solid (styled API, Solid 2)67 ms ±3%
next-yak (styled API)67 ms ±4%
vanilla (hand-written ceiling)68 ms ±5%
Bamboo69 ms ±3%
StyleX without CSS layers (:not() specificity hack)72 ms ±3%
StyleX73 ms ±4%
cn (shadcn-ui)77 ms ±5%
styled-components78 ms ±7%
cn (shadcn-ui) on Solid87 ms ±5%
Panda (style props)89 ms ±3%
Goober89 ms ±4%
tailwind-merge91 ms ±3%
StyleX on Solid93 ms ±3%
Panda (css fn)94 ms ±1%
Emotion95 ms ±4%
vanilla Solid (hand-written ceiling)107 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)6.96 ms active(25.24 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false7.43 ms active(33.27 ms span)
vanilla Solid (hand-written ceiling)10.29 ms active(16.07 ms span)
vanilla (hand-written ceiling)11.08 ms active(23.04 ms span)
next-yak (styled API)11.43 ms active(31.82 ms span)
StyleX on Solid11.51 ms active(13.80 ms span)
next-yak (styled API) foldStatic: false11.83 ms active(40.27 ms span)
StyleX12.02 ms active(36.05 ms span)
StyleX without CSS layers (:not() specificity hack)12.25 ms active(35.99 ms span)
Bamboo12.28 ms active(35.76 ms span)
cn (shadcn-ui) on Solid12.53 ms active(22.54 ms span)
Goober13.21 ms active(39.91 ms span)
styled-components13.69 ms active(23.71 ms span)
cn (shadcn-ui)14.22 ms active(38.36 ms span)
Panda (style props)14.36 ms active(32.84 ms span)
Emotion18.28 ms active(23.51 ms span)
Panda (css fn)19.32 ms active(30.58 ms span)
tailwind-merge25.56 ms active(31.61 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

cn (shadcn-ui) on Solid58 ms ±6%
@yak/solid (styled API, Solid 2)58 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false59 ms ±5%
vanilla Solid (hand-written ceiling)60 ms ±2%
next-yak (styled API) foldStatic: false61 ms ±2%
cn (shadcn-ui)61 ms ±4%
next-yak (styled API)62 ms ±7%
StyleX on Solid62 ms ±3%
Bamboo62 ms ±3%
vanilla (hand-written ceiling)63 ms ±2%
tailwind-merge63 ms ±4%
StyleX64 ms ±3%
StyleX without CSS layers (:not() specificity hack)65 ms ±3%
Panda (style props)66 ms ±5%
styled-components67 ms ±4%
Emotion74 ms ±2%
Panda (css fn)76 ms ±6%
Goober78 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)1.78 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)1.94 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
cn (shadcn-ui)1.96 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Panda (style props)1.97 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
cn (shadcn-ui) on Solid2.03 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
next-yak (styled API)2.04 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
StyleX on Solid2.22 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.32 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
next-yak (styled API) foldStatic: false2.34 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.47 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
tailwind-merge2.54 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
Bamboo2.56 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Panda (css fn)2.57 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX2.61 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
vanilla (hand-written ceiling)2.70 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
styled-components2.92 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Emotion3.34 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
Goober3.39 ms active(33.36 ms span · 33.36 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

@yak/solid (styled API, Solid 2) foldStatic: false45 ms ±4%
@yak/solid (styled API, Solid 2)45 ms ±3%
cn (shadcn-ui) on Solid64 ms ±5%
vanilla Solid (hand-written ceiling)67 ms ±5%
next-yak (styled API)69 ms ±2%
next-yak (styled API) foldStatic: false69 ms ±4%
StyleX on Solid69 ms ±3%
vanilla (hand-written ceiling)69 ms ±3%
Bamboo73 ms ±3%
cn (shadcn-ui)78 ms ±3%
StyleX without CSS layers (:not() specificity hack)78 ms ±5%
StyleX78 ms ±4%
styled-components89 ms ±4%
Panda (style props)95 ms ±4%
Goober95 ms ±2%
tailwind-merge96 ms ±3%
Emotion104 ms ±3%
Panda (css fn)105 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false10.25 ms active(12.85 ms span)
@yak/solid (styled API, Solid 2)10.37 ms active(28.07 ms span)
vanilla Solid (hand-written ceiling)12.24 ms active(15.53 ms span)
cn (shadcn-ui) on Solid13.60 ms active(16.48 ms span)
StyleX on Solid13.73 ms active(20.59 ms span)
vanilla (hand-written ceiling)16.83 ms active(33.88 ms span)
next-yak (styled API) foldStatic: false17.23 ms active(33.17 ms span)
StyleX without CSS layers (:not() specificity hack)17.85 ms active(41.14 ms span)
Bamboo17.95 ms active(41.52 ms span)
StyleX18.15 ms active(40.23 ms span)
next-yak (styled API)18.19 ms active(42.08 ms span)
styled-components19.78 ms active(30.83 ms span)
cn (shadcn-ui)20.26 ms active(38.75 ms span)
Goober21.22 ms active(40.84 ms span)
Panda (style props)24.54 ms active(49.94 ms span)
Panda (css fn)26.25 ms active(36.36 ms span)
Emotion27.14 ms active(38.96 ms span)
tailwind-merge29.72 ms active(38.28 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
BambooChrome4.8 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui)Chrome4.9 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome4.5 ms1 recalc
Firefox1.0 ms
EmotionChrome4.7 ms1 recalc
Firefox1.0 ms
GooberChrome4.7 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome4.9 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome5.1 ms1 recalc
Firefox1.0 ms
styled-componentsChrome4.8 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome4.7 ms1 recalc
Firefox3.0 ms
StyleXChrome5.2 ms1 recalc
Firefox1.0 ms
StyleX on SolidChrome5.0 ms1 recalc
Firefox2.0 ms
tailwind-mergeChrome5.5 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome4.5 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome5.0 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2)Chrome4.7 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.5 ms1 recalc
Firefox1.0 ms
next-yak (styled API) foldStatic: falseChrome4.7 ms1 recalc
Firefox2.0 ms
next-yak (styled API)Chrome5.1 ms1 recalc
Firefox2.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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)3,164 B(0.0 / 0.5 / 2.6 kB)
next-yak (styled API)4,095 B(1.1 / 0.2 / 2.6 kB)
next-yak (styled API) foldStatic: false4,231 B(1.3 / 0.2 / 2.6 kB)
vanilla Solid (hand-written ceiling)5,954 B(0.0 / 0.5 / 5.4 kB)
Bamboo6,212 B(2.0 / 0.4 / 3.7 kB)
Goober6,861 B(3.9 / 0.2 / 2.6 kB)
StyleX without CSS layers (:not() specificity hack)7,014 B(3.5 / 0.3 / 3.0 kB)
StyleX7,015 B(3.5 / 0.3 / 3.0 kB)
Panda (css fn)8,798 B(4.9 / 0.3 / 3.4 kB)
@yak/solid (styled API, Solid 2) foldStatic: false11,380 B(5.7 / 0.2 / 5.2 kB)
StyleX on Solid11,464 B(5.1 / 0.3 / 5.8 kB)
@yak/solid (styled API, Solid 2)12,030 B(6.3 / 0.2 / 5.2 kB)
Panda (style props)13,207 B(9.2 / 0.3 / 3.4 kB)
tailwind-merge14,646 B(10.4 / 0.7 / 3.2 kB)
Emotion15,998 B(12.9 / 0.3 / 2.4 kB)
styled-components16,659 B(13.2 / 0.3 / 2.7 kB)
cn (shadcn-ui)16,802 B(12.5 / 0.7 / 3.2 kB)
cn (shadcn-ui) on Solid18,985 B(12.1 / 0.7 / 5.8 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.04.08.012161005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-yak (styled API)next-yak (styled API) foldStatic: falsePanda (css fn)Panda (style props)styled-componentsStyleX without CSS layers (:not() specificity hack)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: false
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.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

vanilla Solid (hand-written ceiling)160,735 r/s ±7%(0.78 ms / 1k elems)
cn (shadcn-ui) on Solid142,878 r/s ±8%(0.87 ms / 1k elems)
Plumeria on Solid132,592 r/s ±11%(0.94 ms / 1k elems)
vanilla (hand-written ceiling)92,578 r/s ±3%(1.35 ms / 1k elems)
StyleX83,136 r/s ±5%(1.50 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)83,135 r/s ±5%(1.50 ms / 1k elems)
Plumeria (classStyle prop)78,548 r/s ±3%(1.59 ms / 1k elems)
StyleX on Solid77,681 r/s ±5%(1.61 ms / 1k elems)
next-yak (css prop) foldStatic: false69,592 r/s ±4%(1.80 ms / 1k elems)
@yak/solid (styled API, Solid 2)67,257 r/s ±7%(1.86 ms / 1k elems)
next-yak (css prop)67,044 r/s ±2%(1.86 ms / 1k elems)
cn (shadcn-ui)55,441 r/s ±1%(2.25 ms / 1k elems)
next-yak (styled API)52,003 r/s ±2%(2.40 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false51,718 r/s ±3%(2.42 ms / 1k elems)
next-yak (styled API) foldStatic: false44,829 r/s ±2%(2.79 ms / 1k elems)
Bamboo43,306 r/s ±2%(2.89 ms / 1k elems)
tailwind-merge37,396 r/s ±1%(3.34 ms / 1k elems)
Goober27,617 r/s ±2%(4.53 ms / 1k elems)
Panda (css fn)23,425 r/s ±1%(5.34 ms / 1k elems)
styled-components23,301 r/s ±2%(5.36 ms / 1k elems)
Emotion15,521 r/s ±1%(8.05 ms / 1k elems)
Panda (style props)13,622 r/s ±2%(9.18 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

vanilla Solid (hand-written ceiling)762 req/s ±1%
vanilla (hand-written ceiling)498 req/s ±0%
Plumeria on Solid492 req/s ±1%
next-yak (css prop)437 req/s ±0%
cn (shadcn-ui) on Solid393 req/s ±1%
@yak/solid (styled API, Solid 2)369 req/s ±1%
next-yak (css prop) foldStatic: false360 req/s ±0%
StyleX without CSS layers (:not() specificity hack)353 req/s ±1%
StyleX351 req/s ±0%
StyleX on Solid340 req/s ±0%
Plumeria (classStyle prop)326 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false305 req/s ±0%
next-yak (styled API)298 req/s ±1%
next-yak (styled API) foldStatic: false271 req/s ±1%
cn (shadcn-ui)213 req/s ±1%
Bamboo182 req/s ±0%
tailwind-merge171 req/s ±1%
Goober169 req/s ±0%
styled-components133 req/s ±1%
Panda (css fn)114 req/s ±1%
Emotion113 req/s ±0%
Panda (style props)75 req/s ±1%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
Plumeria on Solid1.69 ms
next-yak (css prop)1.96 ms
cn (shadcn-ui) on Solid1.98 ms
vanilla Solid (hand-written ceiling)1.99 ms
StyleX without CSS layers (:not() specificity hack)2.01 ms
StyleX2.02 ms
StyleX on Solid2.38 ms
Plumeria (classStyle prop)2.48 ms
vanilla (hand-written ceiling)2.67 ms
next-yak (css prop) foldStatic: false2.70 ms
@yak/solid (styled API, Solid 2)2.97 ms
next-yak (styled API)3.31 ms
@yak/solid (styled API, Solid 2) foldStatic: false3.37 ms
next-yak (styled API) foldStatic: false3.52 ms
cn (shadcn-ui)3.56 ms
tailwind-merge4.68 ms
Bamboo5.76 ms
Goober6.08 ms
styled-components7.75 ms
Panda (css fn)8.50 ms
Emotion10.21 ms
Panda (style props)13.96 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

next-yak (css prop)68 ms ±3%
StyleX68 ms ±5%
StyleX without CSS layers (:not() specificity hack)69 ms ±4%
vanilla (hand-written ceiling)69 ms ±3%
Plumeria (classStyle prop)69 ms ±5%
Bamboo77 ms ±5%
cn (shadcn-ui)81 ms ±3%
next-yak (css prop) foldStatic: false81 ms ±6%
next-yak (styled API)83 ms ±5%
next-yak (styled API) foldStatic: false91 ms ±6%
Panda (css fn)96 ms ±3%
tailwind-merge96 ms ±4%
Plumeria on Solid101 ms ±4%
vanilla Solid (hand-written ceiling)102 ms ±5%
Goober103 ms ±3%
styled-components106 ms ±3%
cn (shadcn-ui) on Solid111 ms ±3%
StyleX on Solid129 ms ±4%
Emotion142 ms ±2%
Panda (style props)153 ms ±4%
@yak/solid (styled API, Solid 2)182 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false230 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria (classStyle prop)15.46 ms active(32.37 ms span)
next-yak (css prop)15.60 ms active(36.52 ms span)
StyleX15.90 ms active(40.13 ms span)
StyleX without CSS layers (:not() specificity hack)16.21 ms active(45.28 ms span)
vanilla (hand-written ceiling)16.37 ms active(39.15 ms span)
Bamboo16.95 ms active(34.96 ms span)
next-yak (css prop) foldStatic: false18.71 ms active(34.74 ms span)
next-yak (styled API)21.10 ms active(38.06 ms span)
Plumeria on Solid21.19 ms active(21.49 ms span)
Panda (css fn)21.85 ms active(35.21 ms span)
cn (shadcn-ui)22.02 ms active(34.91 ms span)
next-yak (styled API) foldStatic: false23.55 ms active(40.85 ms span)
vanilla Solid (hand-written ceiling)24.20 ms active(28.59 ms span)
Goober26.35 ms active(35.06 ms span)
cn (shadcn-ui) on Solid26.50 ms active(36.70 ms span)
styled-components27.81 ms active(29.27 ms span)
tailwind-merge28.65 ms active(31.21 ms span)
StyleX on Solid29.46 ms active(42.47 ms span)
Emotion33.20 ms active(46.80 ms span)
Panda (style props)34.00 ms active(55.98 ms span)
@yak/solid (styled API, Solid 2)45.25 ms active(54.52 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false49.74 ms active(53.58 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

StyleX150 ms ±9%
Plumeria (classStyle prop)163 ms ±6%
StyleX without CSS layers (:not() specificity hack)193 ms ±5%
StyleX on Solid204 ms ±8%
Plumeria on Solid205 ms ±7%
vanilla Solid (hand-written ceiling)235 ms ±13%
next-yak (css prop)252 ms ±7%
next-yak (css prop) foldStatic: false261 ms ±5%
next-yak (styled API)263 ms ±4%
@yak/solid (styled API, Solid 2)272 ms ±8%
styled-components273 ms ±7%
next-yak (styled API) foldStatic: false275 ms ±10%
cn (shadcn-ui) on Solid275 ms ±8%
Goober278 ms ±8%
Bamboo285 ms ±8%
Emotion288 ms ±3%
vanilla (hand-written ceiling)292 ms ±10%
@yak/solid (styled API, Solid 2) foldStatic: false296 ms ±5%
Panda (css fn)311 ms ±7%
cn (shadcn-ui)313 ms ±4%
Panda (style props)321 ms ±6%
tailwind-merge335 ms ±11%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX18.36 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
Plumeria (classStyle prop)21.90 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)22.99 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
StyleX on Solid23.24 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
next-yak (css prop)23.74 ms active(32.72 ms span · 32.72 ms profiled median (n=5))
next-yak (styled API) foldStatic: false25.58 ms active(30.70 ms span · 30.70 ms profiled median (n=5))
next-yak (css prop) foldStatic: false26.20 ms active(31.01 ms span · 31.01 ms profiled median (n=5))
vanilla (hand-written ceiling)26.42 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
Goober26.46 ms active(32.28 ms span · 32.28 ms profiled median (n=5))
Plumeria on Solid27.09 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
styled-components27.16 ms active(31.88 ms span · 31.88 ms profiled median (n=5))
tailwind-merge27.71 ms active(35.59 ms span · 35.59 ms profiled median (n=5))
Bamboo28.57 ms active(35.20 ms span · 35.20 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)28.75 ms active(33.17 ms span · 33.17 ms profiled median (n=5))
next-yak (styled API)29.19 ms active(34.23 ms span · 34.23 ms profiled median (n=5))
cn (shadcn-ui)29.22 ms active(35.19 ms span · 35.19 ms profiled median (n=5))
Emotion29.78 ms active(35.80 ms span · 35.80 ms profiled median (n=5))
Panda (css fn)31.64 ms active(34.41 ms span · 34.41 ms profiled median (n=5))
cn (shadcn-ui) on Solid31.76 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
Panda (style props)31.92 ms active(35.87 ms span · 35.87 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)33.86 ms active(33.91 ms span · 33.91 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false48.00 ms active(49.84 ms span · 49.84 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

vanilla Solid (hand-written ceiling)80 ms ±4%
Plumeria on Solid82 ms ±4%
vanilla (hand-written ceiling)86 ms ±3%
next-yak (css prop)87 ms ±5%
StyleX without CSS layers (:not() specificity hack)87 ms ±3%
Plumeria (classStyle prop)88 ms ±2%
StyleX90 ms ±3%
cn (shadcn-ui) on Solid90 ms ±3%
Bamboo93 ms ±3%
next-yak (css prop) foldStatic: false96 ms ±5%
cn (shadcn-ui)98 ms ±5%
next-yak (styled API)107 ms ±5%
next-yak (styled API) foldStatic: false109 ms ±2%
StyleX on Solid111 ms ±7%
tailwind-merge116 ms ±5%
Panda (css fn)118 ms ±1%
Goober130 ms ±3%
styled-components141 ms ±3%
@yak/solid (styled API, Solid 2)156 ms ±4%
Emotion175 ms ±7%
Panda (style props)180 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false191 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX37.47 ms active(44.08 ms span)
Plumeria on Solid37.83 ms active(37.86 ms span)
StyleX on Solid38.44 ms active(38.47 ms span)
next-yak (css prop)39.01 ms active(50.26 ms span)
vanilla Solid (hand-written ceiling)39.06 ms active(39.10 ms span)
vanilla (hand-written ceiling)40.89 ms active(41.00 ms span)
StyleX without CSS layers (:not() specificity hack)42.27 ms active(48.59 ms span)
Bamboo43.06 ms active(51.43 ms span)
Plumeria (classStyle prop)43.83 ms active(58.35 ms span)
next-yak (styled API)44.78 ms active(50.00 ms span)
next-yak (styled API) foldStatic: false48.25 ms active(53.22 ms span)
next-yak (css prop) foldStatic: false48.86 ms active(63.63 ms span)
Goober50.28 ms active(55.13 ms span)
Panda (css fn)51.85 ms active(60.48 ms span)
cn (shadcn-ui)54.69 ms active(67.25 ms span)
@yak/solid (styled API, Solid 2)55.62 ms active(55.66 ms span)
cn (shadcn-ui) on Solid58.02 ms active(58.06 ms span)
tailwind-merge60.46 ms active(69.68 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false61.20 ms active(61.23 ms span)
Panda (style props)62.11 ms active(69.34 ms span)
Emotion67.91 ms active(76.43 ms span)
styled-components70.18 ms active(76.32 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
@yak/solid (styled API, Solid 2)Chrome18.4 ms1 recalc
Firefox14.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome17.6 ms1 recalc
Firefox11.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome20.0 ms1 recalc
Firefox10.0 ms
Panda (style props)Chrome20.3 ms1 recalc
Firefox13.0 ms
StyleXChrome15.7 ms1 recalc
Firefox9.0 ms
styled-componentsChrome22.6 ms1 recalc
Firefox11.0 ms
GooberChrome18.4 ms1 recalc
Firefox12.0 ms
Panda (css fn)Chrome19.7 ms1 recalc
Firefox15.0 ms
vanilla (hand-written ceiling)Chrome19.7 ms1 recalc
Firefox10.0 ms
next-yak (css prop) foldStatic: falseChrome21.8 ms1 recalc
Firefox11.0 ms
next-yak (styled API)Chrome18.2 ms1 recalc
Firefox15.0 ms
next-yak (css prop)Chrome18.1 ms1 recalc
Firefox10.0 ms
StyleX on SolidChrome14.6 ms1 recalc
Firefox10.0 ms
cn (shadcn-ui) on SolidChrome29.2 ms1 recalc
Firefox13.0 ms
Plumeria (classStyle prop)Chrome21.1 ms1 recalc
Firefox10.0 ms
BambooChrome20.6 ms1 recalc
Firefox13.0 ms
next-yak (styled API) foldStatic: falseChrome18.3 ms1 recalc
Firefox16.0 ms
Plumeria on SolidChrome19.2 ms1 recalc
Firefox11.0 ms
EmotionChrome20.7 ms1 recalc
Firefox12.0 ms
vanilla Solid (hand-written ceiling)Chrome18.7 ms1 recalc
Firefox12.0 ms
tailwind-mergeChrome24.9 ms1 recalc
Firefox15.0 ms
cn (shadcn-ui)Chrome25.4 ms1 recalc
Firefox17.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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)2,690 B(0.0 / 1.0 / 1.6 kB)
next-yak (css prop) foldStatic: false2,826 B(0.4 / 0.7 / 1.7 kB)
next-yak (css prop)2,835 B(0.5 / 0.6 / 1.7 kB)
next-yak (styled API)3,528 B(1.1 / 0.6 / 1.7 kB)
next-yak (styled API) foldStatic: false3,658 B(1.3 / 0.6 / 1.7 kB)
vanilla Solid (hand-written ceiling)5,087 B(0.0 / 1.0 / 3.9 kB)
Goober6,446 B(3.9 / 0.7 / 1.7 kB)
Plumeria (classStyle prop)6,838 B(1.1 / 0.9 / 4.7 kB)
StyleX9,096 B(3.5 / 0.9 / 4.5 kB)
StyleX without CSS layers (:not() specificity hack)9,169 B(3.5 / 1.0 / 4.5 kB)
Plumeria on Solid9,336 B(0.0 / 0.9 / 8.2 kB)
@yak/solid (styled API, Solid 2)11,039 B(6.3 / 0.6 / 3.8 kB)
Bamboo12,172 B(2.0 / 1.3 / 8.6 kB)
@yak/solid (styled API, Solid 2) foldStatic: false12,500 B(5.7 / 0.6 / 5.9 kB)
StyleX on Solid14,467 B(5.1 / 0.9 / 8.1 kB)
Emotion15,723 B(12.9 / 0.8 / 1.6 kB)
styled-components16,163 B(13.2 / 0.8 / 1.7 kB)
Panda (css fn)16,608 B(4.9 / 1.2 / 10.1 kB)
Panda (style props)21,354 B(9.2 / 1.2 / 10.5 kB)
tailwind-merge21,697 B(10.4 / 1.6 / 9.2 kB)
cn (shadcn-ui)23,853 B(12.5 / 1.6 / 9.2 kB)
cn (shadcn-ui) on Solid26,769 B(12.1 / 1.6 / 12.5 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.0901812713611005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-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)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: falsePlumeria (classStyle prop)Plumeria on Solid
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.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

vanilla Solid (hand-written ceiling)156,780 r/s ±8%(0.80 ms / 1k elems)
cn (shadcn-ui) on Solid139,312 r/s ±14%(0.90 ms / 1k elems)
Plumeria on Solid128,855 r/s ±10%(0.97 ms / 1k elems)
vanilla (hand-written ceiling)91,569 r/s ±3%(1.37 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)83,420 r/s ±5%(1.50 ms / 1k elems)
StyleX81,494 r/s ±6%(1.53 ms / 1k elems)
Plumeria (classStyle prop)78,447 r/s ±2%(1.59 ms / 1k elems)
StyleX on Solid75,117 r/s ±5%(1.66 ms / 1k elems)
next-yak (css prop) foldStatic: false67,073 r/s ±5%(1.86 ms / 1k elems)
next-yak (css prop)63,716 r/s ±2%(1.96 ms / 1k elems)
@yak/solid (styled API, Solid 2)62,538 r/s ±12%(2.00 ms / 1k elems)
cn (shadcn-ui)55,204 r/s ±1%(2.26 ms / 1k elems)
next-yak (styled API)51,233 r/s ±2%(2.44 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false51,078 r/s ±6%(2.45 ms / 1k elems)
next-yak (styled API) foldStatic: false45,910 r/s ±2%(2.72 ms / 1k elems)
Bamboo43,474 r/s ±2%(2.88 ms / 1k elems)
tailwind-merge37,508 r/s ±2%(3.33 ms / 1k elems)
Goober27,536 r/s ±2%(4.54 ms / 1k elems)
styled-components23,861 r/s ±1%(5.24 ms / 1k elems)
Panda (css fn)23,162 r/s ±2%(5.40 ms / 1k elems)
Emotion15,574 r/s ±2%(8.03 ms / 1k elems)
Panda (style props)13,528 r/s ±2%(9.24 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

vanilla Solid (hand-written ceiling)753 req/s ±1%
vanilla (hand-written ceiling)499 req/s ±0%
Plumeria on Solid491 req/s ±1%
next-yak (css prop)417 req/s ±0%
cn (shadcn-ui) on Solid393 req/s ±0%
@yak/solid (styled API, Solid 2)360 req/s ±1%
StyleX353 req/s ±1%
StyleX without CSS layers (:not() specificity hack)352 req/s ±1%
next-yak (css prop) foldStatic: false342 req/s ±0%
StyleX on Solid339 req/s ±1%
Plumeria (classStyle prop)326 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false303 req/s ±1%
next-yak (styled API)299 req/s ±1%
next-yak (styled API) foldStatic: false268 req/s ±1%
cn (shadcn-ui)214 req/s ±0%
Bamboo182 req/s ±0%
tailwind-merge170 req/s ±1%
Goober170 req/s ±1%
styled-components135 req/s ±1%
Panda (css fn)114 req/s ±0%
Emotion114 req/s ±0%
Panda (style props)76 req/s ±1%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
cn (shadcn-ui) on Solid1.34 ms
vanilla Solid (hand-written ceiling)1.66 ms
Plumeria on Solid1.72 ms
StyleX2.00 ms
StyleX without CSS layers (:not() specificity hack)2.06 ms
vanilla (hand-written ceiling)2.06 ms
next-yak (css prop)2.23 ms
StyleX on Solid2.81 ms
@yak/solid (styled API, Solid 2)2.90 ms
cn (shadcn-ui)3.12 ms
next-yak (css prop) foldStatic: false3.13 ms
@yak/solid (styled API, Solid 2) foldStatic: false3.20 ms
next-yak (styled API)3.57 ms
Plumeria (classStyle prop)3.94 ms
next-yak (styled API) foldStatic: false4.03 ms
Bamboo4.56 ms
tailwind-merge4.77 ms
Goober6.05 ms
styled-components7.90 ms
Panda (css fn)8.42 ms
Emotion10.12 ms
Panda (style props)15.41 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

Plumeria (classStyle prop)68 ms ±2%
StyleX without CSS layers (:not() specificity hack)69 ms ±6%
next-yak (css prop)69 ms ±3%
vanilla (hand-written ceiling)70 ms ±3%
StyleX70 ms ±2%
Bamboo76 ms ±5%
next-yak (css prop) foldStatic: false78 ms ±4%
cn (shadcn-ui)80 ms ±2%
next-yak (styled API)88 ms ±5%
next-yak (styled API) foldStatic: false88 ms ±4%
tailwind-merge95 ms ±4%
Panda (css fn)97 ms ±4%
Plumeria on Solid100 ms ±3%
vanilla Solid (hand-written ceiling)101 ms ±3%
Goober103 ms ±5%
styled-components106 ms ±3%
cn (shadcn-ui) on Solid114 ms ±3%
StyleX on Solid130 ms ±4%
Emotion145 ms ±5%
Panda (style props)155 ms ±4%
@yak/solid (styled API, Solid 2)187 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false230 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX15.92 ms active(40.57 ms span)
next-yak (css prop)16.38 ms active(37.72 ms span)
Plumeria (classStyle prop)17.39 ms active(36.26 ms span)
vanilla (hand-written ceiling)17.74 ms active(36.17 ms span)
next-yak (css prop) foldStatic: false20.47 ms active(40.48 ms span)
Bamboo21.08 ms active(45.33 ms span)
StyleX without CSS layers (:not() specificity hack)21.41 ms active(40.86 ms span)
next-yak (styled API)21.67 ms active(44.14 ms span)
Plumeria on Solid23.98 ms active(30.94 ms span)
vanilla Solid (hand-written ceiling)24.93 ms active(27.65 ms span)
next-yak (styled API) foldStatic: false25.01 ms active(44.12 ms span)
cn (shadcn-ui)25.86 ms active(33.59 ms span)
Goober27.36 ms active(35.13 ms span)
Panda (css fn)27.56 ms active(28.97 ms span)
styled-components27.69 ms active(53.55 ms span)
cn (shadcn-ui) on Solid28.14 ms active(36.79 ms span)
StyleX on Solid29.05 ms active(30.19 ms span)
tailwind-merge30.54 ms active(39.98 ms span)
Panda (style props)32.27 ms active(57.11 ms span)
Emotion36.10 ms active(65.08 ms span)
@yak/solid (styled API, Solid 2)37.56 ms active(51.72 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false45.79 ms active(50.21 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

StyleX147 ms ±9%
Plumeria (classStyle prop)182 ms ±4%
StyleX without CSS layers (:not() specificity hack)196 ms ±5%
Plumeria on Solid197 ms ±9%
StyleX on Solid203 ms ±7%
vanilla Solid (hand-written ceiling)250 ms ±4%
next-yak (css prop)254 ms ±3%
next-yak (styled API)262 ms ±9%
vanilla (hand-written ceiling)264 ms ±2%
next-yak (css prop) foldStatic: false267 ms ±9%
next-yak (styled API) foldStatic: false273 ms ±12%
Bamboo276 ms ±4%
cn (shadcn-ui) on Solid278 ms ±11%
Goober280 ms ±6%
@yak/solid (styled API, Solid 2)283 ms ±8%
styled-components285 ms ±5%
Panda (css fn)288 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false290 ms ±9%
cn (shadcn-ui)295 ms ±8%
Emotion302 ms ±7%
tailwind-merge316 ms ±7%
Panda (style props)329 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX17.93 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)21.28 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Plumeria (classStyle prop)21.88 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
vanilla (hand-written ceiling)23.98 ms active(33.39 ms span · 33.39 ms profiled median (n=5))
StyleX on Solid24.24 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Goober24.67 ms active(35.80 ms span · 35.80 ms profiled median (n=5))
Bamboo25.08 ms active(35.65 ms span · 35.65 ms profiled median (n=5))
next-yak (css prop)26.57 ms active(30.41 ms span · 30.41 ms profiled median (n=5))
Plumeria on Solid26.75 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)26.82 ms active(33.13 ms span · 33.13 ms profiled median (n=5))
next-yak (styled API)27.05 ms active(35.66 ms span · 35.66 ms profiled median (n=5))
tailwind-merge27.10 ms active(35.66 ms span · 35.66 ms profiled median (n=5))
next-yak (css prop) foldStatic: false27.59 ms active(32.56 ms span · 32.56 ms profiled median (n=5))
Emotion27.84 ms active(35.40 ms span · 35.40 ms profiled median (n=5))
next-yak (styled API) foldStatic: false29.05 ms active(31.96 ms span · 31.96 ms profiled median (n=5))
Panda (style props)29.75 ms active(35.42 ms span · 35.42 ms profiled median (n=5))
cn (shadcn-ui) on Solid33.18 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Panda (css fn)33.19 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
styled-components34.93 ms active(35.80 ms span · 35.80 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)37.01 ms active(49.84 ms span · 49.84 ms profiled median (n=5))
cn (shadcn-ui)37.63 ms active(45.26 ms span · 45.26 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false46.55 ms active(49.85 ms span · 49.85 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

vanilla Solid (hand-written ceiling)77 ms ±3%
Plumeria on Solid81 ms ±6%
vanilla (hand-written ceiling)84 ms ±3%
next-yak (css prop)84 ms ±5%
StyleX without CSS layers (:not() specificity hack)87 ms ±1%
StyleX89 ms ±6%
Plumeria (classStyle prop)90 ms ±3%
Bamboo91 ms ±5%
cn (shadcn-ui) on Solid93 ms ±6%
cn (shadcn-ui)97 ms ±3%
next-yak (css prop) foldStatic: false97 ms ±4%
next-yak (styled API)107 ms ±4%
next-yak (styled API) foldStatic: false109 ms ±4%
StyleX on Solid110 ms ±5%
tailwind-merge118 ms ±3%
Panda (css fn)124 ms ±4%
Goober124 ms ±6%
styled-components147 ms ±6%
@yak/solid (styled API, Solid 2)160 ms ±3%
Emotion175 ms ±5%
Panda (style props)177 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false191 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid36.59 ms active(36.80 ms span)
vanilla Solid (hand-written ceiling)38.00 ms active(40.90 ms span)
StyleX38.43 ms active(44.44 ms span)
StyleX on Solid39.81 ms active(39.86 ms span)
next-yak (css prop)40.19 ms active(55.72 ms span)
Bamboo42.05 ms active(55.34 ms span)
vanilla (hand-written ceiling)43.04 ms active(58.31 ms span)
Plumeria (classStyle prop)44.77 ms active(51.30 ms span)
next-yak (styled API)44.98 ms active(50.87 ms span)
StyleX without CSS layers (:not() specificity hack)45.05 ms active(54.00 ms span)
cn (shadcn-ui) on Solid48.46 ms active(48.53 ms span)
next-yak (styled API) foldStatic: false48.67 ms active(53.05 ms span)
Goober50.98 ms active(56.31 ms span)
cn (shadcn-ui)51.59 ms active(56.02 ms span)
styled-components51.99 ms active(67.27 ms span)
next-yak (css prop) foldStatic: false53.25 ms active(55.60 ms span)
@yak/solid (styled API, Solid 2)54.12 ms active(54.19 ms span)
Panda (css fn)56.13 ms active(61.15 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false57.21 ms active(57.28 ms span)
tailwind-merge61.79 ms active(66.27 ms span)
Panda (style props)62.21 ms active(76.72 ms span)
Emotion70.04 ms active(76.37 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
next-yak (styled API)Chrome18.0 ms1 recalc
Firefox10.0 ms
next-yak (css prop) foldStatic: falseChrome23.5 ms1 recalc
Firefox9.0 ms
@yak/solid (styled API, Solid 2)Chrome18.7 ms1 recalc
Firefox10.0 ms
EmotionChrome24.3 ms1 recalc
Firefox9.0 ms
next-yak (styled API) foldStatic: falseChrome20.0 ms1 recalc
Firefox11.0 ms
Panda (style props)Chrome20.0 ms1 recalc
Firefox10.0 ms
vanilla (hand-written ceiling)Chrome19.5 ms1 recalc
Firefox11.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome17.2 ms1 recalc
Firefox12.0 ms
next-yak (css prop)Chrome19.2 ms1 recalc
Firefox11.0 ms
Plumeria on SolidChrome19.5 ms1 recalc
Firefox11.0 ms
vanilla Solid (hand-written ceiling)Chrome19.5 ms1 recalc
Firefox10.0 ms
StyleXChrome16.1 ms1 recalc
Firefox9.0 ms
Plumeria (classStyle prop)Chrome21.7 ms1 recalc
Firefox13.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome20.0 ms1 recalc
Firefox10.0 ms
cn (shadcn-ui) on SolidChrome22.6 ms1 recalc
Firefox17.0 ms
StyleX on SolidChrome14.2 ms1 recalc
Firefox10.0 ms
tailwind-mergeChrome25.4 ms1 recalc
Firefox16.0 ms
styled-componentsChrome17.0 ms1 recalc
Firefox11.0 ms
Panda (css fn)Chrome23.3 ms1 recalc
Firefox16.0 ms
GooberChrome19.1 ms1 recalc
Firefox13.0 ms
BambooChrome20.1 ms1 recalc
Firefox15.0 ms
cn (shadcn-ui)Chrome23.0 ms1 recalc
Firefox14.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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)2,690 B(0.0 / 1.0 / 1.6 kB)
next-yak (css prop) foldStatic: false2,822 B(0.4 / 0.6 / 1.7 kB)
next-yak (css prop)2,834 B(0.5 / 0.6 / 1.7 kB)
next-yak (styled API)3,529 B(1.1 / 0.6 / 1.7 kB)
next-yak (styled API) foldStatic: false3,660 B(1.3 / 0.6 / 1.7 kB)
vanilla Solid (hand-written ceiling)5,087 B(0.0 / 1.0 / 3.9 kB)
Goober6,446 B(3.9 / 0.7 / 1.7 kB)
Plumeria (classStyle prop)6,838 B(1.1 / 0.9 / 4.7 kB)
StyleX9,096 B(3.5 / 0.9 / 4.5 kB)
StyleX without CSS layers (:not() specificity hack)9,169 B(3.5 / 1.0 / 4.5 kB)
Plumeria on Solid9,336 B(0.0 / 0.9 / 8.2 kB)
@yak/solid (styled API, Solid 2)11,042 B(6.3 / 0.6 / 3.8 kB)
Bamboo12,172 B(2.0 / 1.3 / 8.6 kB)
@yak/solid (styled API, Solid 2) foldStatic: false12,492 B(5.7 / 0.6 / 5.9 kB)
StyleX on Solid14,467 B(5.1 / 0.9 / 8.1 kB)
Emotion15,723 B(12.9 / 0.8 / 1.6 kB)
styled-components16,186 B(13.2 / 0.8 / 1.8 kB)
Panda (css fn)16,608 B(4.9 / 1.2 / 10.1 kB)
Panda (style props)21,354 B(9.2 / 1.2 / 10.5 kB)
tailwind-merge21,697 B(10.4 / 1.6 / 9.2 kB)
cn (shadcn-ui)23,853 B(12.5 / 1.6 / 9.2 kB)
cn (shadcn-ui) on Solid26,769 B(12.1 / 1.6 / 12.5 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.0901792693591005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-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)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: falsePlumeria (classStyle prop)Plumeria on Solid
Dynamic valuetranslateX (written directly)n = 1,000high cardinality

1,000 elements each with a unique translateX, a value no build step can know, such as a position from a database record or from a drag-and-drop interaction, written directly: it goes straight into the style declaration, the way each library's API takes it. styled-components emits a CSS rule per value, while next-yak turns it into a CSS variable, so its per-instance work stays constant. The build-time lanes have no cell here: Tailwind's scanner, Panda's extractor and Bamboo's compiler resolve names ahead of time and emit no rule for a render-time value. Their working path, an inline style, is what dyn-fair measures.

Source · generated HTML · generated CSS · rendered preview

rendered preview

Bamboo resolves every css() object at build time to a class-string literal and rejects a value that exists only at render. The direct path this case measures, a translateX computed from the instance index inside css(), fails the build with css() — dynamic.

The library's author states the position and the intended path:

Bamboo intentionally does not handle dynamic styles and in rare cases where it has to be a runtime value delegates to style= or data- attributes.

Source: gajus, Bamboo's author, in issue #7. That style= path is what dyn-fair measures, and this lane takes part there.

Tailwind generates its CSS ahead of time by scanning source files for class names. From the docs: "The most important implication of how Tailwind extracts class names is that it will only find classes that exist as complete unbroken strings in your source files. If you use string interpolation or concatenate partial class names together, Tailwind will not find them and therefore will not generate the corresponding CSS" (Content configuration, Dynamic class names).

The direct path this case measures builds [transform:translateX(<i>px)] from the instance index at render, so the scanner never sees the class and the element ships without its transform. cn() still parses the string on every render, but no rule ever exists behind it. Making it work takes a safelist of every value, or running the JIT over the rendered output, which no production build does.

This lane's working path, a static utility plus an inline style for the value, is what dyn-fair measures, and this lane takes part there.

Tailwind generates its CSS ahead of time by scanning source files for class names. From the docs: "The most important implication of how Tailwind extracts class names is that it will only find classes that exist as complete unbroken strings in your source files. If you use string interpolation or concatenate partial class names together, Tailwind will not find them and therefore will not generate the corresponding CSS" (Content configuration, Dynamic class names).

The direct path this case measures builds [transform:translateX(<i>px)] from the instance index at render, so the scanner never sees the class and the element ships without its transform. cn() still parses the string on every render, but no rule ever exists behind it. Making it work takes a safelist of every value, or running the JIT over the rendered output, which no production build does.

This lane's working path, a static utility plus an inline style for the value, is what dyn-fair measures, and this lane takes part there.

Panda extracts styles at build time: css() resolves names for the declarations the extractor can read from the source and emits nothing for a value composed at render. A translateX built from the instance index yields a class with no rule behind it, so the direct path this case measures cannot be written with the primitive.

A Panda maintainer, asked about this cell:

Panda is build time and css() is mostly a name resolver; it doesn't generate a rule. Using translateX(${i}px) is pretty far outside what Panda's for.

Source: Adebesin Tolulope, Panda maintainer, in a direct message to the bench author, August 2026. The docs say the same: "We recommend that you avoid relying on runtime values for your styles. Consider using recipes, css variables or data-* attributes instead" (Dynamic styling). The inline-style path is what dyn-fair measures, and this lane takes part there.

Panda extracts styles at build time: a style prop resolves to a name for the declaration the extractor can read from the source and emits nothing for a value composed at render. A transform prop built from the instance index yields a class with no rule behind it, so the direct path this case measures cannot be written with the primitive.

A Panda maintainer, asked about this cell:

Panda is build time and css() is mostly a name resolver; it doesn't generate a rule. Using translateX(${i}px) is pretty far outside what Panda's for.

Source: Adebesin Tolulope, Panda maintainer, in a direct message to the bench author, August 2026. The docs say the same: "We recommend that you avoid relying on runtime values for your styles. Consider using recipes, css variables or data-* attributes instead" (Dynamic styling). The inline-style path is what dyn-fair measures, and this lane takes part there.

Tailwind generates its CSS ahead of time by scanning source files for class names. From the docs: "The most important implication of how Tailwind extracts class names is that it will only find classes that exist as complete unbroken strings in your source files. If you use string interpolation or concatenate partial class names together, Tailwind will not find them and therefore will not generate the corresponding CSS" (Content configuration, Dynamic class names).

The direct path this case measures builds [transform:translateX(<i>px)] from the instance index at render, so the scanner never sees the class and the element ships without its transform. cn() still parses the string on every render, but no rule ever exists behind it. Making it work takes a safelist of every value, or running the JIT over the rendered output, which no production build does.

This lane's working path, a static utility plus an inline style for the value, is what dyn-fair measures, and this lane takes part there.

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

vanilla Solid (hand-written ceiling)2,252,060 r/s ±7%(0.44 ms / 1k elems)
Plumeria on Solid2,046,061 r/s ±7%(0.49 ms / 1k elems)
Plumeria (classStyle prop)1,360,146 r/s ±2%(0.74 ms / 1k elems)
vanilla (hand-written ceiling)1,317,764 r/s ±1%(0.76 ms / 1k elems)
@yak/solid (styled API, Solid 2)1,080,671 r/s ±13%(0.93 ms / 1k elems)
next-yak (css prop) foldStatic: false998,557 r/s ±3%(1.00 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false939,869 r/s ±3%(1.06 ms / 1k elems)
StyleX812,599 r/s ±1%(1.23 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)805,227 r/s ±2%(1.24 ms / 1k elems)
StyleX on Solid676,548 r/s ±3%(1.48 ms / 1k elems)
next-yak (css prop)608,194 r/s ±2%(1.64 ms / 1k elems)
next-yak (styled API) foldStatic: false573,823 r/s ±1%(1.74 ms / 1k elems)
next-yak (styled API)557,334 r/s ±1%(1.79 ms / 1k elems)
Emotion234,165 r/s ±2%(4.27 ms / 1k elems)
styled-components176,457 r/s ±4%(5.67 ms / 1k elems)
Goober148,552 r/s ±3%(6.73 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

Plumeria on Solid1,620 req/s ±1%
vanilla Solid (hand-written ceiling)1,580 req/s ±2%
vanilla (hand-written ceiling)1,148 req/s ±1%
Plumeria (classStyle prop)1,141 req/s ±1%
@yak/solid (styled API, Solid 2)1,040 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false1,038 req/s ±1%
next-yak (css prop) foldStatic: false954 req/s ±1%
next-yak (css prop)940 req/s ±1%
StyleX771 req/s ±1%
StyleX without CSS layers (:not() specificity hack)765 req/s ±0%
next-yak (styled API)662 req/s ±0%
next-yak (styled API) foldStatic: false650 req/s ±2%
StyleX on Solid583 req/s ±1%
Emotion258 req/s ±2%
styled-components181 req/s ±1%
Goober149 req/s ±2%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
@yak/solid (styled API, Solid 2)1.02 ms
Plumeria on Solid1.03 ms
vanilla Solid (hand-written ceiling)1.10 ms
next-yak (css prop)1.11 ms
next-yak (css prop) foldStatic: false1.11 ms
vanilla (hand-written ceiling)1.35 ms
StyleX without CSS layers (:not() specificity hack)1.53 ms
StyleX1.53 ms
Plumeria (classStyle prop)1.60 ms
@yak/solid (styled API, Solid 2) foldStatic: false1.61 ms
next-yak (styled API)2.19 ms
StyleX on Solid2.42 ms
next-yak (styled API) foldStatic: false2.46 ms
Emotion4.71 ms
styled-components7.59 ms
Goober8.27 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

Plumeria (classStyle prop)62 ms ±3%
vanilla (hand-written ceiling)65 ms ±4%
StyleX without CSS layers (:not() specificity hack)69 ms ±2%
next-yak (css prop)70 ms ±5%
next-yak (css prop) foldStatic: false71 ms ±5%
Plumeria on Solid71 ms ±2%
StyleX71 ms ±3%
next-yak (styled API)73 ms ±4%
vanilla Solid (hand-written ceiling)74 ms ±5%
next-yak (styled API) foldStatic: false76 ms ±8%
styled-components88 ms ±3%
Emotion89 ms ±2%
StyleX on Solid97 ms ±5%
@yak/solid (styled API, Solid 2)115 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false116 ms ±3%
Goober123 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid7.40 ms active(29.32 ms span)
vanilla Solid (hand-written ceiling)7.59 ms active(33.19 ms span)
StyleX on Solid9.48 ms active(25.67 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false11.01 ms active(25.71 ms span)
@yak/solid (styled API, Solid 2)11.07 ms active(29.88 ms span)
vanilla (hand-written ceiling)11.09 ms active(30.73 ms span)
Plumeria (classStyle prop)11.10 ms active(29.39 ms span)
next-yak (css prop)11.57 ms active(31.04 ms span)
next-yak (styled API) foldStatic: false11.57 ms active(21.98 ms span)
next-yak (css prop) foldStatic: false11.63 ms active(20.87 ms span)
StyleX11.88 ms active(35.25 ms span)
StyleX without CSS layers (:not() specificity hack)12.21 ms active(30.85 ms span)
styled-components12.62 ms active(34.41 ms span)
Emotion13.80 ms active(32.78 ms span)
next-yak (styled API)14.23 ms active(22.89 ms span)
Goober14.67 ms active(23.33 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

vanilla (hand-written ceiling)60 ms ±4%
vanilla Solid (hand-written ceiling)61 ms ±3%
Plumeria on Solid67 ms ±8%
Plumeria (classStyle prop)71 ms ±5%
next-yak (css prop) foldStatic: false71 ms ±6%
next-yak (styled API)73 ms ±3%
next-yak (styled API) foldStatic: false73 ms ±6%
styled-components74 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false74 ms ±5%
next-yak (css prop)75 ms ±8%
StyleX without CSS layers (:not() specificity hack)78 ms ±10%
StyleX78 ms ±10%
Emotion79 ms ±3%
@yak/solid (styled API, Solid 2)80 ms ±10%
StyleX on Solid84 ms ±8%
Goober85 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)1.87 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
Emotion1.91 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
styled-components2.10 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Goober2.33 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
vanilla (hand-written ceiling)2.40 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.40 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
next-yak (styled API) foldStatic: false2.42 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Plumeria (classStyle prop)2.51 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.55 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Plumeria on Solid2.60 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
next-yak (css prop)2.71 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)3.00 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX3.07 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
next-yak (styled API)3.09 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
StyleX on Solid3.78 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
next-yak (css prop) foldStatic: false6.76 ms active(33.30 ms span · 33.30 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

Plumeria on Solid53 ms ±3%
vanilla Solid (hand-written ceiling)56 ms ±6%
Plumeria (classStyle prop)70 ms ±5%
vanilla (hand-written ceiling)72 ms ±4%
StyleX on Solid78 ms ±10%
StyleX80 ms ±4%
next-yak (css prop) foldStatic: false80 ms ±2%
next-yak (css prop)81 ms ±5%
next-yak (styled API)81 ms ±4%
StyleX without CSS layers (:not() specificity hack)81 ms ±4%
next-yak (styled API) foldStatic: false82 ms ±4%
@yak/solid (styled API, Solid 2)97 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false99 ms ±6%
styled-components155 ms ±2%
Emotion157 ms ±2%
Goober2,304 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid9.38 ms active(21.07 ms span)
vanilla Solid (hand-written ceiling)10.21 ms active(17.33 ms span)
StyleX on Solid11.26 ms active(20.97 ms span)
@yak/solid (styled API, Solid 2)12.25 ms active(15.17 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false14.16 ms active(18.98 ms span)
vanilla (hand-written ceiling)16.10 ms active(42.83 ms span)
next-yak (css prop) foldStatic: false16.27 ms active(30.89 ms span)
Plumeria (classStyle prop)16.42 ms active(39.65 ms span)
StyleX17.22 ms active(39.68 ms span)
next-yak (css prop)17.48 ms active(37.65 ms span)
StyleX without CSS layers (:not() specificity hack)18.60 ms active(34.56 ms span)
next-yak (styled API)18.63 ms active(37.04 ms span)
next-yak (styled API) foldStatic: false19.46 ms active(34.13 ms span)
Goober20.50 ms active(41.25 ms span)
styled-components21.95 ms active(39.93 ms span)
Emotion25.47 ms active(51.13 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
EmotionChrome3.1 ms1 recalc
Firefox1.0 ms
next-yak (css prop)Chrome3.2 ms1 recalc
Firefox1.0 ms
next-yak (css prop) foldStatic: falseChrome3.0 ms1 recalc
Firefox1.0 ms
next-yak (styled API) foldStatic: falseChrome3.2 ms1 recalc
Firefox1.0 ms
styled-componentsChrome3.3 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome3.5 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome3.1 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome3.1 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome2.9 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome3.2 ms1 recalc
Firefox1.0 ms
Plumeria (classStyle prop)Chrome3.4 ms1 recalc
Firefox1.0 ms
Plumeria on SolidChrome2.9 ms1 recalc
Firefox1.0 ms
StyleX on SolidChrome2.9 ms1 recalc
Firefox1.0 ms
next-yak (styled API)Chrome3.1 ms1 recalc
Firefox1.0 ms
StyleXChrome3.3 ms1 recalc
Firefox1.0 ms
GooberChrome2.9 ms1 recalc
Firefox6.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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)5,502 B(0.0 / 0.3 / 5.0 kB)
next-yak (css prop) foldStatic: false5,793 B(0.4 / 0.1 / 5.1 kB)
next-yak (css prop)5,815 B(0.5 / 0.1 / 5.1 kB)
next-yak (styled API)6,405 B(1.1 / 0.1 / 5.0 kB)
next-yak (styled API) foldStatic: false6,535 B(1.3 / 0.1 / 5.0 kB)
Plumeria (classStyle prop)6,773 B(1.1 / 0.1 / 5.4 kB)
vanilla Solid (hand-written ceiling)8,278 B(0.0 / 0.3 / 7.8 kB)
Plumeria on Solid8,426 B(0.0 / 0.1 / 8.1 kB)
StyleX without CSS layers (:not() specificity hack)9,246 B(3.5 / 0.1 / 5.4 kB)
StyleX9,252 B(3.5 / 0.1 / 5.4 kB)
StyleX on Solid13,714 B(5.1 / 0.1 / 8.1 kB)
@yak/solid (styled API, Solid 2) foldStatic: false13,762 B(5.7 / 0.1 / 7.7 kB)
@yak/solid (styled API, Solid 2)14,413 B(6.3 / 0.1 / 7.7 kB)
Goober21,595 B(3.9 / 8.9 / 8.3 kB)
styled-components35,605 B(13.2 / 13.5 / 8.0 kB)
Emotion39,570 B(12.9 / 17.9 / 7.8 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.01082163244321005001,0002,0004,000
EmotionGoobernext-yak (styled API)next-yak (css prop)next-yak (css prop) foldStatic: falsenext-yak (styled API) foldStatic: falsestyled-componentsStyleX without CSS layers (:not() specificity hack)StyleXStyleX on Solidvanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: falsePlumeria (classStyle prop)Plumeria on Solid
Dynamic valuetranslateX (each lane's best practice)n = 1,000high cardinality

The same 1,000-unique-translateX workload as the direct case, but idiomatic. The value is one no build step can know, such as a position from a database record or from a drag-and-drop interaction. Lanes whose styles are fixed at build time pass it as an inline style over a static class, their documented answer to such values; StyleX uses its dynamic style function. The next-yak lanes write the same plain inline style here for comparison only: their own path, the interpolation compiled to a CSS variable, is what dyn-translate measures, so the gap between the two cases for those lanes is what that path costs. Compare with dyn-translate to see what the direct pattern costs each ecosystem.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

@yak/solid (styled API, Solid 2)2,197,112 r/s ±8%(0.46 ms / 1k elems)
vanilla Solid (hand-written ceiling)2,154,211 r/s ±5%(0.46 ms / 1k elems)
Plumeria on Solid2,056,170 r/s ±7%(0.49 ms / 1k elems)
cn (shadcn-ui) on Solid1,686,654 r/s ±6%(0.59 ms / 1k elems)
Plumeria (classStyle prop)1,367,506 r/s ±5%(0.73 ms / 1k elems)
Bamboo1,366,906 r/s ±4%(0.73 ms / 1k elems)
cn (shadcn-ui)1,344,523 r/s ±3%(0.74 ms / 1k elems)
vanilla (hand-written ceiling)1,319,352 r/s ±2%(0.76 ms / 1k elems)
next-yak (styled API)1,315,405 r/s ±4%(0.76 ms / 1k elems)
tailwind-merge1,185,274 r/s ±5%(0.84 ms / 1k elems)
next-yak (css prop) foldStatic: false1,094,136 r/s ±5%(0.91 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false1,063,000 r/s ±3%(0.94 ms / 1k elems)
StyleX811,106 r/s ±3%(1.23 ms / 1k elems)
next-yak (styled API) foldStatic: false810,318 r/s ±2%(1.23 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)807,937 r/s ±3%(1.24 ms / 1k elems)
next-yak (css prop)744,875 r/s ±2%(1.34 ms / 1k elems)
Panda (css fn)713,957 r/s ±3%(1.40 ms / 1k elems)
StyleX on Solid682,229 r/s ±3%(1.47 ms / 1k elems)
Goober652,386 r/s ±3%(1.53 ms / 1k elems)
styled-components536,581 r/s ±1%(1.86 ms / 1k elems)
Panda (style props)443,871 r/s ±1%(2.25 ms / 1k elems)
Emotion294,415 r/s ±1%(3.40 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

Plumeria on Solid1,643 req/s ±1%
vanilla Solid (hand-written ceiling)1,609 req/s ±1%
@yak/solid (styled API, Solid 2)1,604 req/s ±1%
cn (shadcn-ui) on Solid1,356 req/s ±1%
next-yak (styled API)1,163 req/s ±1%
vanilla (hand-written ceiling)1,156 req/s ±1%
next-yak (css prop)1,154 req/s ±1%
Bamboo1,152 req/s ±1%
Plumeria (classStyle prop)1,130 req/s ±1%
cn (shadcn-ui)1,111 req/s ±0%
tailwind-merge1,082 req/s ±2%
@yak/solid (styled API, Solid 2) foldStatic: false1,046 req/s ±0%
next-yak (css prop) foldStatic: false973 req/s ±1%
next-yak (styled API) foldStatic: false788 req/s ±0%
StyleX without CSS layers (:not() specificity hack)769 req/s ±1%
StyleX760 req/s ±1%
Goober657 req/s ±0%
StyleX on Solid585 req/s ±1%
Panda (css fn)583 req/s ±6%
styled-components537 req/s ±1%
Panda (style props)473 req/s ±0%
Emotion344 req/s ±0%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
cn (shadcn-ui) on Solid0.72 ms
Plumeria on Solid0.73 ms
@yak/solid (styled API, Solid 2) foldStatic: false0.88 ms
vanilla (hand-written ceiling)0.95 ms
@yak/solid (styled API, Solid 2)0.99 ms
next-yak (css prop)1.00 ms
tailwind-merge1.04 ms
vanilla Solid (hand-written ceiling)1.10 ms
next-yak (css prop) foldStatic: false1.11 ms
Bamboo1.34 ms
Panda (css fn)1.35 ms
cn (shadcn-ui)1.37 ms
StyleX without CSS layers (:not() specificity hack)1.39 ms
StyleX1.43 ms
Plumeria (classStyle prop)1.51 ms
next-yak (styled API)1.55 ms
Goober1.66 ms
next-yak (styled API) foldStatic: false1.86 ms
Panda (style props)2.21 ms
StyleX on Solid2.35 ms
styled-components2.48 ms
Emotion3.21 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

Bamboo62 ms ±3%
next-yak (css prop)62 ms ±3%
next-yak (styled API)62 ms ±3%
Plumeria (classStyle prop)63 ms ±4%
vanilla (hand-written ceiling)66 ms ±4%
Panda (css fn)66 ms ±2%
next-yak (css prop) foldStatic: false66 ms ±4%
cn (shadcn-ui)67 ms ±4%
StyleX68 ms ±4%
StyleX without CSS layers (:not() specificity hack)71 ms ±4%
next-yak (styled API) foldStatic: false71 ms ±13%
Plumeria on Solid72 ms ±4%
@yak/solid (styled API, Solid 2)73 ms ±3%
vanilla Solid (hand-written ceiling)73 ms ±6%
cn (shadcn-ui) on Solid75 ms ±2%
styled-components78 ms ±4%
Goober79 ms ±6%
Panda (style props)86 ms ±3%
tailwind-merge88 ms ±4%
Emotion91 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false95 ms ±4%
StyleX on Solid97 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid7.36 ms active(28.94 ms span)
@yak/solid (styled API, Solid 2)7.76 ms active(28.80 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false8.92 ms active(25.02 ms span)
vanilla Solid (hand-written ceiling)9.34 ms active(33.08 ms span)
cn (shadcn-ui) on Solid9.41 ms active(30.70 ms span)
StyleX on Solid9.68 ms active(32.40 ms span)
Plumeria (classStyle prop)10.70 ms active(28.03 ms span)
next-yak (css prop)11.09 ms active(39.94 ms span)
next-yak (css prop) foldStatic: false11.15 ms active(23.84 ms span)
next-yak (styled API) foldStatic: false11.33 ms active(33.44 ms span)
StyleX11.35 ms active(21.59 ms span)
next-yak (styled API)11.94 ms active(39.47 ms span)
styled-components12.22 ms active(39.66 ms span)
Goober12.75 ms active(34.29 ms span)
cn (shadcn-ui)13.03 ms active(33.69 ms span)
vanilla (hand-written ceiling)13.26 ms active(20.97 ms span)
Bamboo13.61 ms active(33.54 ms span)
StyleX without CSS layers (:not() specificity hack)13.68 ms active(38.20 ms span)
Emotion13.92 ms active(33.12 ms span)
Panda (css fn)14.33 ms active(32.39 ms span)
Panda (style props)15.29 ms active(33.82 ms span)
tailwind-merge21.16 ms active(32.22 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

next-yak (styled API)58 ms ±10%
vanilla Solid (hand-written ceiling)58 ms ±3%
cn (shadcn-ui) on Solid59 ms ±2%
@yak/solid (styled API, Solid 2)59 ms ±2%
next-yak (styled API) foldStatic: false60 ms ±3%
cn (shadcn-ui)60 ms ±3%
next-yak (css prop) foldStatic: false60 ms ±5%
next-yak (css prop)61 ms ±5%
vanilla (hand-written ceiling)62 ms ±5%
Goober62 ms ±3%
Panda (css fn)63 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false63 ms ±2%
Panda (style props)65 ms ±3%
tailwind-merge65 ms ±4%
Bamboo68 ms ±10%
styled-components69 ms ±4%
Plumeria on Solid71 ms ±5%
Plumeria (classStyle prop)71 ms ±8%
Emotion75 ms ±5%
StyleX83 ms ±12%
StyleX without CSS layers (:not() specificity hack)83 ms ±11%
StyleX on Solid85 ms ±9%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria (classStyle prop)1.77 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)1.83 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Plumeria on Solid1.90 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
tailwind-merge1.91 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)1.93 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
vanilla (hand-written ceiling)2.20 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Panda (style props)2.31 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
cn (shadcn-ui) on Solid2.39 ms active(33.39 ms span · 33.39 ms profiled median (n=5))
cn (shadcn-ui)2.46 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.50 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
Panda (css fn)2.54 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
StyleX2.59 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
next-yak (css prop) foldStatic: false2.71 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
next-yak (css prop)2.71 ms active(33.38 ms span · 33.38 ms profiled median (n=5))
next-yak (styled API)2.71 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.77 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
next-yak (styled API) foldStatic: false2.91 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Bamboo3.03 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
styled-components3.16 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
StyleX on Solid3.18 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Goober3.60 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
Emotion3.71 ms active(33.32 ms span · 33.32 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

@yak/solid (styled API, Solid 2)53 ms ±6%
Plumeria on Solid54 ms ±5%
vanilla Solid (hand-written ceiling)55 ms ±4%
cn (shadcn-ui) on Solid62 ms ±5%
Bamboo68 ms ±3%
next-yak (styled API)68 ms ±3%
vanilla (hand-written ceiling)70 ms ±3%
next-yak (css prop)71 ms ±3%
Plumeria (classStyle prop)71 ms ±3%
cn (shadcn-ui)73 ms ±4%
next-yak (css prop) foldStatic: false74 ms ±3%
next-yak (styled API) foldStatic: false74 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false77 ms ±4%
Panda (css fn)80 ms ±4%
StyleX without CSS layers (:not() specificity hack)81 ms ±5%
StyleX82 ms ±5%
StyleX on Solid86 ms ±9%
Goober89 ms ±1%
styled-components93 ms ±5%
tailwind-merge95 ms ±4%
Panda (style props)97 ms ±6%
Emotion104 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)8.81 ms active(31.08 ms span)
Plumeria on Solid9.00 ms active(26.83 ms span)
@yak/solid (styled API, Solid 2)9.35 ms active(27.16 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false10.76 ms active(29.11 ms span)
cn (shadcn-ui) on Solid11.15 ms active(19.11 ms span)
StyleX on Solid13.01 ms active(32.30 ms span)
next-yak (styled API)14.50 ms active(32.16 ms span)
vanilla (hand-written ceiling)14.95 ms active(29.49 ms span)
next-yak (styled API) foldStatic: false15.84 ms active(38.53 ms span)
StyleX without CSS layers (:not() specificity hack)16.12 ms active(41.50 ms span)
Plumeria (classStyle prop)16.59 ms active(35.44 ms span)
next-yak (css prop) foldStatic: false16.62 ms active(44.21 ms span)
next-yak (css prop)16.64 ms active(44.80 ms span)
StyleX16.91 ms active(42.85 ms span)
Panda (css fn)17.03 ms active(33.90 ms span)
Goober17.14 ms active(33.71 ms span)
Bamboo17.24 ms active(42.93 ms span)
styled-components18.02 ms active(34.24 ms span)
cn (shadcn-ui)18.04 ms active(34.73 ms span)
Panda (style props)19.31 ms active(35.31 ms span)
Emotion21.43 ms active(33.81 ms span)
tailwind-merge25.47 ms active(40.32 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
BambooChrome3.2 ms1 recalc
Firefox0.0 ms
cn (shadcn-ui)Chrome3.3 ms1 recalc
Firefox2.0 ms
EmotionChrome3.4 ms1 recalc
Firefox1.0 ms
GooberChrome2.8 ms1 recalc
Firefox0.0 ms
next-yak (styled API)Chrome2.8 ms1 recalc
Firefox1.0 ms
next-yak (css prop)Chrome3.1 ms1 recalc
Firefox1.0 ms
next-yak (css prop) foldStatic: falseChrome2.9 ms1 recalc
Firefox0.0 ms
next-yak (styled API) foldStatic: falseChrome2.8 ms1 recalc
Firefox0.0 ms
Panda (style props)Chrome3.3 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome3.2 ms1 recalc
Firefox1.0 ms
StyleXChrome3.2 ms1 recalc
Firefox1.0 ms
StyleX on SolidChrome3.1 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome3.0 ms1 recalc
Firefox1.0 ms
Plumeria (classStyle prop)Chrome3.5 ms1 recalc
Firefox1.0 ms
Plumeria on SolidChrome3.0 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome3.0 ms1 recalc
Firefox2.0 ms
styled-componentsChrome3.0 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome2.9 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome3.2 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome2.8 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome3.1 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome3.3 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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)5,502 B(0.0 / 0.3 / 5.0 kB)
next-yak (css prop) foldStatic: false5,888 B(0.4 / 0.1 / 5.2 kB)
next-yak (css prop)5,903 B(0.5 / 0.1 / 5.2 kB)
next-yak (styled API)6,595 B(1.1 / 0.1 / 5.2 kB)
next-yak (styled API) foldStatic: false6,728 B(1.3 / 0.1 / 5.2 kB)
Plumeria (classStyle prop)6,773 B(1.1 / 0.1 / 5.4 kB)
Bamboo7,638 B(2.0 / 0.1 / 5.4 kB)
vanilla Solid (hand-written ceiling)8,278 B(0.0 / 0.3 / 7.8 kB)
Plumeria on Solid8,426 B(0.0 / 0.1 / 8.1 kB)
StyleX without CSS layers (:not() specificity hack)9,246 B(3.5 / 0.1 / 5.4 kB)
StyleX9,252 B(3.5 / 0.1 / 5.4 kB)
Goober9,362 B(3.9 / 0.1 / 5.2 kB)
Panda (css fn)10,633 B(4.9 / 0.1 / 5.4 kB)
StyleX on Solid13,714 B(5.1 / 0.1 / 8.1 kB)
@yak/solid (styled API, Solid 2) foldStatic: false13,906 B(5.7 / 0.1 / 7.9 kB)
@yak/solid (styled API, Solid 2)14,580 B(6.3 / 0.1 / 7.9 kB)
Panda (style props)15,202 B(9.2 / 0.1 / 5.5 kB)
tailwind-merge16,444 B(10.4 / 0.5 / 5.2 kB)
Emotion18,596 B(12.9 / 0.1 / 5.2 kB)
cn (shadcn-ui)18,600 B(12.5 / 0.5 / 5.2 kB)
styled-components18,978 B(13.2 / 0.1 / 5.2 kB)
cn (shadcn-ui) on Solid21,037 B(12.1 / 0.5 / 8.0 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.03.87.711151005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-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)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: falsePlumeria (classStyle prop)Plumeria on Solid
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.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

StyleX without CSS layers (:not() specificity hack)1,461,670 r/s ±34%(0.68 ms / 1k elems)
StyleX1,444,584 r/s ±22%(0.69 ms / 1k elems)
Panda (recipe)1,239,830 r/s ±21%(0.81 ms / 1k elems)
@yak/solid (styled API, Solid 2)1,210,902 r/s ±15%(0.83 ms / 1k elems)
Plumeria (classStyle prop)1,195,189 r/s ±20%(0.84 ms / 1k elems)
cn (shadcn-ui) on Solid1,131,610 r/s ±15%(0.88 ms / 1k elems)
StyleX on Solid1,067,056 r/s ±11%(0.94 ms / 1k elems)
vanilla Solid (hand-written ceiling)1,058,673 r/s ±25%(0.94 ms / 1k elems)
tailwind-merge1,002,053 r/s ±11%(1.00 ms / 1k elems)
next-yak (css prop) foldStatic: false928,999 r/s ±13%(1.08 ms / 1k elems)
Bamboo891,555 r/s ±38%(1.12 ms / 1k elems)
next-yak (styled API)890,093 r/s ±39%(1.12 ms / 1k elems)
vanilla (hand-written ceiling)796,720 r/s ±51%(1.26 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false723,953 r/s ±21%(1.38 ms / 1k elems)
Plumeria on Solid625,394 r/s ±42%(1.60 ms / 1k elems)
next-yak (css prop)621,540 r/s ±14%(1.61 ms / 1k elems)
next-yak (styled API) foldStatic: false504,704 r/s ±19%(1.98 ms / 1k elems)
cn (shadcn-ui)492,181 r/s ±31%(2.03 ms / 1k elems)
Panda (css fn)447,480 r/s ±10%(2.23 ms / 1k elems)
Panda (style props)399,682 r/s ±4%(2.50 ms / 1k elems)
styled-components399,037 r/s ±17%(2.51 ms / 1k elems)
Goober250,543 r/s ±24%(3.99 ms / 1k elems)
Emotion222,432 r/s ±6%(4.50 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

@yak/solid (styled API, Solid 2)2,259 req/s ±1%
next-yak (styled API)1,515 req/s ±1%
vanilla (hand-written ceiling)1,340 req/s ±1%
next-yak (css prop)1,320 req/s ±0%
StyleX without CSS layers (:not() specificity hack)1,303 req/s ±0%
StyleX1,302 req/s ±1%
Plumeria (classStyle prop)1,128 req/s ±2%
vanilla Solid (hand-written ceiling)1,118 req/s ±0%
cn (shadcn-ui)991 req/s ±0%
Bamboo926 req/s ±4%
StyleX on Solid917 req/s ±0%
Plumeria on Solid915 req/s ±0%
next-yak (css prop) foldStatic: false882 req/s ±1%
cn (shadcn-ui) on Solid828 req/s ±0%
Panda (recipe)807 req/s ±1%
tailwind-merge780 req/s ±2%
@yak/solid (styled API, Solid 2) foldStatic: false734 req/s ±1%
next-yak (styled API) foldStatic: false615 req/s ±0%
Panda (css fn)592 req/s ±1%
styled-components451 req/s ±2%
Panda (style props)400 req/s ±0%
Goober395 req/s ±1%
Emotion311 req/s ±1%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
next-yak (styled API)0.80 ms
@yak/solid (styled API, Solid 2)0.89 ms
vanilla Solid (hand-written ceiling)0.92 ms
StyleX without CSS layers (:not() specificity hack)0.99 ms
cn (shadcn-ui)1.07 ms
Plumeria on Solid1.08 ms
next-yak (css prop)1.18 ms
cn (shadcn-ui) on Solid1.19 ms
StyleX on Solid1.31 ms
next-yak (css prop) foldStatic: false1.38 ms
@yak/solid (styled API, Solid 2) foldStatic: false1.38 ms
Bamboo1.42 ms
vanilla (hand-written ceiling)1.44 ms
StyleX1.50 ms
tailwind-merge1.57 ms
Plumeria (classStyle prop)1.63 ms
Panda (recipe)1.73 ms
Panda (css fn)1.92 ms
Panda (style props)2.27 ms
next-yak (styled API) foldStatic: false2.64 ms
styled-components2.65 ms
Goober2.88 ms
Emotion4.17 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

next-yak (styled API)49 ms ±5%
next-yak (css prop)63 ms ±5%
StyleX without CSS layers (:not() specificity hack)64 ms ±4%
Bamboo64 ms ±3%
Plumeria (classStyle prop)64 ms ±3%
StyleX65 ms ±2%
vanilla (hand-written ceiling)65 ms ±4%
@yak/solid (styled API, Solid 2)72 ms ±3%
cn (shadcn-ui)73 ms ±4%
next-yak (css prop) foldStatic: false74 ms ±7%
Panda (recipe)75 ms ±4%
Panda (css fn)75 ms ±3%
vanilla Solid (hand-written ceiling)76 ms ±4%
Plumeria on Solid76 ms ±4%
styled-components80 ms ±7%
next-yak (styled API) foldStatic: false80 ms ±4%
StyleX on Solid84 ms ±3%
cn (shadcn-ui) on Solid86 ms ±2%
Goober87 ms ±9%
tailwind-merge89 ms ±4%
Panda (style props)92 ms ±2%
Emotion97 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false125 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid7.71 ms active(27.27 ms span)
@yak/solid (styled API, Solid 2)7.92 ms active(18.18 ms span)
vanilla Solid (hand-written ceiling)8.21 ms active(19.27 ms span)
StyleX on Solid8.58 ms active(24.56 ms span)
cn (shadcn-ui) on Solid11.10 ms active(30.72 ms span)
next-yak (styled API)11.11 ms active(29.63 ms span)
next-yak (css prop) foldStatic: false11.11 ms active(30.85 ms span)
next-yak (css prop)11.41 ms active(36.09 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false11.48 ms active(22.16 ms span)
Bamboo11.54 ms active(35.34 ms span)
vanilla (hand-written ceiling)11.55 ms active(36.19 ms span)
Panda (css fn)11.58 ms active(29.52 ms span)
StyleX11.71 ms active(35.33 ms span)
styled-components12.05 ms active(34.32 ms span)
next-yak (styled API) foldStatic: false12.23 ms active(28.73 ms span)
Goober13.74 ms active(24.69 ms span)
Plumeria (classStyle prop)13.79 ms active(34.98 ms span)
StyleX without CSS layers (:not() specificity hack)14.12 ms active(36.57 ms span)
cn (shadcn-ui)14.18 ms active(26.51 ms span)
Emotion14.32 ms active(30.85 ms span)
Panda (style props)14.98 ms active(26.64 ms span)
Panda (recipe)15.69 ms active(34.76 ms span)
tailwind-merge18.63 ms active(33.06 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

next-yak (css prop)67 ms ±2%
next-yak (styled API)68 ms ±7%
@yak/solid (styled API, Solid 2)69 ms ±1%
vanilla (hand-written ceiling)69 ms ±1%
Panda (recipe)70 ms ±4%
vanilla Solid (hand-written ceiling)70 ms ±2%
cn (shadcn-ui)70 ms ±5%
Bamboo70 ms ±2%
StyleX71 ms ±6%
next-yak (css prop) foldStatic: false72 ms ±3%
tailwind-merge72 ms ±2%
cn (shadcn-ui) on Solid72 ms ±8%
Plumeria (classStyle prop)72 ms ±1%
Panda (css fn)73 ms ±9%
styled-components74 ms ±5%
StyleX on Solid74 ms ±8%
StyleX without CSS layers (:not() specificity hack)75 ms ±2%
Plumeria on Solid76 ms ±3%
next-yak (styled API) foldStatic: false76 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false78 ms ±6%
Goober78 ms ±4%
Panda (style props)80 ms ±6%
Emotion84 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)2.45 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
cn (shadcn-ui) on Solid2.58 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.59 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
Plumeria on Solid2.62 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
vanilla (hand-written ceiling)2.62 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Goober2.74 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
next-yak (styled API) foldStatic: false2.98 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
next-yak (css prop)3.05 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
StyleX on Solid3.16 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Plumeria (classStyle prop)3.20 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)3.21 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
Panda (style props)3.22 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
tailwind-merge3.28 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
next-yak (styled API)3.29 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Panda (recipe)3.30 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
Emotion3.32 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
cn (shadcn-ui)3.34 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
next-yak (css prop) foldStatic: false3.36 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
Panda (css fn)3.39 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
styled-components3.43 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)3.65 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
StyleX3.69 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Bamboo3.94 ms active(33.38 ms span · 33.38 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

@yak/solid (styled API, Solid 2)49 ms ±5%
next-yak (styled API)54 ms ±3%
Plumeria on Solid55 ms ±3%
vanilla Solid (hand-written ceiling)56 ms ±3%
StyleX on Solid63 ms ±6%
next-yak (css prop)64 ms ±3%
cn (shadcn-ui) on Solid65 ms ±3%
Plumeria (classStyle prop)66 ms ±3%
vanilla (hand-written ceiling)67 ms ±3%
StyleX68 ms ±3%
Bamboo69 ms ±2%
StyleX without CSS layers (:not() specificity hack)69 ms ±3%
cn (shadcn-ui)75 ms ±4%
next-yak (css prop) foldStatic: false77 ms ±2%
Panda (recipe)78 ms ±5%
Panda (css fn)81 ms ±2%
next-yak (styled API) foldStatic: false86 ms ±6%
Goober93 ms ±4%
styled-components94 ms ±2%
tailwind-merge94 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false98 ms ±6%
Panda (style props)99 ms ±2%
Emotion111 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)10.87 ms active(17.35 ms span)
vanilla Solid (hand-written ceiling)11.72 ms active(29.25 ms span)
Plumeria on Solid12.20 ms active(29.15 ms span)
cn (shadcn-ui) on Solid13.85 ms active(20.31 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false13.92 ms active(20.78 ms span)
StyleX on Solid14.02 ms active(20.39 ms span)
next-yak (css prop) foldStatic: false17.33 ms active(40.17 ms span)
next-yak (css prop)17.50 ms active(33.04 ms span)
Plumeria (classStyle prop)17.93 ms active(31.98 ms span)
StyleX17.99 ms active(31.80 ms span)
next-yak (styled API)18.22 ms active(40.77 ms span)
Panda (css fn)18.28 ms active(31.22 ms span)
vanilla (hand-written ceiling)18.34 ms active(33.89 ms span)
StyleX without CSS layers (:not() specificity hack)18.39 ms active(41.06 ms span)
next-yak (styled API) foldStatic: false18.74 ms active(43.03 ms span)
Panda (recipe)19.74 ms active(43.11 ms span)
Goober20.23 ms active(40.14 ms span)
Bamboo20.82 ms active(32.11 ms span)
Panda (style props)20.85 ms active(33.36 ms span)
cn (shadcn-ui)21.09 ms active(40.47 ms span)
styled-components23.10 ms active(36.88 ms span)
Emotion24.07 ms active(49.99 ms span)
tailwind-merge25.93 ms active(45.97 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
cn (shadcn-ui)Chrome5.1 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome5.0 ms1 recalc
Firefox1.0 ms
GooberChrome5.1 ms1 recalc
Firefox1.0 ms
next-yak (css prop)Chrome5.4 ms1 recalc
Firefox2.0 ms
next-yak (css prop) foldStatic: falseChrome4.7 ms1 recalc
Firefox1.0 ms
next-yak (styled API) foldStatic: falseChrome5.0 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome5.0 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome4.7 ms1 recalc
Firefox1.0 ms
Panda (recipe)Chrome5.1 ms1 recalc
Firefox2.0 ms
styled-componentsChrome5.5 ms1 recalc
Firefox3.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.3 ms1 recalc
Firefox2.0 ms
StyleXChrome5.1 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome5.4 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome5.2 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2)Chrome4.8 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.8 ms1 recalc
Firefox1.0 ms
Plumeria on SolidChrome5.0 ms1 recalc
Firefox1.0 ms
StyleX on SolidChrome5.4 ms1 recalc
Firefox2.0 ms
BambooChrome5.4 ms1 recalc
Firefox2.0 ms
next-yak (styled API)Chrome5.2 ms1 recalc
Firefox2.0 ms
tailwind-mergeChrome5.0 ms1 recalc
Firefox2.0 ms
EmotionChrome4.8 ms1 recalc
Firefox2.0 ms
Plumeria (classStyle prop)Chrome5.3 ms1 recalc
Firefox2.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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)3,789 B(0.0 / 0.5 / 3.2 kB)
next-yak (css prop) foldStatic: false3,850 B(0.4 / 0.2 / 3.1 kB)
next-yak (css prop)3,865 B(0.5 / 0.2 / 3.1 kB)
next-yak (styled API)4,557 B(1.1 / 0.2 / 3.1 kB)
next-yak (styled API) foldStatic: false4,694 B(1.3 / 0.2 / 3.1 kB)
Plumeria (classStyle prop)5,509 B(1.1 / 0.3 / 4.0 kB)
vanilla Solid (hand-written ceiling)5,837 B(0.0 / 0.5 / 5.2 kB)
Plumeria on Solid6,117 B(0.0 / 0.3 / 5.7 kB)
Panda (recipe)6,472 B(1.4 / 0.3 / 4.6 kB)
Bamboo6,567 B(2.0 / 0.3 / 4.1 kB)
Goober7,396 B(3.9 / 0.3 / 3.1 kB)
StyleX7,989 B(3.5 / 0.3 / 4.0 kB)
StyleX without CSS layers (:not() specificity hack)7,997 B(3.5 / 0.3 / 4.0 kB)
Panda (css fn)10,054 B(4.9 / 0.3 / 4.6 kB)
@yak/solid (styled API, Solid 2) foldStatic: false11,212 B(5.7 / 0.2 / 5.1 kB)
StyleX on Solid11,606 B(5.1 / 0.3 / 5.9 kB)
@yak/solid (styled API, Solid 2)12,041 B(6.3 / 0.2 / 5.2 kB)
Panda (style props)14,463 B(9.2 / 0.3 / 4.6 kB)
tailwind-merge15,529 B(10.4 / 0.7 / 4.0 kB)
Emotion16,693 B(12.9 / 0.3 / 3.1 kB)
styled-components17,128 B(13.2 / 0.3 / 3.2 kB)
cn (shadcn-ui)17,685 B(12.5 / 0.7 / 4.0 kB)
cn (shadcn-ui) on Solid18,894 B(12.1 / 0.7 / 5.7 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.04.59.014181005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-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)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: falsePlumeria (classStyle prop)Plumeria on Solid
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.

Source · generated HTML · generated CSS · rendered preview

rendered preview

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

@yak/solid (styled API, Solid 2)5,025,403 r/s ±27%(0.20 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false3,564,275 r/s ±7%(0.28 ms / 1k elems)
vanilla Solid (hand-written ceiling)3,120,300 r/s ±7%(0.32 ms / 1k elems)
StyleX on Solid3,044,474 r/s ±8%(0.33 ms / 1k elems)
Plumeria on Solid2,961,147 r/s ±6%(0.34 ms / 1k elems)
cn (shadcn-ui) on Solid2,095,966 r/s ±5%(0.48 ms / 1k elems)
next-yak (styled API)1,995,430 r/s ±4%(0.50 ms / 1k elems)
Bamboo1,678,993 r/s ±1%(0.60 ms / 1k elems)
vanilla (hand-written ceiling)1,669,867 r/s ±2%(0.60 ms / 1k elems)
StyleX1,657,531 r/s ±2%(0.60 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,648,421 r/s ±3%(0.61 ms / 1k elems)
Plumeria (classStyle prop)1,648,367 r/s ±2%(0.61 ms / 1k elems)
cn (shadcn-ui)1,544,898 r/s ±2%(0.65 ms / 1k elems)
tailwind-merge1,513,058 r/s ±3%(0.66 ms / 1k elems)
next-yak (css prop) foldStatic: false1,312,603 r/s ±2%(0.76 ms / 1k elems)
next-yak (styled API) foldStatic: false1,101,073 r/s ±2%(0.91 ms / 1k elems)
Panda (css fn)869,211 r/s ±7%(1.15 ms / 1k elems)
Goober812,242 r/s ±1%(1.23 ms / 1k elems)
next-yak (css prop)724,605 r/s ±11%(1.38 ms / 1k elems)
styled-components631,821 r/s ±1%(1.58 ms / 1k elems)
Panda (style props)494,656 r/s ±1%(2.02 ms / 1k elems)
Emotion328,600 r/s ±1%(3.04 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

@yak/solid (styled API, Solid 2)3,380 req/s ±0%
@yak/solid (styled API, Solid 2) foldStatic: false2,572 req/s ±1%
vanilla Solid (hand-written ceiling)2,393 req/s ±1%
StyleX on Solid2,257 req/s ±0%
Plumeria on Solid2,239 req/s ±1%
next-yak (styled API)1,744 req/s ±1%
cn (shadcn-ui) on Solid1,694 req/s ±1%
next-yak (css prop)1,509 req/s ±0%
vanilla (hand-written ceiling)1,485 req/s ±2%
StyleX without CSS layers (:not() specificity hack)1,463 req/s ±1%
StyleX1,446 req/s ±0%
Plumeria (classStyle prop)1,445 req/s ±0%
Bamboo1,424 req/s ±1%
cn (shadcn-ui)1,367 req/s ±1%
tailwind-merge1,350 req/s ±1%
next-yak (css prop) foldStatic: false1,286 req/s ±1%
next-yak (styled API) foldStatic: false1,140 req/s ±1%
Goober854 req/s ±1%
styled-components660 req/s ±0%
Panda (css fn)584 req/s ±0%
Panda (style props)513 req/s ±1%
Emotion401 req/s ±1%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
@yak/solid (styled API, Solid 2) foldStatic: false0.68 ms
StyleX on Solid0.73 ms
@yak/solid (styled API, Solid 2)0.77 ms
vanilla Solid (hand-written ceiling)0.77 ms
cn (shadcn-ui) on Solid1.01 ms
Plumeria on Solid1.04 ms
StyleX1.08 ms
next-yak (styled API)1.09 ms
tailwind-merge1.27 ms
Bamboo1.32 ms
Plumeria (classStyle prop)1.34 ms
StyleX without CSS layers (:not() specificity hack)1.34 ms
next-yak (css prop)1.36 ms
vanilla (hand-written ceiling)1.43 ms
next-yak (css prop) foldStatic: false1.45 ms
cn (shadcn-ui)1.47 ms
next-yak (styled API) foldStatic: false1.71 ms
Panda (css fn)1.79 ms
Goober1.96 ms
Panda (style props)2.22 ms
styled-components2.54 ms
Emotion2.64 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

next-yak (styled API)50 ms ±3%
StyleX53 ms ±2%
next-yak (css prop)53 ms ±3%
Bamboo54 ms ±4%
StyleX without CSS layers (:not() specificity hack)54 ms ±3%
vanilla (hand-written ceiling)55 ms ±4%
Plumeria (classStyle prop)55 ms ±3%
@yak/solid (styled API, Solid 2)65 ms ±5%
next-yak (css prop) foldStatic: false66 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false66 ms ±5%
next-yak (styled API) foldStatic: false68 ms ±7%
Panda (css fn)68 ms ±4%
vanilla Solid (hand-written ceiling)69 ms ±7%
cn (shadcn-ui)70 ms ±13%
Plumeria on Solid70 ms ±3%
StyleX on Solid71 ms ±4%
styled-components75 ms ±5%
Goober77 ms ±7%
cn (shadcn-ui) on Solid77 ms ±4%
tailwind-merge85 ms ±2%
Emotion86 ms ±3%
Panda (style props)89 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX on Solid6.70 ms active(23.03 ms span)
@yak/solid (styled API, Solid 2)7.04 ms active(31.41 ms span)
vanilla Solid (hand-written ceiling)7.07 ms active(17.05 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false7.61 ms active(16.65 ms span)
Plumeria on Solid8.64 ms active(19.78 ms span)
cn (shadcn-ui) on Solid9.07 ms active(31.93 ms span)
Plumeria (classStyle prop)10.54 ms active(34.91 ms span)
next-yak (css prop)10.96 ms active(33.68 ms span)
next-yak (css prop) foldStatic: false11.07 ms active(29.67 ms span)
StyleX without CSS layers (:not() specificity hack)11.33 ms active(36.54 ms span)
next-yak (styled API)11.33 ms active(38.81 ms span)
Bamboo11.36 ms active(39.15 ms span)
StyleX11.42 ms active(34.35 ms span)
Panda (css fn)11.73 ms active(32.16 ms span)
styled-components11.90 ms active(29.32 ms span)
next-yak (styled API) foldStatic: false12.22 ms active(35.35 ms span)
Goober12.29 ms active(26.25 ms span)
cn (shadcn-ui)12.96 ms active(23.40 ms span)
Emotion13.25 ms active(36.75 ms span)
vanilla (hand-written ceiling)13.46 ms active(22.61 ms span)
Panda (style props)17.85 ms active(49.89 ms span)
tailwind-merge20.81 ms active(33.60 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

Plumeria on Solid56 ms ±3%
next-yak (css prop) foldStatic: false57 ms ±4%
StyleX on Solid57 ms ±3%
@yak/solid (styled API, Solid 2)57 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false57 ms ±3%
Bamboo58 ms ±2%
next-yak (styled API)58 ms ±3%
vanilla Solid (hand-written ceiling)59 ms ±2%
StyleX without CSS layers (:not() specificity hack)59 ms ±3%
cn (shadcn-ui)59 ms ±4%
next-yak (css prop)59 ms ±4%
cn (shadcn-ui) on Solid59 ms ±3%
next-yak (styled API) foldStatic: false60 ms ±4%
StyleX60 ms ±7%
Plumeria (classStyle prop)60 ms ±3%
Panda (css fn)61 ms ±4%
tailwind-merge61 ms ±2%
Goober63 ms ±3%
styled-components63 ms ±4%
vanilla (hand-written ceiling)64 ms ±3%
Panda (style props)65 ms ±2%
Emotion71 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
next-yak (styled API)1.89 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
vanilla (hand-written ceiling)1.91 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
next-yak (css prop) foldStatic: false1.92 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Plumeria on Solid1.98 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)1.99 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)2.10 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
next-yak (styled API) foldStatic: false2.19 ms active(33.41 ms span · 33.41 ms profiled median (n=5))
Panda (style props)2.19 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
cn (shadcn-ui)2.23 ms active(33.20 ms span · 33.20 ms profiled median (n=5))
Bamboo2.26 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.28 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Plumeria (classStyle prop)2.29 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
cn (shadcn-ui) on Solid2.32 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.35 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
tailwind-merge2.38 ms active(33.38 ms span · 33.38 ms profiled median (n=5))
StyleX on Solid2.42 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
next-yak (css prop)2.69 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
styled-components2.69 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX2.80 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Panda (css fn)2.96 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Goober3.21 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Emotion3.21 ms active(33.31 ms span · 33.31 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

@yak/solid (styled API, Solid 2)34 ms ±5%
Plumeria on Solid44 ms ±3%
vanilla Solid (hand-written ceiling)45 ms ±4%
StyleX on Solid46 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false46 ms ±4%
next-yak (styled API)52 ms ±5%
cn (shadcn-ui) on Solid56 ms ±4%
next-yak (css prop)56 ms ±4%
StyleX without CSS layers (:not() specificity hack)56 ms ±4%
StyleX58 ms ±5%
Bamboo58 ms ±3%
Plumeria (classStyle prop)58 ms ±5%
vanilla (hand-written ceiling)58 ms ±3%
next-yak (styled API) foldStatic: false69 ms ±4%
next-yak (css prop) foldStatic: false71 ms ±3%
cn (shadcn-ui)72 ms ±3%
Panda (css fn)74 ms ±3%
Goober77 ms ±4%
styled-components83 ms ±3%
tailwind-merge91 ms ±3%
Panda (style props)94 ms ±2%
Emotion97 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)9.83 ms active(16.39 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false10.13 ms active(26.41 ms span)
Plumeria on Solid10.37 ms active(18.64 ms span)
StyleX on Solid10.61 ms active(18.15 ms span)
vanilla Solid (hand-written ceiling)11.39 ms active(23.49 ms span)
cn (shadcn-ui) on Solid14.67 ms active(14.70 ms span)
StyleX without CSS layers (:not() specificity hack)16.88 ms active(29.41 ms span)
vanilla (hand-written ceiling)17.14 ms active(41.29 ms span)
next-yak (styled API)17.28 ms active(33.68 ms span)
next-yak (css prop)17.37 ms active(43.94 ms span)
next-yak (css prop) foldStatic: false17.65 ms active(30.22 ms span)
Bamboo17.68 ms active(38.70 ms span)
next-yak (styled API) foldStatic: false17.78 ms active(43.16 ms span)
Plumeria (classStyle prop)17.89 ms active(44.00 ms span)
cn (shadcn-ui)18.95 ms active(40.25 ms span)
Goober19.22 ms active(29.98 ms span)
styled-components19.75 ms active(33.14 ms span)
Panda (css fn)20.26 ms active(45.30 ms span)
StyleX20.36 ms active(38.45 ms span)
Panda (style props)20.62 ms active(44.76 ms span)
Emotion20.94 ms active(44.33 ms span)
tailwind-merge26.21 ms active(39.06 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
BambooChrome5.1 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui)Chrome4.9 ms1 recalc
Firefox2.0 ms
EmotionChrome4.5 ms1 recalc
Firefox1.0 ms
GooberChrome4.7 ms1 recalc
Firefox1.0 ms
next-yak (styled API)Chrome5.1 ms1 recalc
Firefox2.0 ms
next-yak (css prop)Chrome4.5 ms1 recalc
Firefox1.0 ms
next-yak (css prop) foldStatic: falseChrome4.7 ms1 recalc
Firefox1.0 ms
next-yak (styled API) foldStatic: falseChrome5.0 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome5.0 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome5.1 ms1 recalc
Firefox1.0 ms
styled-componentsChrome5.1 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome4.6 ms1 recalc
Firefox1.0 ms
StyleXChrome5.3 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome4.6 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome4.6 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.5 ms1 recalc
Firefox1.0 ms
Plumeria (classStyle prop)Chrome5.3 ms1 recalc
Firefox2.0 ms
Plumeria on SolidChrome4.4 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome4.8 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome4.6 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui) on SolidChrome4.9 ms1 recalc
Firefox2.0 ms
tailwind-mergeChrome5.1 ms1 recalc
Firefox2.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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)2,700 B(0.0 / 0.3 / 2.4 kB)
next-yak (css prop) foldStatic: false3,137 B(0.4 / 0.1 / 2.5 kB)
next-yak (css prop)3,160 B(0.5 / 0.1 / 2.5 kB)
next-yak (styled API)3,850 B(1.1 / 0.1 / 2.5 kB)
next-yak (styled API) foldStatic: false3,980 B(1.3 / 0.1 / 2.5 kB)
Plumeria (classStyle prop)4,201 B(1.1 / 0.2 / 2.8 kB)
Bamboo5,119 B(2.0 / 0.2 / 2.8 kB)
vanilla Solid (hand-written ceiling)5,340 B(0.0 / 0.3 / 5.0 kB)
Plumeria on Solid5,506 B(0.0 / 0.2 / 5.2 kB)
StyleX without CSS layers (:not() specificity hack)6,567 B(3.5 / 0.2 / 2.7 kB)
StyleX6,571 B(3.5 / 0.2 / 2.7 kB)
Goober6,731 B(3.9 / 0.1 / 2.6 kB)
Panda (css fn)8,347 B(4.9 / 0.2 / 3.1 kB)
StyleX on Solid10,801 B(5.1 / 0.2 / 5.2 kB)
@yak/solid (styled API, Solid 2) foldStatic: false10,983 B(5.7 / 0.1 / 4.9 kB)
@yak/solid (styled API, Solid 2)11,763 B(6.3 / 0.1 / 5.1 kB)
Panda (style props)12,756 B(9.2 / 0.2 / 3.1 kB)
tailwind-merge14,202 B(10.4 / 0.6 / 2.9 kB)
Emotion15,877 B(12.9 / 0.2 / 2.4 kB)
styled-components16,337 B(13.2 / 0.2 / 2.6 kB)
cn (shadcn-ui)16,358 B(12.5 / 0.6 / 2.9 kB)
cn (shadcn-ui) on Solid18,451 B(12.1 / 0.6 / 5.4 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.03.26.39.5131005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-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)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: falsePlumeria (classStyle prop)Plumeria on Solid
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.

Source · generated HTML · generated CSS · rendered preview

rendered preview

This case is a wrapper chain: each of three levels adds its own style and hands the accumulated styles down to the next component. Plumeria lets a style value cross one component boundary, but the component that receives it has to put it on an element itself; relaying a received classStyle to another component is a build error, because the compiler resolves composition at the call site where the styles are written.

Joining independently resolved class strings at runtime would get the chain through, but it would bypass Plumeria's conflict resolution and make the result depend on class order. So the lane sits this case out rather than measure a workaround. Source: the lane author's note on the pull request.

This case is a wrapper chain: each of three levels adds its own style and hands the accumulated styles down to the next component. Plumeria lets a style value cross one component boundary, but the component that receives it has to put it on an element itself; relaying a received classStyle to another component is a build error, because the compiler resolves composition at the call site where the styles are written.

Joining independently resolved class strings at runtime would get the chain through, but it would bypass Plumeria's conflict resolution and make the result depend on class order. So the lane sits this case out rather than measure a workaround. Source: the lane author's note on the pull request.

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

@yak/solid (styled API, Solid 2)4,694,175 r/s ±28%(0.21 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false3,502,394 r/s ±6%(0.29 ms / 1k elems)
next-yak (styled API)1,987,933 r/s ±3%(0.50 ms / 1k elems)
vanilla Solid (hand-written ceiling)1,159,721 r/s ±7%(0.86 ms / 1k elems)
Bamboo1,144,370 r/s ±4%(0.87 ms / 1k elems)
cn (shadcn-ui)1,117,387 r/s ±3%(0.89 ms / 1k elems)
vanilla (hand-written ceiling)1,115,703 r/s ±2%(0.90 ms / 1k elems)
cn (shadcn-ui) on Solid1,099,811 r/s ±5%(0.91 ms / 1k elems)
next-yak (styled API) foldStatic: false939,789 r/s ±1%(1.06 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)834,640 r/s ±2%(1.20 ms / 1k elems)
StyleX828,017 r/s ±2%(1.21 ms / 1k elems)
StyleX on Solid748,421 r/s ±3%(1.34 ms / 1k elems)
next-yak (css prop) foldStatic: false732,778 r/s ±2%(1.36 ms / 1k elems)
tailwind-merge679,250 r/s ±3%(1.47 ms / 1k elems)
styled-components575,931 r/s ±1%(1.74 ms / 1k elems)
Panda (style props)538,767 r/s ±1%(1.86 ms / 1k elems)
Goober400,678 r/s ±1%(2.50 ms / 1k elems)
Panda (css fn)355,780 r/s ±2%(2.81 ms / 1k elems)
Emotion290,145 r/s ±1%(3.45 ms / 1k elems)
next-yak (css prop)287,702 r/s ±57%(3.48 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

@yak/solid (styled API, Solid 2)3,366 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false2,505 req/s ±1%
next-yak (styled API)1,728 req/s ±1%
vanilla (hand-written ceiling)1,059 req/s ±0%
vanilla Solid (hand-written ceiling)986 req/s ±1%
next-yak (styled API) foldStatic: false940 req/s ±2%
cn (shadcn-ui)855 req/s ±0%
StyleX825 req/s ±1%
Bamboo824 req/s ±1%
StyleX without CSS layers (:not() specificity hack)806 req/s ±1%
cn (shadcn-ui) on Solid770 req/s ±0%
next-yak (css prop)732 req/s ±3%
next-yak (css prop) foldStatic: false729 req/s ±2%
StyleX on Solid640 req/s ±1%
styled-components596 req/s ±1%
tailwind-merge580 req/s ±0%
Panda (style props)519 req/s ±1%
Goober424 req/s ±0%
Emotion346 req/s ±0%
Panda (css fn)326 req/s ±1%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
@yak/solid (styled API, Solid 2)0.45 ms
@yak/solid (styled API, Solid 2) foldStatic: false0.45 ms
vanilla Solid (hand-written ceiling)0.94 ms
cn (shadcn-ui)0.96 ms
cn (shadcn-ui) on Solid1.04 ms
vanilla (hand-written ceiling)1.04 ms
next-yak (styled API)1.16 ms
Bamboo1.35 ms
StyleX1.49 ms
StyleX without CSS layers (:not() specificity hack)1.51 ms
tailwind-merge1.55 ms
next-yak (css prop) foldStatic: false1.61 ms
next-yak (css prop)1.65 ms
next-yak (styled API) foldStatic: false1.98 ms
Panda (style props)2.20 ms
styled-components2.23 ms
StyleX on Solid2.25 ms
Goober3.06 ms
Panda (css fn)3.11 ms
Emotion3.39 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

next-yak (styled API)50 ms ±3%
Bamboo66 ms ±4%
next-yak (styled API) foldStatic: false66 ms ±6%
@yak/solid (styled API, Solid 2) foldStatic: false66 ms ±4%
@yak/solid (styled API, Solid 2)67 ms ±4%
vanilla (hand-written ceiling)68 ms ±4%
next-yak (css prop) foldStatic: false72 ms ±3%
next-yak (css prop)73 ms ±3%
StyleX74 ms ±4%
StyleX without CSS layers (:not() specificity hack)74 ms ±5%
styled-components77 ms ±5%
cn (shadcn-ui)77 ms ±5%
vanilla Solid (hand-written ceiling)82 ms ±3%
cn (shadcn-ui) on Solid87 ms ±2%
Panda (style props)88 ms ±3%
Goober88 ms ±5%
tailwind-merge90 ms ±3%
StyleX on Solid92 ms ±3%
Panda (css fn)92 ms ±1%
Emotion92 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false6.72 ms active(23.80 ms span)
@yak/solid (styled API, Solid 2)6.89 ms active(17.74 ms span)
vanilla Solid (hand-written ceiling)7.73 ms active(25.39 ms span)
StyleX on Solid9.36 ms active(12.18 ms span)
cn (shadcn-ui) on Solid9.98 ms active(35.34 ms span)
next-yak (styled API)10.86 ms active(36.55 ms span)
next-yak (css prop) foldStatic: false11.22 ms active(22.48 ms span)
next-yak (styled API) foldStatic: false11.30 ms active(31.80 ms span)
next-yak (css prop)11.96 ms active(30.44 ms span)
styled-components11.96 ms active(31.29 ms span)
StyleX without CSS layers (:not() specificity hack)12.14 ms active(36.79 ms span)
StyleX12.23 ms active(32.64 ms span)
Panda (css fn)12.65 ms active(25.25 ms span)
Goober12.82 ms active(23.78 ms span)
Emotion12.95 ms active(33.09 ms span)
cn (shadcn-ui)14.03 ms active(34.01 ms span)
vanilla (hand-written ceiling)14.08 ms active(27.94 ms span)
Bamboo14.21 ms active(32.88 ms span)
Panda (style props)15.39 ms active(33.30 ms span)
tailwind-merge22.89 ms active(26.91 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

@yak/solid (styled API, Solid 2)57 ms ±5%
next-yak (styled API)59 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false59 ms ±3%
cn (shadcn-ui) on Solid59 ms ±4%
Bamboo60 ms ±6%
next-yak (styled API) foldStatic: false61 ms ±4%
StyleX on Solid62 ms ±3%
cn (shadcn-ui)62 ms ±5%
vanilla Solid (hand-written ceiling)63 ms ±1%
StyleX64 ms ±3%
vanilla (hand-written ceiling)65 ms ±3%
next-yak (css prop) foldStatic: false65 ms ±3%
next-yak (css prop)67 ms ±3%
StyleX without CSS layers (:not() specificity hack)67 ms ±4%
tailwind-merge68 ms ±6%
Panda (style props)68 ms ±5%
styled-components68 ms ±5%
Panda (css fn)76 ms ±4%
Goober77 ms ±3%
Emotion78 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX on Solid1.82 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false1.94 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Panda (style props)2.03 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
next-yak (css prop)2.05 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)2.09 ms active(33.25 ms span · 33.25 ms profiled median (n=5))
Panda (css fn)2.19 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
styled-components2.28 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
cn (shadcn-ui)2.30 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Emotion2.31 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
tailwind-merge2.35 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.37 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Goober2.41 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
cn (shadcn-ui) on Solid2.44 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.48 ms active(33.45 ms span · 33.45 ms profiled median (n=5))
next-yak (css prop) foldStatic: false2.52 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX2.66 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Bamboo2.71 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
next-yak (styled API)3.02 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
next-yak (styled API) foldStatic: false3.14 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
vanilla (hand-written ceiling)3.39 ms active(33.30 ms span · 33.30 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

@yak/solid (styled API, Solid 2)33 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false46 ms ±3%
next-yak (styled API)53 ms ±3%
vanilla Solid (hand-written ceiling)60 ms ±12%
cn (shadcn-ui) on Solid64 ms ±2%
next-yak (styled API) foldStatic: false70 ms ±3%
Bamboo71 ms ±2%
StyleX on Solid72 ms ±5%
vanilla (hand-written ceiling)74 ms ±5%
StyleX without CSS layers (:not() specificity hack)75 ms ±4%
next-yak (css prop) foldStatic: false76 ms ±3%
next-yak (css prop)78 ms ±4%
StyleX78 ms ±3%
cn (shadcn-ui)78 ms ±5%
styled-components89 ms ±6%
Goober92 ms ±3%
Panda (style props)92 ms ±6%
tailwind-merge97 ms ±4%
Panda (css fn)98 ms ±3%
Emotion100 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false10.17 ms active(24.80 ms span)
vanilla Solid (hand-written ceiling)11.17 ms active(19.37 ms span)
@yak/solid (styled API, Solid 2)11.60 ms active(23.11 ms span)
cn (shadcn-ui) on Solid13.29 ms active(15.68 ms span)
StyleX on Solid13.64 ms active(15.30 ms span)
Bamboo17.86 ms active(39.21 ms span)
StyleX17.96 ms active(39.00 ms span)
next-yak (css prop)18.25 ms active(36.27 ms span)
vanilla (hand-written ceiling)18.43 ms active(42.99 ms span)
next-yak (styled API) foldStatic: false18.57 ms active(40.77 ms span)
next-yak (styled API)18.66 ms active(29.58 ms span)
Panda (css fn)19.41 ms active(36.08 ms span)
Goober19.76 ms active(46.08 ms span)
next-yak (css prop) foldStatic: false20.44 ms active(46.20 ms span)
cn (shadcn-ui)20.56 ms active(37.63 ms span)
StyleX without CSS layers (:not() specificity hack)20.60 ms active(38.03 ms span)
styled-components20.72 ms active(39.60 ms span)
Emotion21.68 ms active(38.27 ms span)
Panda (style props)22.93 ms active(38.60 ms span)
tailwind-merge29.38 ms active(37.40 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
cn (shadcn-ui)Chrome5.0 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome4.6 ms1 recalc
Firefox1.0 ms
EmotionChrome4.7 ms1 recalc
Firefox1.0 ms
next-yak (styled API)Chrome4.9 ms1 recalc
Firefox1.0 ms
next-yak (css prop)Chrome4.9 ms1 recalc
Firefox1.0 ms
next-yak (css prop) foldStatic: falseChrome4.9 ms1 recalc
Firefox2.0 ms
next-yak (styled API) foldStatic: falseChrome5.3 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome5.2 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome5.4 ms1 recalc
Firefox1.0 ms
styled-componentsChrome4.8 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.1 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome5.1 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome5.0 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.5 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome4.4 ms1 recalc
Firefox2.0 ms
GooberChrome4.8 ms1 recalc
Firefox3.0 ms
vanilla Solid (hand-written ceiling)Chrome4.8 ms1 recalc
Firefox2.0 ms
BambooChrome4.6 ms1 recalc
Firefox2.0 ms
tailwind-mergeChrome5.0 ms1 recalc
Firefox2.0 ms
StyleXChrome5.0 ms1 recalc
Firefox2.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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)3,044 B(0.0 / 0.4 / 2.6 kB)
next-yak (css prop) foldStatic: false3,261 B(0.4 / 0.2 / 2.6 kB)
next-yak (css prop)3,279 B(0.5 / 0.2 / 2.6 kB)
next-yak (styled API)3,975 B(1.1 / 0.2 / 2.6 kB)
next-yak (styled API) foldStatic: false4,097 B(1.3 / 0.2 / 2.6 kB)
vanilla Solid (hand-written ceiling)5,191 B(0.0 / 0.4 / 4.7 kB)
Bamboo5,513 B(2.0 / 0.2 / 3.1 kB)
StyleX6,793 B(3.5 / 0.3 / 2.9 kB)
StyleX without CSS layers (:not() specificity hack)6,795 B(3.5 / 0.3 / 2.9 kB)
Goober6,811 B(3.9 / 0.2 / 2.6 kB)
Panda (css fn)8,513 B(4.9 / 0.2 / 3.2 kB)
StyleX on Solid11,128 B(5.1 / 0.3 / 5.5 kB)
@yak/solid (styled API, Solid 2) foldStatic: false11,240 B(5.7 / 0.2 / 5.1 kB)
@yak/solid (styled API, Solid 2)11,856 B(6.3 / 0.2 / 5.1 kB)
Panda (style props)12,922 B(9.2 / 0.2 / 3.2 kB)
tailwind-merge14,487 B(10.4 / 0.7 / 3.1 kB)
Emotion15,929 B(12.9 / 0.2 / 2.4 kB)
styled-components16,600 B(13.2 / 0.3 / 2.7 kB)
cn (shadcn-ui)16,643 B(12.5 / 0.7 / 3.1 kB)
cn (shadcn-ui) on Solid18,930 B(12.1 / 0.7 / 5.8 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.03.67.211141005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-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)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: false
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.

Source · generated HTML · generated CSS · rendered preview

rendered preview

Same limit as compose-3, six levels deep. Each level adds a style and passes the accumulated styles to the next component, and Plumeria stops a received classStyle from travelling further than one component boundary: the receiver has to apply it to an element, not forward it, because composition is resolved where the styles are written.

Concatenating class strings at runtime would bypass Plumeria's conflict resolution and make the result order-dependent, so there is no cell. Source: the lane author's note on the pull request.

Same limit as compose-3, six levels deep. Each level adds a style and passes the accumulated styles to the next component, and Plumeria stops a received classStyle from travelling further than one component boundary: the receiver has to apply it to an element, not forward it, because composition is resolved where the styles are written.

Concatenating class strings at runtime would bypass Plumeria's conflict resolution and make the result order-dependent, so there is no cell. Source: the lane author's note on the pull request.

SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.

@yak/solid (styled API, Solid 2)5,342,965 r/s ±12%(0.19 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false3,559,441 r/s ±6%(0.28 ms / 1k elems)
next-yak (styled API)2,038,620 r/s ±2%(0.49 ms / 1k elems)
next-yak (styled API) foldStatic: false883,791 r/s ±1%(1.13 ms / 1k elems)
Bamboo758,356 r/s ±4%(1.32 ms / 1k elems)
vanilla (hand-written ceiling)757,258 r/s ±3%(1.32 ms / 1k elems)
cn (shadcn-ui)719,254 r/s ±1%(1.39 ms / 1k elems)
vanilla Solid (hand-written ceiling)675,203 r/s ±6%(1.48 ms / 1k elems)
cn (shadcn-ui) on Solid634,817 r/s ±6%(1.58 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)595,673 r/s ±2%(1.68 ms / 1k elems)
StyleX584,669 r/s ±1%(1.71 ms / 1k elems)
styled-components527,669 r/s ±1%(1.90 ms / 1k elems)
StyleX on Solid486,221 r/s ±4%(2.06 ms / 1k elems)
next-yak (css prop) foldStatic: false453,271 r/s ±2%(2.21 ms / 1k elems)
Panda (style props)411,006 r/s ±1%(2.43 ms / 1k elems)
next-yak (css prop)409,603 r/s ±15%(2.44 ms / 1k elems)
tailwind-merge382,273 r/s ±3%(2.62 ms / 1k elems)
Emotion238,681 r/s ±1%(4.19 ms / 1k elems)
Goober215,916 r/s ±1%(4.63 ms / 1k elems)
Panda (css fn)171,454 r/s ±2%(5.83 ms / 1k elems)

SSR throughput under load — requests / sec · higher is betteriUses autocannon to measure end-to-end HTTP throughput on this machine: sending a request, rendering the whole workload into an HTML fragment, and transferring and receiving the response. Clients keep concurrent connections open. Each request renders the full workload, so a workload of 400 product tiles counts as one request. Excludes build time, browser rendering and external network latency. Higher is better.

@yak/solid (styled API, Solid 2)3,156 req/s ±0%
@yak/solid (styled API, Solid 2) foldStatic: false2,419 req/s ±1%
next-yak (styled API)1,649 req/s ±2%
next-yak (styled API) foldStatic: false872 req/s ±2%
vanilla (hand-written ceiling)760 req/s ±1%
cn (shadcn-ui)639 req/s ±1%
vanilla Solid (hand-written ceiling)607 req/s ±1%
StyleX without CSS layers (:not() specificity hack)594 req/s ±0%
StyleX574 req/s ±2%
Bamboo566 req/s ±1%
styled-components542 req/s ±0%
cn (shadcn-ui) on Solid498 req/s ±1%
next-yak (css prop) foldStatic: false489 req/s ±0%
next-yak (css prop)478 req/s ±4%
StyleX on Solid441 req/s ±1%
Panda (style props)409 req/s ±1%
tailwind-merge345 req/s ±1%
Emotion304 req/s ±1%
Goober234 req/s ±1%
Panda (css fn)172 req/s ±0%

Server and load generator run in separate processes on this host. Bars show the median of round request rates. Responses contain rendered HTML fragments. Raw results retain each round's latency and error data; latency percentiles are not pooled across rounds.

Where the SSR render time goes — Node CPU profile · median ms / renderiOne server render, split into UI framework work (React or Solid), styling library runtime, and your components. Framework work can differ when a lane removes component calls.other is garbage collection and native work. Taken from a sampled CPU profile mapped back to source (web-performance-debugger 1.5.0).

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
@yak/solid (styled API, Solid 2) foldStatic: false0.58 ms
@yak/solid (styled API, Solid 2)0.72 ms
next-yak (styled API)1.06 ms
Bamboo1.45 ms
vanilla (hand-written ceiling)1.73 ms
vanilla Solid (hand-written ceiling)1.76 ms
next-yak (styled API) foldStatic: false1.80 ms
cn (shadcn-ui)1.83 ms
StyleX1.88 ms
StyleX without CSS layers (:not() specificity hack)1.96 ms
cn (shadcn-ui) on Solid2.01 ms
StyleX on Solid2.39 ms
next-yak (css prop)2.46 ms
next-yak (css prop) foldStatic: false2.48 ms
Panda (style props)2.56 ms
styled-components2.74 ms
tailwind-merge2.81 ms
Emotion3.80 ms
Goober5.50 ms
Panda (css fn)7.10 ms

Client hydration — repeated timing + Chrome-profiled span anatomyiThe browser gets finished HTML, then the framework takes it over — attaching event handlers and wiring up state without rebuilding the markup. That is hydration. Repeated timing starts on a ready page and ends at DOM commit: React useLayoutEffect or Solid flush, before paint. The second chart profiles a span through the next frame and shows JavaScript, style, layout and paint (web-performance-debugger 1.5.0). Lower is better.

next-yak (styled API)49 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false66 ms ±3%
@yak/solid (styled API, Solid 2)67 ms ±3%
next-yak (styled API) foldStatic: false69 ms ±6%
Bamboo71 ms ±2%
vanilla (hand-written ceiling)72 ms ±3%
styled-components76 ms ±2%
StyleX without CSS layers (:not() specificity hack)77 ms ±4%
StyleX78 ms ±3%
cn (shadcn-ui)80 ms ±2%
next-yak (css prop)83 ms ±2%
next-yak (css prop) foldStatic: false84 ms ±4%
Panda (style props)93 ms ±4%
vanilla Solid (hand-written ceiling)95 ms ±5%
Emotion98 ms ±3%
tailwind-merge98 ms ±2%
cn (shadcn-ui) on Solid99 ms ±3%
StyleX on Solid105 ms ±4%
Goober107 ms ±5%
Panda (css fn)121 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false6.88 ms active(29.84 ms span)
@yak/solid (styled API, Solid 2)6.90 ms active(19.34 ms span)
vanilla Solid (hand-written ceiling)8.03 ms active(26.78 ms span)
cn (shadcn-ui) on Solid10.46 ms active(31.01 ms span)
next-yak (styled API)10.57 ms active(33.24 ms span)
next-yak (styled API) foldStatic: false11.43 ms active(36.23 ms span)
vanilla (hand-written ceiling)12.20 ms active(35.03 ms span)
StyleX on Solid12.24 ms active(13.61 ms span)
styled-components12.40 ms active(29.24 ms span)
next-yak (css prop) foldStatic: false12.44 ms active(27.80 ms span)
StyleX without CSS layers (:not() specificity hack)12.49 ms active(35.26 ms span)
Bamboo12.63 ms active(33.08 ms span)
next-yak (css prop)12.81 ms active(34.80 ms span)
StyleX12.88 ms active(34.13 ms span)
Emotion14.62 ms active(26.15 ms span)
Goober15.17 ms active(23.34 ms span)
Panda (style props)16.34 ms active(26.98 ms span)
cn (shadcn-ui)16.51 ms active(24.19 ms span)
Panda (css fn)18.84 ms active(27.94 ms span)
tailwind-merge23.59 ms active(28.15 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Interaction update — repeated timing + Chrome-profiled span anatomyiEach instance's value changes from i to i + 1, so every element really updates. React sets state, Solid sets a signal; both are warmed first, and the reset sits outside the timer. This is not Google's INP — the timing stops at the first animation frame, and the profiled span adds one frame to catch rendering work. Cross-framework ratios describe the whole workload, including the framework. Use vanilla lanes as references; compilers and styled runtimes can also remove component work. Lower is better.

@yak/solid (styled API, Solid 2) foldStatic: false57 ms ±3%
@yak/solid (styled API, Solid 2)58 ms ±2%
next-yak (styled API)59 ms ±4%
vanilla Solid (hand-written ceiling)62 ms ±2%
cn (shadcn-ui) on Solid64 ms ±5%
next-yak (styled API) foldStatic: false64 ms ±5%
cn (shadcn-ui)65 ms ±2%
StyleX on Solid65 ms ±4%
styled-components65 ms ±5%
Bamboo67 ms ±3%
StyleX67 ms ±2%
tailwind-merge68 ms ±3%
vanilla (hand-written ceiling)70 ms ±5%
StyleX without CSS layers (:not() specificity hack)71 ms ±3%
next-yak (css prop)72 ms ±4%
Emotion73 ms ±5%
Panda (style props)73 ms ±6%
next-yak (css prop) foldStatic: false74 ms ±4%
Goober90 ms ±1%
Panda (css fn)94 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)1.95 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
next-yak (css prop) foldStatic: false1.95 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)1.99 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
next-yak (styled API) foldStatic: false2.03 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
StyleX2.09 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.18 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
cn (shadcn-ui) on Solid2.30 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
styled-components2.34 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Goober2.37 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
StyleX on Solid2.38 ms active(33.21 ms span · 33.21 ms profiled median (n=5))
cn (shadcn-ui)2.41 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
vanilla (hand-written ceiling)2.49 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
tailwind-merge2.66 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.71 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
next-yak (css prop)3.01 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Bamboo3.03 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
Panda (css fn)3.06 ms active(33.39 ms span · 33.39 ms profiled median (n=5))
next-yak (styled API)3.16 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Emotion3.28 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Panda (style props)4.16 ms active(33.29 ms span · 33.29 ms profiled median (n=5))

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Cold mount — repeated timing + Chrome-profiled span anatomyiThe workload renders into an empty root on a ready page. Repeated timing stops at DOM commit, before paint; it excludes page load and network work. A runtime library may insert CSS during that work. The separate profiled span includes the next frame's rendering work. Lower is better.

@yak/solid (styled API, Solid 2)34 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false44 ms ±5%
next-yak (styled API)55 ms ±4%
vanilla Solid (hand-written ceiling)68 ms ±4%
next-yak (styled API) foldStatic: false73 ms ±6%
cn (shadcn-ui) on Solid76 ms ±4%
Bamboo78 ms ±5%
vanilla (hand-written ceiling)80 ms ±3%
StyleX on Solid82 ms ±4%
StyleX85 ms ±4%
StyleX without CSS layers (:not() specificity hack)86 ms ±6%
cn (shadcn-ui)86 ms ±2%
next-yak (css prop)88 ms ±4%
next-yak (css prop) foldStatic: false88 ms ±4%
styled-components93 ms ±4%
Panda (style props)102 ms ±4%
Emotion107 ms ±6%
tailwind-merge107 ms ±3%
Goober116 ms ±2%
Panda (css fn)128 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)10.01 ms active(18.01 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false10.15 ms active(26.51 ms span)
vanilla Solid (hand-written ceiling)11.05 ms active(26.74 ms span)
StyleX on Solid12.72 ms active(22.97 ms span)
cn (shadcn-ui) on Solid14.53 ms active(17.27 ms span)
next-yak (styled API)17.36 ms active(43.92 ms span)
next-yak (styled API) foldStatic: false18.45 ms active(36.64 ms span)
next-yak (css prop)18.54 ms active(30.50 ms span)
vanilla (hand-written ceiling)18.78 ms active(41.38 ms span)
StyleX18.97 ms active(38.29 ms span)
Goober20.15 ms active(37.74 ms span)
cn (shadcn-ui)20.35 ms active(31.79 ms span)
Bamboo20.53 ms active(45.75 ms span)
Panda (css fn)20.68 ms active(32.20 ms span)
StyleX without CSS layers (:not() specificity hack)21.62 ms active(42.58 ms span)
next-yak (css prop) foldStatic: false21.63 ms active(34.23 ms span)
Panda (style props)22.12 ms active(34.41 ms span)
Emotion22.87 ms active(47.87 ms span)
styled-components24.56 ms active(47.07 ms span)
tailwind-merge26.18 ms active(42.39 ms span)

Chrome profile (web-performance-debugger 1.5.0); segments sum to the span wall time. Rank uses active time (wall minus idle). Each bar describes one named action, including its frame waits. Repeated actions use the occurrence with the lower-median profile duration. The timing median uses all recorded action durations and includes profiler overhead. The main timing charts use separate measurements.

Browser render-work on a cold mount — style-recalc / layout / paint · Chrome + FirefoxiThe browser's own work rather than JavaScript: recalculating styles, laying out and painting. This is where a library that writes CSS at runtime pays a tax the build-time ones avoid — it adds a style rule per instance, so the engine recalculates styles once per instance instead of once for the page.Chrome's honest signal is that recalc count, on the badge. Firefox reports sampled milliseconds instead, where a zero can mean "not sampled" rather than "no work". Compare within one engine. Lower is better.

style recalclayoutpaint (Chrome)
cn (shadcn-ui)Chrome4.7 ms1 recalc
Firefox1.0 ms
GooberChrome5.1 ms1 recalc
Firefox1.0 ms
next-yak (styled API)Chrome4.6 ms1 recalc
Firefox1.0 ms
next-yak (css prop)Chrome5.0 ms1 recalc
Firefox1.0 ms
next-yak (css prop) foldStatic: falseChrome5.1 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome4.8 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome5.0 ms1 recalc
Firefox2.0 ms
styled-componentsChrome5.1 ms1 recalc
Firefox2.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.0 ms1 recalc
Firefox2.0 ms
StyleXChrome5.0 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome4.5 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome4.8 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome5.0 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome4.6 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.5 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome5.0 ms1 recalc
Firefox2.0 ms
BambooChrome4.7 ms1 recalc
Firefox2.0 ms
next-yak (styled API) foldStatic: falseChrome4.7 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2)Chrome4.4 ms1 recalc
Firefox2.0 ms
EmotionChrome4.9 ms1 recalc
Firefox2.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: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)3,075 B(0.0 / 0.4 / 2.6 kB)
next-yak (css prop) foldStatic: false3,418 B(0.4 / 0.2 / 2.7 kB)
next-yak (css prop)3,432 B(0.5 / 0.2 / 2.7 kB)
next-yak (styled API)4,129 B(1.1 / 0.2 / 2.7 kB)
next-yak (styled API) foldStatic: false4,259 B(1.3 / 0.2 / 2.7 kB)
vanilla Solid (hand-written ceiling)5,670 B(0.0 / 0.4 / 5.1 kB)
Bamboo5,794 B(2.0 / 0.3 / 3.4 kB)
StyleX6,817 B(3.5 / 0.3 / 2.9 kB)
StyleX without CSS layers (:not() specificity hack)6,818 B(3.5 / 0.3 / 2.9 kB)
Goober7,124 B(3.9 / 0.2 / 2.8 kB)
Panda (css fn)8,513 B(4.9 / 0.2 / 3.2 kB)
StyleX on Solid11,171 B(5.1 / 0.3 / 5.5 kB)
@yak/solid (styled API, Solid 2) foldStatic: false11,369 B(5.7 / 0.2 / 5.2 kB)
@yak/solid (styled API, Solid 2)11,982 B(6.3 / 0.2 / 5.2 kB)
Panda (style props)12,922 B(9.2 / 0.2 / 3.2 kB)
tailwind-merge14,487 B(10.4 / 0.7 / 3.1 kB)
Emotion15,949 B(12.9 / 0.2 / 2.4 kB)
cn (shadcn-ui)16,643 B(12.5 / 0.7 / 3.1 kB)
styled-components16,875 B(13.2 / 0.3 / 2.9 kB)
cn (shadcn-ui) on Solid18,930 B(12.1 / 0.7 / 5.8 kB)

Scaling — SSR render time (ms) vs instance countiRender time as the workload grows from a handful of instances to thousands. A flatter line means the cost per element stays put as the page gets bigger.

0.06.11218241005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoobernext-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)StyleXStyleX on Solidtailwind-mergevanilla (hand-written ceiling)vanilla Solid (hand-written ceiling)@yak/solid (styled API, Solid 2)@yak/solid (styled API, Solid 2) foldStatic: false

Build time — full client build · lower is betteriHow long the production build takes — the same bundle measured for page bytes. cold clears the lane's caches and generated code first, so it pays for regenerating them; warm runs it again with nothing cleared. Median of 3. This is developer experience and it depends on the machine — it says nothing about what users get.

vanilla Solid (hand-written ceiling)109 msnone · warm 103 ms
Emotion127 msruntime · warm 128 ms
Goober128 msruntime · warm 125 ms
vanilla (hand-written ceiling)128 msnone · warm 129 ms
styled-components137 msruntime · warm 143 ms
cn (shadcn-ui) on Solid141 msutility · warm 134 ms
cn (shadcn-ui)151 msutility · warm 152 ms
tailwind-merge154 msutility · warm 153 ms
Plumeria on Solid165 msatomic · warm 154 ms
Plumeria (classStyle prop)167 msatomic · warm 162 ms
next-yak (styled API) foldStatic: false197 msextracted · warm 201 ms
@yak/solid (styled API, Solid 2)201 msextracted · warm 201 ms
@yak/solid (styled API, Solid 2) foldStatic: false203 msextracted · warm 205 ms
next-yak (styled API)205 msextracted · warm 199 ms
next-yak (css prop) foldStatic: false210 msextracted · warm 212 ms
next-yak (css prop)214 msextracted · warm 205 ms
Panda (recipe)457 msatomic · warm 461 ms
StyleX without CSS layers (:not() specificity hack)457 msatomic · warm 465 ms
StyleX462 msatomic · warm 460 ms
StyleX on Solid475 msatomic · warm 483 ms
Panda (css fn)476 msatomic · warm 468 ms
Panda (style props)481 msatomic · warm 476 ms
Bamboo1,313 msatomic · warm 1,283 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.