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)509,035 r/s ±22%(0.15 ms / 1k elems)
cn (shadcn-ui) on Solid448,843 r/s ±21%(0.17 ms / 1k elems)
Plumeria on Solid435,679 r/s ±5%(0.17 ms / 1k elems)
@yak/solid (styled API, Solid 2)327,004 r/s ±5%(0.23 ms / 1k elems)
StyleX on Solid197,426 r/s ±5%(0.38 ms / 1k elems)
vanilla (hand-written ceiling)138,601 r/s ±1%(0.53 ms / 1k elems)
Plumeria (classStyle prop)132,130 r/s ±2%(0.56 ms / 1k elems)
@yak/react (css prop)120,046 r/s ±2%(0.62 ms / 1k elems)
StyleX117,954 r/s ±3%(0.63 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)116,937 r/s ±3%(0.63 ms / 1k elems)
cn (shadcn-ui)113,101 r/s ±1%(0.65 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false109,094 r/s ±3%(0.68 ms / 1k elems)
@yak/react (styled API)106,807 r/s ±2%(0.69 ms / 1k elems)
@yak/react (css prop) foldStatic: false106,148 r/s ±3%(0.70 ms / 1k elems)
Bamboo97,972 r/s ±1%(0.76 ms / 1k elems)
tailwind-merge89,688 r/s ±1%(0.83 ms / 1k elems)
@yak/react (styled API) foldStatic: false61,624 r/s ±1%(1.20 ms / 1k elems)
Panda (css fn)53,800 r/s ±2%(1.38 ms / 1k elems)
Goober38,536 r/s ±1%(1.92 ms / 1k elems)
styled-components34,536 r/s ±1%(2.14 ms / 1k elems)
Panda (style props)23,475 r/s ±3%(3.16 ms / 1k elems)
Emotion20,903 r/s ±3%(3.54 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)480 req/s ±0%
@yak/solid (styled API, Solid 2)395 req/s ±0%
Plumeria on Solid280 req/s ±0%
vanilla (hand-written ceiling)221 req/s ±1%
@yak/react (css prop)213 req/s ±2%
StyleX on Solid212 req/s ±0%
@yak/react (styled API)190 req/s ±0%
@yak/react (css prop) foldStatic: false182 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false181 req/s ±2%
cn (shadcn-ui) on Solid178 req/s ±0%
StyleX163 req/s ±1%
StyleX without CSS layers (:not() specificity hack)163 req/s ±1%
Plumeria (classStyle prop)162 req/s ±1%
@yak/react (styled API) foldStatic: false128 req/s ±1%
cn (shadcn-ui)112 req/s ±1%
Bamboo105 req/s ±1%
tailwind-merge101 req/s ±2%
Goober82 req/s ±1%
styled-components76 req/s ±2%
Panda (css fn)75 req/s ±0%
Emotion56 req/s ±2%
Panda (style props)43 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
vanilla Solid (hand-written ceiling)1.36 ms
cn (shadcn-ui) on Solid1.93 ms
Plumeria on Solid1.94 ms
@yak/solid (styled API, Solid 2)2.11 ms
StyleX on Solid3.01 ms
vanilla (hand-written ceiling)3.48 ms
@yak/react (css prop)4.13 ms
StyleX4.32 ms
Plumeria (classStyle prop)4.48 ms
StyleX without CSS layers (:not() specificity hack)4.54 ms
cn (shadcn-ui)4.59 ms
@yak/react (css prop) foldStatic: false4.74 ms
@yak/react (styled API)5.10 ms
@yak/solid (styled API, Solid 2) foldStatic: false5.55 ms
Bamboo5.58 ms
tailwind-merge6.50 ms
@yak/react (styled API) foldStatic: false7.16 ms
Panda (css fn)9.96 ms
styled-components13.86 ms
Goober14.07 ms
Emotion21.27 ms
Panda (style props)21.44 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.

vanilla (hand-written ceiling)92 ms ±5%
Plumeria (classStyle prop)93 ms ±2%
Bamboo95 ms ±3%
@yak/react (css prop)98 ms ±3%
StyleX101 ms ±1%
StyleX without CSS layers (:not() specificity hack)105 ms ±2%
cn (shadcn-ui)107 ms ±3%
@yak/react (styled API)109 ms ±3%
@yak/react (css prop) foldStatic: false111 ms ±4%
tailwind-merge116 ms ±3%
Panda (css fn)117 ms ±4%
@yak/react (styled API) foldStatic: false133 ms ±6%
vanilla Solid (hand-written ceiling)145 ms ±3%
Plumeria on Solid149 ms ±5%
cn (shadcn-ui) on Solid152 ms ±5%
styled-components154 ms ±2%
Goober168 ms ±5%
@yak/solid (styled API, Solid 2)194 ms ±5%
StyleX on Solid197 ms ±2%
Panda (style props)214 ms ±3%
Emotion217 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false334 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria (classStyle prop)16.71 ms active(33.86 ms span)
vanilla (hand-written ceiling)17.67 ms active(28.27 ms span)
StyleX without CSS layers (:not() specificity hack)17.74 ms active(35.59 ms span)
@yak/react (css prop)17.89 ms active(43.14 ms span)
Bamboo18.23 ms active(34.22 ms span)
StyleX19.79 ms active(35.67 ms span)
@yak/react (css prop) foldStatic: false20.07 ms active(30.03 ms span)
@yak/react (styled API)20.18 ms active(33.13 ms span)
vanilla Solid (hand-written ceiling)20.70 ms active(20.73 ms span)
cn (shadcn-ui)21.81 ms active(32.67 ms span)
cn (shadcn-ui) on Solid22.48 ms active(23.26 ms span)
@yak/solid (styled API, Solid 2)23.93 ms active(23.95 ms span)
Plumeria on Solid23.98 ms active(24.35 ms span)
Panda (css fn)24.48 ms active(36.69 ms span)
@yak/react (styled API) foldStatic: false25.79 ms active(32.03 ms span)
StyleX on Solid26.45 ms active(26.49 ms span)
styled-components28.40 ms active(57.07 ms span)
tailwind-merge30.48 ms active(55.37 ms span)
Goober32.97 ms active(62.51 ms span)
Panda (style props)37.23 ms active(67.99 ms span)
Emotion39.10 ms active(65.78 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false41.36 ms active(41.43 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)270 ms ±9%
Plumeria on Solid273 ms ±3%
@yak/solid (styled API, Solid 2)281 ms ±5%
Bamboo284 ms ±6%
StyleX on Solid290 ms ±7%
vanilla (hand-written ceiling)300 ms ±3%
Panda (css fn)303 ms ±4%
@yak/react (css prop) foldStatic: false311 ms ±4%
@yak/react (css prop)314 ms ±6%
@yak/react (styled API) foldStatic: false314 ms ±4%
StyleX317 ms ±3%
@yak/react (styled API)318 ms ±4%
Plumeria (classStyle prop)324 ms ±9%
StyleX without CSS layers (:not() specificity hack)328 ms ±6%
Goober329 ms ±7%
Emotion341 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false345 ms ±2%
styled-components353 ms ±5%
cn (shadcn-ui) on Solid361 ms ±9%
Panda (style props)373 ms ±7%
tailwind-merge396 ms ±8%
cn (shadcn-ui)404 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)9.97 ms active(33.10 ms span · 33.10 ms profiled median (n=5))
vanilla (hand-written ceiling)11.25 ms active(32.47 ms span · 32.47 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false11.33 ms active(33.23 ms span · 33.23 ms profiled median (n=5))
Plumeria on Solid11.38 ms active(33.25 ms span · 33.25 ms profiled median (n=5))
StyleX on Solid12.12 ms active(33.14 ms span · 33.14 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)12.12 ms active(33.23 ms span · 33.23 ms profiled median (n=5))
@yak/react (css prop)12.23 ms active(34.05 ms span · 34.05 ms profiled median (n=5))
@yak/react (styled API)12.29 ms active(32.25 ms span · 32.25 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)12.82 ms active(32.44 ms span · 32.44 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false12.88 ms active(32.30 ms span · 32.30 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false13.46 ms active(32.41 ms span · 32.41 ms profiled median (n=5))
Plumeria (classStyle prop)13.75 ms active(32.24 ms span · 32.24 ms profiled median (n=5))
styled-components14.10 ms active(32.32 ms span · 32.32 ms profiled median (n=5))
cn (shadcn-ui) on Solid14.41 ms active(33.15 ms span · 33.15 ms profiled median (n=5))
Panda (css fn)14.66 ms active(32.10 ms span · 32.10 ms profiled median (n=5))
Panda (style props)14.98 ms active(33.98 ms span · 33.98 ms profiled median (n=5))
Goober15.01 ms active(33.88 ms span · 33.88 ms profiled median (n=5))
Bamboo15.71 ms active(34.41 ms span · 34.41 ms profiled median (n=5))
StyleX15.77 ms active(32.35 ms span · 32.35 ms profiled median (n=5))
Emotion18.52 ms active(34.08 ms span · 34.08 ms profiled median (n=5))
cn (shadcn-ui)19.15 ms active(30.68 ms span · 30.68 ms profiled median (n=5))
tailwind-merge19.29 ms active(30.74 ms span · 30.74 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)117 ms ±4%
Plumeria on Solid123 ms ±6%
vanilla (hand-written ceiling)126 ms ±4%
Bamboo129 ms ±4%
Plumeria (classStyle prop)130 ms ±3%
@yak/react (css prop)135 ms ±3%
cn (shadcn-ui)137 ms ±6%
cn (shadcn-ui) on Solid138 ms ±4%
StyleX without CSS layers (:not() specificity hack)138 ms ±2%
StyleX141 ms ±3%
@yak/react (css prop) foldStatic: false142 ms ±2%
@yak/react (styled API)147 ms ±4%
Panda (css fn)155 ms ±2%
tailwind-merge156 ms ±4%
@yak/solid (styled API, Solid 2)160 ms ±3%
@yak/react (styled API) foldStatic: false164 ms ±2%
StyleX on Solid175 ms ±4%
styled-components207 ms ±3%
Goober209 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false242 ms ±4%
Emotion264 ms ±3%
Panda (style props)266 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)29.85 ms active(29.91 ms span)
StyleX37.39 ms active(44.91 ms span)
Plumeria on Solid37.59 ms active(37.66 ms span)
@yak/react (styled API)37.72 ms active(53.98 ms span)
@yak/react (css prop)38.20 ms active(53.90 ms span)
Plumeria (classStyle prop)38.60 ms active(54.20 ms span)
StyleX on Solid39.05 ms active(39.12 ms span)
Panda (css fn)40.47 ms active(46.49 ms span)
vanilla (hand-written ceiling)40.75 ms active(54.72 ms span)
@yak/solid (styled API, Solid 2)41.28 ms active(41.32 ms span)
cn (shadcn-ui) on Solid43.02 ms active(43.10 ms span)
StyleX without CSS layers (:not() specificity hack)43.80 ms active(50.34 ms span)
@yak/react (css prop) foldStatic: false46.94 ms active(50.22 ms span)
Bamboo48.68 ms active(64.20 ms span)
tailwind-merge50.39 ms active(57.70 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false52.33 ms active(52.39 ms span)
cn (shadcn-ui)52.94 ms active(65.95 ms span)
@yak/react (styled API) foldStatic: false55.77 ms active(63.54 ms span)
Emotion57.14 ms active(73.19 ms span)
styled-components59.57 ms active(66.59 ms span)
Goober62.95 ms active(67.99 ms span)
Panda (style props)64.20 ms active(70.87 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/react (styled API) foldStatic: falseChrome21.1 ms1 recalc
Firefox11.0 ms
styled-componentsChrome17.2 ms1 recalc
Firefox12.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome17.6 ms1 recalc
Firefox10.0 ms
@yak/react (css prop) foldStatic: falseChrome19.2 ms1 recalc
Firefox12.0 ms
EmotionChrome16.7 ms1 recalc
Firefox13.0 ms
Panda (css fn)Chrome17.3 ms1 recalc
Firefox8.0 ms
@yak/react (styled API)Chrome16.6 ms1 recalc
Firefox12.0 ms
vanilla Solid (hand-written ceiling)Chrome16.2 ms1 recalc
Firefox11.0 ms
@yak/react (css prop)Chrome17.1 ms1 recalc
Firefox15.0 ms
vanilla (hand-written ceiling)Chrome16.1 ms1 recalc
Firefox10.0 ms
@yak/solid (styled API, Solid 2)Chrome17.1 ms1 recalc
Firefox13.0 ms
BambooChrome18.3 ms1 recalc
Firefox11.0 ms
Plumeria (classStyle prop)Chrome19.1 ms1 recalc
Firefox14.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome19.7 ms1 recalc
Firefox15.0 ms
Plumeria on SolidChrome21.5 ms1 recalc
Firefox13.0 ms
StyleXChrome17.0 ms1 recalc
Firefox15.0 ms
Panda (style props)Chrome19.5 ms1 recalc
Firefox14.0 ms
StyleX on SolidChrome17.2 ms1 recalc
Firefox12.9 ms
cn (shadcn-ui) on SolidChrome22.8 ms1 recalc
Firefox19.0 ms
tailwind-mergeChrome19.3 ms1 recalc
Firefox19.0 ms
GooberChrome19.5 ms1 recalc
Firefox17.0 ms
cn (shadcn-ui)Chrome21.3 ms1 recalc
Firefox19.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
@yak/react (css prop) foldStatic: false7,614 B(0.4 / 0.9 / 6.1 kB)
@yak/react (css prop)7,632 B(0.5 / 0.9 / 6.1 kB)
vanilla (hand-written ceiling)7,793 B(0.0 / 1.5 / 6.1 kB)
@yak/react (styled API)8,314 B(1.1 / 0.9 / 6.1 kB)
@yak/react (styled API) foldStatic: false8,439 B(1.3 / 0.9 / 6.1 kB)
vanilla Solid (hand-written ceiling)9,706 B(0.0 / 1.5 / 8.0 kB)
Plumeria (classStyle prop)11,287 B(1.1 / 1.4 / 8.5 kB)
Goober11,551 B(3.9 / 1.2 / 6.2 kB)
Plumeria on Solid13,033 B(0.4 / 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 Solid18,487 B(5.9 / 1.4 / 10.8 kB)
@yak/solid (styled API, Solid 2)18,875 B(7.1 / 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: false30,549 B(6.5 / 0.9 / 22.5 kB)
cn (shadcn-ui) on Solid31,514 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.050991491991005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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)407,226 r/s ±23%(0.18 ms / 1k elems)
Plumeria on Solid405,654 r/s ±9%(0.18 ms / 1k elems)
cn (shadcn-ui) on Solid355,230 r/s ±21%(0.21 ms / 1k elems)
Plumeria (classStyle prop)132,464 r/s ±1%(0.56 ms / 1k elems)
vanilla (hand-written ceiling)130,423 r/s ±5%(0.57 ms / 1k elems)
@yak/react (css prop)126,140 r/s ±2%(0.59 ms / 1k elems)
cn (shadcn-ui)111,200 r/s ±4%(0.67 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false105,913 r/s ±5%(0.70 ms / 1k elems)
@yak/solid (styled API, Solid 2)104,430 r/s ±7%(0.71 ms / 1k elems)
@yak/react (css prop) foldStatic: false104,357 r/s ±9%(0.71 ms / 1k elems)
Bamboo98,115 r/s ±2%(0.75 ms / 1k elems)
StyleX on Solid95,165 r/s ±4%(0.78 ms / 1k elems)
StyleX93,063 r/s ±3%(0.80 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)90,205 r/s ±3%(0.82 ms / 1k elems)
tailwind-merge87,799 r/s ±1%(0.84 ms / 1k elems)
@yak/react (styled API) foldStatic: false61,224 r/s ±2%(1.21 ms / 1k elems)
@yak/react (styled API)60,608 r/s ±1%(1.22 ms / 1k elems)
Panda (css fn)54,596 r/s ±1%(1.36 ms / 1k elems)
Goober35,427 r/s ±1%(2.09 ms / 1k elems)
styled-components33,748 r/s ±2%(2.19 ms / 1k elems)
Panda (style props)23,657 r/s ±2%(3.13 ms / 1k elems)
Emotion20,483 r/s ±3%(3.62 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)450 req/s ±2%
Plumeria on Solid281 req/s ±0%
vanilla (hand-written ceiling)220 req/s ±2%
@yak/react (css prop)212 req/s ±2%
@yak/react (css prop) foldStatic: false184 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false178 req/s ±0%
@yak/solid (styled API, Solid 2)177 req/s ±0%
cn (shadcn-ui) on Solid171 req/s ±0%
Plumeria (classStyle prop)162 req/s ±1%
StyleX without CSS layers (:not() specificity hack)138 req/s ±1%
StyleX135 req/s ±0%
StyleX on Solid132 req/s ±1%
@yak/react (styled API) foldStatic: false130 req/s ±1%
@yak/react (styled API)128 req/s ±2%
cn (shadcn-ui)113 req/s ±3%
Bamboo102 req/s ±1%
tailwind-merge102 req/s ±1%
styled-components75 req/s ±1%
Goober74 req/s ±1%
Panda (css fn)73 req/s ±1%
Emotion55 req/s ±1%
Panda (style props)43 req/s ±3%

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.51 ms
Plumeria on Solid1.69 ms
vanilla Solid (hand-written ceiling)1.93 ms
Plumeria (classStyle prop)3.54 ms
@yak/react (css prop)3.99 ms
vanilla (hand-written ceiling)4.18 ms
@yak/react (css prop) foldStatic: false4.95 ms
cn (shadcn-ui)5.12 ms
@yak/solid (styled API, Solid 2)5.29 ms
StyleX5.32 ms
StyleX without CSS layers (:not() specificity hack)5.48 ms
StyleX on Solid5.62 ms
tailwind-merge5.68 ms
@yak/solid (styled API, Solid 2) foldStatic: false5.74 ms
Bamboo5.87 ms
@yak/react (styled API) foldStatic: false7.16 ms
@yak/react (styled API)7.42 ms
Panda (css fn)9.74 ms
styled-components14.13 ms
Goober14.18 ms
Emotion19.32 ms
Panda (style props)20.91 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.

vanilla (hand-written ceiling)90 ms ±2%
Bamboo95 ms ±4%
Plumeria (classStyle prop)96 ms ±3%
@yak/react (css prop)100 ms ±4%
cn (shadcn-ui)105 ms ±3%
@yak/react (css prop) foldStatic: false111 ms ±3%
StyleX112 ms ±1%
StyleX without CSS layers (:not() specificity hack)113 ms ±5%
tailwind-merge119 ms ±3%
Panda (css fn)121 ms ±5%
@yak/react (styled API)132 ms ±2%
@yak/react (styled API) foldStatic: false135 ms ±4%
vanilla Solid (hand-written ceiling)147 ms ±4%
Plumeria on Solid148 ms ±3%
styled-components158 ms ±5%
cn (shadcn-ui) on Solid165 ms ±4%
Goober176 ms ±4%
Panda (style props)213 ms ±4%
Emotion216 ms ±4%
StyleX on Solid247 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false328 ms ±2%
@yak/solid (styled API, Solid 2)332 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)16.32 ms active(33.21 ms span)
Plumeria (classStyle prop)16.56 ms active(38.45 ms span)
Bamboo17.20 ms active(27.66 ms span)
@yak/react (css prop)17.33 ms active(38.92 ms span)
Plumeria on Solid18.63 ms active(18.65 ms span)
StyleX21.66 ms active(34.50 ms span)
cn (shadcn-ui) on Solid21.97 ms active(35.37 ms span)
cn (shadcn-ui)22.76 ms active(50.83 ms span)
vanilla Solid (hand-written ceiling)23.03 ms active(23.06 ms span)
@yak/react (css prop) foldStatic: false23.04 ms active(40.29 ms span)
Panda (css fn)23.27 ms active(27.27 ms span)
StyleX without CSS layers (:not() specificity hack)23.82 ms active(29.20 ms span)
@yak/react (styled API)24.54 ms active(33.06 ms span)
@yak/react (styled API) foldStatic: false26.05 ms active(26.14 ms span)
tailwind-merge29.17 ms active(61.11 ms span)
styled-components29.57 ms active(55.50 ms span)
StyleX on Solid32.73 ms active(32.77 ms span)
Goober33.28 ms active(65.36 ms span)
Emotion37.47 ms active(62.25 ms span)
Panda (style props)38.49 ms active(55.76 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false39.78 ms active(39.99 ms span)
@yak/solid (styled API, Solid 2)47.59 ms active(52.53 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)281 ms ±6%
Plumeria on Solid289 ms ±6%
vanilla (hand-written ceiling)299 ms ±3%
Plumeria (classStyle prop)303 ms ±2%
Bamboo306 ms ±2%
Panda (css fn)309 ms ±3%
StyleX310 ms ±6%
@yak/react (css prop)316 ms ±6%
StyleX on Solid317 ms ±8%
@yak/react (css prop) foldStatic: false320 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false322 ms ±5%
Goober335 ms ±4%
@yak/solid (styled API, Solid 2)338 ms ±6%
styled-components339 ms ±3%
@yak/react (styled API) foldStatic: false343 ms ±6%
Emotion347 ms ±6%
StyleX without CSS layers (:not() specificity hack)350 ms ±5%
cn (shadcn-ui) on Solid351 ms ±5%
@yak/react (styled API)362 ms ±4%
cn (shadcn-ui)387 ms ±5%
Panda (style props)391 ms ±5%
tailwind-merge400 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)10.72 ms active(33.15 ms span · 33.15 ms profiled median (n=5))
Plumeria on Solid11.40 ms active(33.18 ms span · 33.18 ms profiled median (n=5))
vanilla (hand-written ceiling)11.41 ms active(32.38 ms span · 32.38 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)11.56 ms active(33.16 ms span · 33.16 ms profiled median (n=5))
Panda (css fn)12.28 ms active(32.20 ms span · 32.20 ms profiled median (n=5))
@yak/react (styled API)12.55 ms active(33.94 ms span · 33.94 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false12.66 ms active(33.13 ms span · 33.13 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)12.73 ms active(33.99 ms span · 33.99 ms profiled median (n=5))
@yak/react (css prop)12.78 ms active(32.39 ms span · 32.39 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false13.37 ms active(34.03 ms span · 34.03 ms profiled median (n=5))
StyleX on Solid13.42 ms active(33.19 ms span · 33.19 ms profiled median (n=5))
Plumeria (classStyle prop)13.69 ms active(32.30 ms span · 32.30 ms profiled median (n=5))
Bamboo13.86 ms active(32.30 ms span · 32.30 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false14.22 ms active(32.29 ms span · 32.29 ms profiled median (n=5))
Goober14.52 ms active(32.31 ms span · 32.31 ms profiled median (n=5))
cn (shadcn-ui) on Solid14.76 ms active(32.99 ms span · 32.99 ms profiled median (n=5))
StyleX14.90 ms active(32.42 ms span · 32.42 ms profiled median (n=5))
Emotion15.66 ms active(34.17 ms span · 34.17 ms profiled median (n=5))
styled-components16.62 ms active(33.09 ms span · 33.09 ms profiled median (n=5))
Panda (style props)17.92 ms active(34.16 ms span · 34.16 ms profiled median (n=5))
cn (shadcn-ui)19.00 ms active(30.88 ms span · 30.88 ms profiled median (n=5))
tailwind-merge25.28 ms active(30.53 ms span · 30.53 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 Solid119 ms ±4%
vanilla Solid (hand-written ceiling)122 ms ±3%
vanilla (hand-written ceiling)126 ms ±2%
Bamboo129 ms ±3%
Plumeria (classStyle prop)131 ms ±5%
@yak/react (css prop)134 ms ±4%
cn (shadcn-ui) on Solid140 ms ±5%
@yak/react (css prop) foldStatic: false141 ms ±3%
cn (shadcn-ui)142 ms ±6%
StyleX152 ms ±5%
StyleX without CSS layers (:not() specificity hack)153 ms ±4%
tailwind-merge153 ms ±4%
Panda (css fn)156 ms ±3%
@yak/react (styled API) foldStatic: false167 ms ±3%
@yak/react (styled API)173 ms ±4%
styled-components210 ms ±3%
Goober218 ms ±2%
StyleX on Solid222 ms ±8%
@yak/solid (styled API, Solid 2)244 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false247 ms ±3%
Panda (style props)265 ms ±3%
Emotion270 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)31.95 ms active(31.99 ms span)
Plumeria on Solid37.72 ms active(37.80 ms span)
Plumeria (classStyle prop)38.95 ms active(44.63 ms span)
cn (shadcn-ui) on Solid41.00 ms active(41.05 ms span)
vanilla (hand-written ceiling)43.07 ms active(48.68 ms span)
@yak/react (css prop) foldStatic: false43.94 ms active(49.72 ms span)
StyleX on Solid44.05 ms active(44.10 ms span)
Panda (css fn)44.70 ms active(53.10 ms span)
@yak/react (css prop)44.92 ms active(56.08 ms span)
@yak/react (styled API)45.36 ms active(50.54 ms span)
Bamboo45.42 ms active(51.90 ms span)
@yak/react (styled API) foldStatic: false47.07 ms active(52.44 ms span)
cn (shadcn-ui)48.89 ms active(61.89 ms span)
StyleX without CSS layers (:not() specificity hack)48.92 ms active(51.70 ms span)
StyleX49.41 ms active(60.72 ms span)
tailwind-merge50.65 ms active(63.92 ms span)
@yak/solid (styled API, Solid 2)53.54 ms active(53.61 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false54.84 ms active(54.92 ms span)
styled-components55.46 ms active(69.90 ms span)
Panda (style props)56.44 ms active(70.40 ms span)
Goober56.97 ms active(63.56 ms span)
Emotion62.09 ms active(75.42 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)
vanilla Solid (hand-written ceiling)Chrome16.7 ms1 recalc
Firefox10.0 ms
StyleX on SolidChrome17.8 ms1 recalc
Firefox10.0 ms
@yak/react (css prop)Chrome19.5 ms1 recalc
Firefox11.0 ms
Panda (style props)Chrome18.2 ms1 recalc
Firefox10.0 ms
@yak/react (styled API) foldStatic: falseChrome17.6 ms1 recalc
Firefox13.0 ms
styled-componentsChrome17.1 ms1 recalc
Firefox11.0 ms
@yak/react (styled API)Chrome17.5 ms1 recalc
Firefox12.0 ms
vanilla (hand-written ceiling)Chrome17.7 ms1 recalc
Firefox10.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome20.5 ms1 recalc
Firefox12.0 ms
@yak/react (css prop) foldStatic: falseChrome17.9 ms1 recalc
Firefox12.0 ms
@yak/solid (styled API, Solid 2)Chrome18.6 ms1 recalc
Firefox10.0 ms
Panda (css fn)Chrome16.7 ms1 recalc
Firefox14.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome18.1 ms1 recalc
Firefox11.0 ms
Plumeria (classStyle prop)Chrome18.7 ms1 recalc
Firefox11.0 ms
EmotionChrome17.8 ms1 recalc
Firefox12.0 ms
BambooChrome20.6 ms1 recalc
Firefox13.0 ms
Plumeria on SolidChrome21.3 ms1 recalc
Firefox14.0 ms
StyleXChrome18.9 ms1 recalc
Firefox15.0 ms
GooberChrome18.3 ms1 recalc
Firefox15.0 ms
tailwind-mergeChrome18.5 ms1 recalc
Firefox18.0 ms
cn (shadcn-ui) on SolidChrome19.4 ms1 recalc
Firefox20.0 ms
cn (shadcn-ui)Chrome20.7 ms1 recalc
Firefox21.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
@yak/react (css prop) foldStatic: false7,602 B(0.4 / 0.9 / 6.1 kB)
@yak/react (css prop)7,624 B(0.5 / 0.9 / 6.1 kB)
vanilla (hand-written ceiling)7,793 B(0.0 / 1.5 / 6.1 kB)
@yak/react (styled API)8,449 B(1.1 / 0.9 / 6.2 kB)
@yak/react (styled API) foldStatic: false8,569 B(1.3 / 0.9 / 6.2 kB)
vanilla Solid (hand-written ceiling)9,706 B(0.0 / 1.5 / 8.0 kB)
Plumeria (classStyle prop)11,287 B(1.1 / 1.4 / 8.5 kB)
Goober11,402 B(3.9 / 1.1 / 6.1 kB)
Plumeria on Solid13,035 B(0.4 / 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 Solid18,593 B(5.9 / 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: false30,534 B(6.5 / 0.9 / 22.5 kB)
@yak/solid (styled API, Solid 2)31,171 B(7.1 / 0.9 / 22.5 kB)
cn (shadcn-ui) on Solid31,512 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.050991491991005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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,459,835 r/s ±9%(0.27 ms / 1k elems)
cn (shadcn-ui) on Solid1,386,109 r/s ±8%(0.29 ms / 1k elems)
@yak/solid (styled API, Solid 2)1,356,543 r/s ±9%(0.29 ms / 1k elems)
Plumeria on Solid1,299,842 r/s ±8%(0.31 ms / 1k elems)
StyleX on Solid760,459 r/s ±6%(0.53 ms / 1k elems)
StyleX612,192 r/s ±8%(0.65 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)580,802 r/s ±5%(0.69 ms / 1k elems)
@yak/react (styled API)580,279 r/s ±2%(0.69 ms / 1k elems)
@yak/react (css prop)546,307 r/s ±5%(0.73 ms / 1k elems)
vanilla (hand-written ceiling)495,025 r/s ±2%(0.81 ms / 1k elems)
@yak/react (css prop) foldStatic: false472,791 r/s ±5%(0.85 ms / 1k elems)
Plumeria (classStyle prop)472,049 r/s ±3%(0.85 ms / 1k elems)
cn (shadcn-ui)450,557 r/s ±8%(0.89 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false443,495 r/s ±3%(0.90 ms / 1k elems)
Bamboo311,594 r/s ±2%(1.28 ms / 1k elems)
tailwind-merge290,904 r/s ±4%(1.38 ms / 1k elems)
@yak/react (styled API) foldStatic: false266,695 r/s ±5%(1.50 ms / 1k elems)
Panda (css fn)262,882 r/s ±4%(1.52 ms / 1k elems)
Goober151,180 r/s ±1%(2.65 ms / 1k elems)
styled-components141,737 r/s ±1%(2.82 ms / 1k elems)
Emotion102,151 r/s ±1%(3.92 ms / 1k elems)
Panda (style props)87,386 r/s ±4%(4.58 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)765 req/s ±1%
vanilla Solid (hand-written ceiling)762 req/s ±0%
Plumeria on Solid533 req/s ±1%
@yak/react (styled API)434 req/s ±1%
@yak/react (css prop)433 req/s ±0%
cn (shadcn-ui) on Solid426 req/s ±1%
StyleX on Solid405 req/s ±1%
@yak/react (css prop) foldStatic: false385 req/s ±1%
vanilla (hand-written ceiling)361 req/s ±1%
StyleX without CSS layers (:not() specificity hack)353 req/s ±2%
@yak/solid (styled API, Solid 2) foldStatic: false351 req/s ±1%
StyleX340 req/s ±2%
Plumeria (classStyle prop)305 req/s ±0%
cn (shadcn-ui)260 req/s ±2%
@yak/react (styled API) foldStatic: false236 req/s ±1%
tailwind-merge198 req/s ±1%
Bamboo182 req/s ±0%
Panda (css fn)151 req/s ±1%
Goober139 req/s ±1%
styled-components138 req/s ±2%
Emotion111 req/s ±0%
Panda (style props)71 req/s ±3%

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.17 ms
Plumeria on Solid1.28 ms
vanilla Solid (hand-written ceiling)1.31 ms
@yak/solid (styled API, Solid 2)1.33 ms
StyleX on Solid1.73 ms
@yak/react (css prop)1.91 ms
StyleX without CSS layers (:not() specificity hack)1.94 ms
StyleX2.18 ms
@yak/react (styled API)2.32 ms
vanilla (hand-written ceiling)2.35 ms
Plumeria (classStyle prop)2.38 ms
@yak/solid (styled API, Solid 2) foldStatic: false2.51 ms
@yak/react (css prop) foldStatic: false2.65 ms
cn (shadcn-ui)2.95 ms
tailwind-merge4.11 ms
@yak/react (styled API) foldStatic: false4.16 ms
Bamboo4.80 ms
Panda (css fn)5.41 ms
Goober7.56 ms
styled-components8.04 ms
Emotion9.85 ms
Panda (style props)15.26 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)82 ms ±4%
@yak/react (css prop)85 ms ±4%
Bamboo86 ms ±4%
StyleX without CSS layers (:not() specificity hack)87 ms ±3%
vanilla (hand-written ceiling)87 ms ±4%
@yak/react (styled API)87 ms ±3%
Plumeria (classStyle prop)87 ms ±4%
StyleX88 ms ±4%
@yak/react (css prop) foldStatic: false97 ms ±3%
cn (shadcn-ui)101 ms ±4%
@yak/react (styled API) foldStatic: false109 ms ±3%
tailwind-merge112 ms ±3%
Goober122 ms ±3%
styled-components126 ms ±3%
@yak/solid (styled API, Solid 2)137 ms ±4%
Plumeria on Solid139 ms ±4%
vanilla Solid (hand-written ceiling)141 ms ±6%
cn (shadcn-ui) on Solid149 ms ±3%
Emotion152 ms ±4%
Panda (style props)160 ms ±4%
StyleX on Solid161 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false238 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)11.42 ms active(14.93 ms span)
vanilla Solid (hand-written ceiling)12.08 ms active(22.37 ms span)
@yak/react (styled API)12.36 ms active(29.78 ms span)
Bamboo12.43 ms active(31.53 ms span)
vanilla (hand-written ceiling)12.61 ms active(32.81 ms span)
@yak/react (css prop)12.63 ms active(33.93 ms span)
StyleX without CSS layers (:not() specificity hack)12.72 ms active(32.15 ms span)
StyleX12.97 ms active(32.24 ms span)
Panda (css fn)13.41 ms active(24.30 ms span)
Plumeria (classStyle prop)13.43 ms active(31.97 ms span)
@yak/react (css prop) foldStatic: false13.53 ms active(32.62 ms span)
Plumeria on Solid13.87 ms active(16.66 ms span)
StyleX on Solid14.43 ms active(22.51 ms span)
@yak/react (styled API) foldStatic: false15.17 ms active(33.56 ms span)
styled-components15.20 ms active(24.50 ms span)
cn (shadcn-ui) on Solid16.11 ms active(25.59 ms span)
Emotion18.02 ms active(25.11 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false18.19 ms active(28.34 ms span)
Goober18.23 ms active(35.62 ms span)
Panda (style props)20.05 ms active(34.23 ms span)
cn (shadcn-ui)22.20 ms active(43.89 ms span)
tailwind-merge25.87 ms active(32.99 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)278 ms ±3%
vanilla (hand-written ceiling)279 ms ±6%
@yak/react (css prop) foldStatic: false280 ms ±7%
Bamboo282 ms ±4%
Goober283 ms ±8%
@yak/react (styled API)286 ms ±5%
@yak/react (css prop)286 ms ±5%
cn (shadcn-ui)292 ms ±4%
@yak/solid (styled API, Solid 2)295 ms ±6%
cn (shadcn-ui) on Solid298 ms ±4%
tailwind-merge299 ms ±3%
Panda (css fn)303 ms ±8%
StyleX304 ms ±2%
Emotion314 ms ±5%
Plumeria on Solid315 ms ±4%
Plumeria (classStyle prop)318 ms ±7%
styled-components321 ms ±6%
@yak/react (styled API) foldStatic: false322 ms ±6%
StyleX on Solid325 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false328 ms ±5%
StyleX without CSS layers (:not() specificity hack)343 ms ±4%
Panda (style props)364 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/react (css prop) foldStatic: false4.39 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
cn (shadcn-ui)4.46 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)4.89 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (styled API)4.90 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
StyleX on Solid4.99 ms active(33.25 ms span · 33.25 ms profiled median (n=5))
cn (shadcn-ui) on Solid5.19 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
tailwind-merge5.26 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)5.44 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Plumeria on Solid5.46 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false5.52 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false5.55 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
styled-components5.64 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
Goober5.67 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
StyleX5.73 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
vanilla (hand-written ceiling)5.98 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
Panda (css fn)6.48 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)6.50 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Panda (style props)6.52 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Bamboo6.58 ms active(33.23 ms span · 33.23 ms profiled median (n=5))
Plumeria (classStyle prop)6.65 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (css prop)7.65 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Emotion9.82 ms active(33.24 ms span · 33.24 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/react (css prop)105 ms ±4%
vanilla Solid (hand-written ceiling)106 ms ±4%
StyleX without CSS layers (:not() specificity hack)108 ms ±6%
@yak/solid (styled API, Solid 2)109 ms ±4%
vanilla (hand-written ceiling)109 ms ±4%
Bamboo109 ms ±4%
@yak/react (styled API)110 ms ±4%
Plumeria on Solid110 ms ±7%
Plumeria (classStyle prop)111 ms ±2%
Panda (css fn)114 ms ±2%
StyleX115 ms ±6%
@yak/react (css prop) foldStatic: false120 ms ±5%
cn (shadcn-ui) on Solid120 ms ±7%
cn (shadcn-ui)124 ms ±4%
StyleX on Solid125 ms ±4%
tailwind-merge136 ms ±5%
@yak/react (styled API) foldStatic: false140 ms ±3%
Goober147 ms ±3%
styled-components166 ms ±1%
@yak/solid (styled API, Solid 2) foldStatic: false168 ms ±6%
Panda (style props)196 ms ±2%
Emotion197 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)14.98 ms active(21.04 ms span)
@yak/solid (styled API, Solid 2)16.50 ms active(33.45 ms span)
StyleX on Solid17.12 ms active(36.68 ms span)
Plumeria on Solid17.29 ms active(35.93 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false20.14 ms active(20.16 ms span)
Panda (css fn)20.15 ms active(38.88 ms span)
@yak/react (styled API)20.34 ms active(31.50 ms span)
vanilla (hand-written ceiling)20.73 ms active(37.29 ms span)
cn (shadcn-ui) on Solid21.15 ms active(21.19 ms span)
StyleX without CSS layers (:not() specificity hack)21.39 ms active(39.61 ms span)
StyleX21.82 ms active(31.98 ms span)
@yak/react (css prop)21.86 ms active(35.99 ms span)
Bamboo22.03 ms active(40.31 ms span)
Plumeria (classStyle prop)22.06 ms active(45.68 ms span)
@yak/react (css prop) foldStatic: false22.88 ms active(37.70 ms span)
@yak/react (styled API) foldStatic: false25.15 ms active(44.86 ms span)
Goober27.64 ms active(49.22 ms span)
Panda (style props)30.32 ms active(44.62 ms span)
cn (shadcn-ui)30.86 ms active(44.88 ms span)
tailwind-merge31.72 ms active(44.81 ms span)
Emotion32.69 ms active(49.30 ms span)
styled-components32.83 ms active(47.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)
EmotionChrome5.9 ms1 recalc
Firefox2.0 ms
@yak/react (css prop)Chrome6.4 ms1 recalc
Firefox2.0 ms
@yak/react (css prop) foldStatic: falseChrome6.8 ms1 recalc
Firefox3.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome6.8 ms1 recalc
Firefox3.0 ms
StyleXChrome7.0 ms1 recalc
Firefox3.0 ms
StyleX on SolidChrome6.2 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome6.1 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome5.7 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome6.5 ms1 recalc
Firefox2.0 ms
BambooChrome6.6 ms1 recalc
Firefox3.0 ms
tailwind-mergeChrome6.0 ms1 recalc
Firefox3.0 ms
@yak/react (styled API) foldStatic: falseChrome7.2 ms1 recalc
Firefox3.0 ms
@yak/solid (styled API, Solid 2)Chrome6.2 ms1 recalc
Firefox3.0 ms
@yak/react (styled API)Chrome6.1 ms1 recalc
Firefox3.0 ms
cn (shadcn-ui)Chrome6.7 ms1 recalc
Firefox3.0 ms
styled-componentsChrome5.9 ms1 recalc
Firefox3.0 ms
Plumeria on SolidChrome6.9 ms1 recalc
Firefox3.0 ms
Panda (css fn)Chrome6.0 ms1 recalc
Firefox3.0 ms
Panda (style props)Chrome6.4 ms1 recalc
Firefox4.0 ms
Plumeria (classStyle prop)Chrome7.4 ms1 recalc
Firefox3.0 ms
GooberChrome6.9 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui) on SolidChrome6.5 ms1 recalc
Firefox4.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
@yak/react (css prop) foldStatic: false4,406 B(0.4 / 0.5 / 3.4 kB)
@yak/react (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)
@yak/react (styled API)4,987 B(1.1 / 0.5 / 3.2 kB)
@yak/react (styled API) foldStatic: false5,115 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 Solid13,327 B(0.4 / 0.8 / 11.7 kB)
@yak/solid (styled API, Solid 2) foldStatic: false16,677 B(6.5 / 0.5 / 9.3 kB)
Emotion17,156 B(12.9 / 0.6 / 3.2 kB)
@yak/solid (styled API, Solid 2)17,487 B(7.1 / 0.5 / 9.5 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,970 B(5.9 / 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,842 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.0122436481005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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) foldStatic: false3,425,385 r/s ±6%(0.29 ms / 1k elems)
Plumeria on Solid3,030,156 r/s ±10%(0.33 ms / 1k elems)
cn (shadcn-ui) on Solid2,196,672 r/s ±6%(0.46 ms / 1k elems)
vanilla Solid (hand-written ceiling)2,011,113 r/s ±6%(0.50 ms / 1k elems)
@yak/react (styled API)1,880,625 r/s ±3%(0.53 ms / 1k elems)
@yak/solid (styled API, Solid 2)1,823,437 r/s ±3%(0.55 ms / 1k elems)
Bamboo1,540,153 r/s ±2%(0.65 ms / 1k elems)
@yak/react (css prop)1,504,782 r/s ±26%(0.66 ms / 1k elems)
Plumeria (classStyle prop)1,500,193 r/s ±6%(0.67 ms / 1k elems)
cn (shadcn-ui)1,467,341 r/s ±3%(0.68 ms / 1k elems)
vanilla (hand-written ceiling)1,392,409 r/s ±3%(0.72 ms / 1k elems)
@yak/react (css prop) foldStatic: false1,237,203 r/s ±3%(0.81 ms / 1k elems)
@yak/react (styled API) foldStatic: false1,046,544 r/s ±2%(0.96 ms / 1k elems)
StyleX978,583 r/s ±2%(1.02 ms / 1k elems)
StyleX on Solid969,837 r/s ±3%(1.03 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)942,299 r/s ±2%(1.06 ms / 1k elems)
tailwind-merge882,729 r/s ±4%(1.13 ms / 1k elems)
Panda (style props)644,449 r/s ±1%(1.55 ms / 1k elems)
Panda (css fn)553,448 r/s ±1%(1.81 ms / 1k elems)
Goober461,064 r/s ±2%(2.17 ms / 1k elems)
styled-components379,236 r/s ±1%(2.64 ms / 1k elems)
Emotion225,459 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,425 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false2,625 req/s ±1%
@yak/react (styled API)1,688 req/s ±1%
vanilla Solid (hand-written ceiling)1,603 req/s ±3%
@yak/react (css prop)1,457 req/s ±1%
Plumeria on Solid1,384 req/s ±0%
vanilla (hand-written ceiling)1,253 req/s ±1%
@yak/react (css prop) foldStatic: false1,227 req/s ±2%
@yak/react (styled API) foldStatic: false1,124 req/s ±1%
cn (shadcn-ui) on Solid1,102 req/s ±2%
Plumeria (classStyle prop)1,020 req/s ±1%
Bamboo997 req/s ±1%
cn (shadcn-ui)964 req/s ±2%
StyleX without CSS layers (:not() specificity hack)759 req/s ±0%
StyleX744 req/s ±1%
StyleX on Solid673 req/s ±2%
tailwind-merge660 req/s ±1%
Panda (style props)535 req/s ±1%
Goober476 req/s ±0%
Panda (css fn)438 req/s ±1%
styled-components404 req/s ±0%
Emotion288 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.53 ms
vanilla Solid (hand-written ceiling)0.57 ms
@yak/solid (styled API, Solid 2) foldStatic: false0.69 ms
@yak/react (css prop)1.02 ms
cn (shadcn-ui) on Solid1.05 ms
Plumeria on Solid1.06 ms
vanilla (hand-written ceiling)1.10 ms
@yak/react (styled API)1.11 ms
Plumeria (classStyle prop)1.19 ms
Bamboo1.33 ms
@yak/react (styled API) foldStatic: false1.34 ms
cn (shadcn-ui)1.39 ms
StyleX1.46 ms
StyleX on Solid1.55 ms
Panda (style props)1.59 ms
@yak/react (css prop) foldStatic: false1.66 ms
StyleX without CSS layers (:not() specificity hack)1.69 ms
tailwind-merge2.03 ms
Goober2.50 ms
Panda (css fn)2.79 ms
styled-components2.94 ms
Emotion4.31 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/react (styled API)53 ms ±4%
@yak/react (css prop)55 ms ±2%
Bamboo56 ms ±3%
Plumeria (classStyle prop)58 ms ±3%
vanilla (hand-written ceiling)64 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false68 ms ±6%
@yak/solid (styled API, Solid 2)69 ms ±4%
@yak/react (styled API) foldStatic: false70 ms ±4%
@yak/react (css prop) foldStatic: false72 ms ±4%
cn (shadcn-ui)75 ms ±5%
StyleX without CSS layers (:not() specificity hack)75 ms ±3%
StyleX75 ms ±3%
Plumeria on Solid75 ms ±3%
vanilla Solid (hand-written ceiling)77 ms ±5%
Panda (css fn)78 ms ±2%
cn (shadcn-ui) on Solid80 ms ±5%
styled-components82 ms ±3%
Goober85 ms ±8%
StyleX on Solid88 ms ±4%
Panda (style props)90 ms ±4%
tailwind-merge92 ms ±2%
Emotion105 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)6.89 ms active(25.22 ms span)
vanilla Solid (hand-written ceiling)7.55 ms active(14.95 ms span)
Plumeria on Solid7.56 ms active(22.67 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false7.87 ms active(26.03 ms span)
StyleX on Solid9.65 ms active(11.61 ms span)
cn (shadcn-ui) on Solid9.98 ms active(35.93 ms span)
@yak/react (css prop) foldStatic: false11.08 ms active(33.76 ms span)
Plumeria (classStyle prop)11.11 ms active(22.17 ms span)
@yak/react (styled API) foldStatic: false11.33 ms active(27.20 ms span)
@yak/react (styled API)11.39 ms active(31.26 ms span)
@yak/react (css prop)11.40 ms active(31.70 ms span)
vanilla (hand-written ceiling)12.00 ms active(30.83 ms span)
StyleX without CSS layers (:not() specificity hack)12.19 ms active(32.40 ms span)
Goober12.50 ms active(22.34 ms span)
StyleX12.65 ms active(35.39 ms span)
styled-components12.86 ms active(20.27 ms span)
Bamboo13.17 ms active(21.95 ms span)
Panda (css fn)13.88 ms active(20.18 ms span)
Emotion15.27 ms active(32.94 ms span)
Panda (style props)16.50 ms active(34.65 ms span)
cn (shadcn-ui)18.35 ms active(50.10 ms span)
tailwind-merge19.63 ms active(36.01 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)60 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false61 ms ±4%
Bamboo61 ms ±2%
cn (shadcn-ui) on Solid61 ms ±1%
vanilla (hand-written ceiling)61 ms ±4%
cn (shadcn-ui)62 ms ±4%
StyleX on Solid62 ms ±3%
@yak/react (styled API) foldStatic: false62 ms ±4%
Plumeria on Solid63 ms ±2%
@yak/react (styled API)63 ms ±2%
vanilla Solid (hand-written ceiling)64 ms ±5%
@yak/react (css prop)65 ms ±5%
@yak/react (css prop) foldStatic: false65 ms ±2%
Plumeria (classStyle prop)65 ms ±1%
tailwind-merge67 ms ±5%
StyleX68 ms ±4%
StyleX without CSS layers (:not() specificity hack)69 ms ±5%
Panda (style props)69 ms ±5%
Panda (css fn)70 ms ±5%
Goober73 ms ±3%
styled-components74 ms ±3%
Emotion82 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false1.72 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
Plumeria on Solid1.78 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
Panda (style props)1.89 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
Bamboo1.91 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
StyleX on Solid1.94 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)1.97 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
vanilla (hand-written ceiling)2.01 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/react (css prop)2.02 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
cn (shadcn-ui) on Solid2.03 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
@yak/react (styled API)2.11 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)2.12 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
tailwind-merge2.22 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Panda (css fn)2.26 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
Goober2.27 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.47 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false2.48 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX2.66 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false2.77 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
styled-components2.78 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
Plumeria (classStyle prop)2.98 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
cn (shadcn-ui)3.19 ms active(33.38 ms span · 33.38 ms profiled median (n=5))
Emotion4.36 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)37 ms ±6%
@yak/solid (styled API, Solid 2) foldStatic: false49 ms ±6%
Plumeria on Solid49 ms ±6%
vanilla Solid (hand-written ceiling)54 ms ±4%
cn (shadcn-ui) on Solid58 ms ±5%
@yak/react (css prop)58 ms ±2%
@yak/react (styled API)59 ms ±3%
Bamboo60 ms ±3%
Plumeria (classStyle prop)61 ms ±2%
StyleX on Solid68 ms ±10%
vanilla (hand-written ceiling)72 ms ±5%
@yak/react (css prop) foldStatic: false72 ms ±5%
@yak/react (styled API) foldStatic: false73 ms ±3%
cn (shadcn-ui)74 ms ±3%
StyleX without CSS layers (:not() specificity hack)77 ms ±3%
StyleX78 ms ±3%
Goober82 ms ±5%
Panda (css fn)83 ms ±4%
styled-components96 ms ±5%
Panda (style props)96 ms ±3%
tailwind-merge99 ms ±3%
Emotion111 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false10.61 ms active(13.73 ms span)
@yak/solid (styled API, Solid 2)10.67 ms active(17.48 ms span)
Plumeria on Solid11.53 ms active(28.36 ms span)
vanilla Solid (hand-written ceiling)11.57 ms active(27.71 ms span)
StyleX on Solid12.71 ms active(15.86 ms span)
cn (shadcn-ui) on Solid13.07 ms active(16.34 ms span)
Bamboo16.55 ms active(28.08 ms span)
@yak/react (styled API) foldStatic: false17.53 ms active(42.99 ms span)
@yak/react (css prop)17.70 ms active(43.23 ms span)
vanilla (hand-written ceiling)17.77 ms active(36.54 ms span)
@yak/react (styled API)17.98 ms active(38.67 ms span)
StyleX18.29 ms active(29.55 ms span)
@yak/react (css prop) foldStatic: false19.48 ms active(43.53 ms span)
Plumeria (classStyle prop)19.51 ms active(34.08 ms span)
Panda (css fn)19.68 ms active(42.28 ms span)
StyleX without CSS layers (:not() specificity hack)20.39 ms active(39.97 ms span)
Panda (style props)21.01 ms active(39.68 ms span)
Goober23.00 ms active(36.57 ms span)
Emotion23.65 ms active(36.55 ms span)
styled-components24.09 ms active(43.59 ms span)
cn (shadcn-ui)24.17 ms active(37.50 ms span)
tailwind-merge29.49 ms active(36.05 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
Firefox2.0 ms
cn (shadcn-ui) on SolidChrome4.8 ms1 recalc
Firefox1.0 ms
EmotionChrome4.7 ms1 recalc
Firefox1.0 ms
GooberChrome5.3 ms1 recalc
Firefox2.0 ms
@yak/react (styled API)Chrome5.0 ms1 recalc
Firefox1.0 ms
@yak/react (css prop)Chrome4.7 ms1 recalc
Firefox1.0 ms
@yak/react (css prop) foldStatic: falseChrome4.8 ms1 recalc
Firefox1.0 ms
@yak/react (styled API) foldStatic: falseChrome4.7 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome4.8 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome4.9 ms1 recalc
Firefox1.0 ms
styled-componentsChrome5.1 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome5.3 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome4.4 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome5.1 ms1 recalc
Firefox1.0 ms
Plumeria on SolidChrome5.1 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.8 ms1 recalc
Firefox2.0 ms
BambooChrome4.7 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome4.7 ms1 recalc
Firefox2.0 ms
StyleXChrome5.0 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome4.8 ms1 recalc
Firefox1.9 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.5 ms1 recalc
Firefox2.0 ms
Plumeria (classStyle prop)Chrome5.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,164 B(0.0 / 0.5 / 2.6 kB)
@yak/react (css prop) foldStatic: false3,190 B(0.4 / 0.2 / 2.5 kB)
@yak/react (css prop)3,209 B(0.5 / 0.2 / 2.5 kB)
@yak/react (styled API)3,907 B(1.1 / 0.2 / 2.5 kB)
@yak/react (styled API) foldStatic: false4,037 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,658 B(0.4 / 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,853 B(6.5 / 0.2 / 4.9 kB)
StyleX on Solid12,254 B(5.9 / 0.3 / 5.8 kB)
@yak/solid (styled API, Solid 2)12,631 B(7.1 / 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 Solid19,026 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.214181005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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. @yak/react 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 @yak/react 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 @yak/react 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 yak-react 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 @yak/react 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 yak-react 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,455,485 r/s ±23%(0.29 ms / 1k elems)
@yak/solid (styled API, Solid 2)3,280,420 r/s ±11%(0.30 ms / 1k elems)
cn (shadcn-ui) on Solid1,110,799 r/s ±12%(0.90 ms / 1k elems)
vanilla (hand-written ceiling)1,031,803 r/s ±4%(0.97 ms / 1k elems)
cn (shadcn-ui)1,009,848 r/s ±4%(0.99 ms / 1k elems)
@yak/react (styled API)890,959 r/s ±6%(1.12 ms / 1k elems)
@yak/react (styled API) foldStatic: false881,427 r/s ±3%(1.13 ms / 1k elems)
StyleX787,862 r/s ±5%(1.27 ms / 1k elems)
Bamboo739,694 r/s ±2%(1.35 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)734,589 r/s ±5%(1.36 ms / 1k elems)
vanilla Solid (hand-written ceiling)732,530 r/s ±30%(1.37 ms / 1k elems)
StyleX on Solid694,003 r/s ±6%(1.44 ms / 1k elems)
tailwind-merge606,047 r/s ±2%(1.65 ms / 1k elems)
styled-components522,787 r/s ±3%(1.91 ms / 1k elems)
Panda (style props)513,599 r/s ±2%(1.95 ms / 1k elems)
Goober355,126 r/s ±1%(2.82 ms / 1k elems)
Panda (css fn)269,314 r/s ±2%(3.71 ms / 1k elems)
Emotion256,110 r/s ±2%(3.90 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,565 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false2,555 req/s ±1%
vanilla (hand-written ceiling)1,056 req/s ±2%
@yak/react (styled API)934 req/s ±2%
@yak/react (styled API) foldStatic: false932 req/s ±1%
cn (shadcn-ui)782 req/s ±0%
vanilla Solid (hand-written ceiling)774 req/s ±1%
cn (shadcn-ui) on Solid707 req/s ±1%
StyleX638 req/s ±1%
StyleX without CSS layers (:not() specificity hack)631 req/s ±1%
styled-components581 req/s ±0%
tailwind-merge520 req/s ±0%
StyleX on Solid520 req/s ±1%
Bamboo503 req/s ±1%
Panda (style props)477 req/s ±1%
Goober397 req/s ±4%
Emotion307 req/s ±0%
Panda (css fn)241 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.71 ms
@yak/solid (styled API, Solid 2) foldStatic: false0.79 ms
cn (shadcn-ui)0.99 ms
cn (shadcn-ui) on Solid1.08 ms
StyleX1.34 ms
vanilla Solid (hand-written ceiling)1.37 ms
Bamboo1.40 ms
@yak/react (styled API)1.51 ms
StyleX on Solid1.60 ms
StyleX without CSS layers (:not() specificity hack)1.65 ms
tailwind-merge1.90 ms
Panda (style props)1.91 ms
vanilla (hand-written ceiling)1.92 ms
@yak/react (styled API) foldStatic: false2.14 ms
styled-components2.37 ms
Goober2.74 ms
Emotion3.76 ms
Panda (css fn)4.29 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)67 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false68 ms ±5%
@yak/react (styled API) foldStatic: false70 ms ±3%
Bamboo72 ms ±1%
@yak/react (styled API)72 ms ±4%
vanilla (hand-written ceiling)72 ms ±5%
StyleX without CSS layers (:not() specificity hack)77 ms ±2%
StyleX79 ms ±5%
cn (shadcn-ui)81 ms ±4%
styled-components82 ms ±4%
cn (shadcn-ui) on Solid87 ms ±3%
tailwind-merge94 ms ±4%
Goober95 ms ±5%
Panda (style props)95 ms ±5%
Panda (css fn)97 ms ±5%
StyleX on Solid98 ms ±3%
Emotion100 ms ±4%
vanilla Solid (hand-written ceiling)106 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false7.41 ms active(27.89 ms span)
@yak/solid (styled API, Solid 2)7.57 ms active(17.17 ms span)
StyleX on Solid10.17 ms active(15.48 ms span)
cn (shadcn-ui) on Solid10.33 ms active(31.29 ms span)
vanilla Solid (hand-written ceiling)10.83 ms active(27.21 ms span)
@yak/react (styled API)11.91 ms active(37.73 ms span)
vanilla (hand-written ceiling)11.98 ms active(29.82 ms span)
StyleX without CSS layers (:not() specificity hack)12.04 ms active(27.56 ms span)
StyleX12.68 ms active(24.10 ms span)
@yak/react (styled API) foldStatic: false12.69 ms active(37.67 ms span)
styled-components13.34 ms active(31.05 ms span)
Goober13.78 ms active(33.67 ms span)
Bamboo14.04 ms active(35.40 ms span)
Panda (css fn)14.31 ms active(31.74 ms span)
cn (shadcn-ui)14.69 ms active(26.75 ms span)
Panda (style props)16.42 ms active(27.50 ms span)
Emotion19.10 ms active(28.62 ms span)
tailwind-merge20.16 ms active(36.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.

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)62 ms ±1%
@yak/solid (styled API, Solid 2) foldStatic: false63 ms ±6%
cn (shadcn-ui)64 ms ±2%
cn (shadcn-ui) on Solid64 ms ±4%
Bamboo67 ms ±5%
tailwind-merge68 ms ±4%
@yak/react (styled API) foldStatic: false68 ms ±4%
vanilla Solid (hand-written ceiling)68 ms ±8%
@yak/react (styled API)68 ms ±5%
vanilla (hand-written ceiling)69 ms ±2%
styled-components70 ms ±3%
StyleX on Solid71 ms ±6%
Panda (style props)72 ms ±4%
StyleX without CSS layers (:not() specificity hack)73 ms ±3%
StyleX74 ms ±5%
Goober78 ms ±3%
Emotion79 ms ±3%
Panda (css fn)79 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false1.60 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)1.81 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
cn (shadcn-ui)1.82 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
StyleX on Solid1.94 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
Bamboo2.05 ms active(33.25 ms span · 33.25 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false2.10 ms active(33.25 ms span · 33.25 ms profiled median (n=5))
styled-components2.13 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.19 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/react (styled API)2.20 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
vanilla (hand-written ceiling)2.32 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
StyleX2.49 ms active(33.25 ms span · 33.25 ms profiled median (n=5))
Panda (css fn)2.63 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
cn (shadcn-ui) on Solid2.66 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.96 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
Panda (style props)3.01 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
Emotion3.09 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
tailwind-merge3.10 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Goober3.38 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) foldStatic: false48 ms ±3%
@yak/solid (styled API, Solid 2)48 ms ±4%
cn (shadcn-ui) on Solid66 ms ±2%
vanilla (hand-written ceiling)74 ms ±3%
@yak/react (styled API) foldStatic: false74 ms ±6%
vanilla Solid (hand-written ceiling)74 ms ±6%
StyleX on Solid78 ms ±9%
@yak/react (styled API)79 ms ±5%
Bamboo79 ms ±3%
StyleX without CSS layers (:not() specificity hack)82 ms ±3%
StyleX83 ms ±5%
cn (shadcn-ui)85 ms ±5%
styled-components95 ms ±4%
Goober96 ms ±6%
Panda (style props)102 ms ±3%
tailwind-merge103 ms ±4%
Panda (css fn)104 ms ±4%
Emotion113 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false10.98 ms active(16.41 ms span)
@yak/solid (styled API, Solid 2)11.11 ms active(29.34 ms span)
vanilla Solid (hand-written ceiling)11.88 ms active(30.33 ms span)
StyleX on Solid12.71 ms active(23.04 ms span)
cn (shadcn-ui) on Solid13.44 ms active(23.89 ms span)
vanilla (hand-written ceiling)17.00 ms active(32.45 ms span)
@yak/react (styled API)17.47 ms active(28.69 ms span)
StyleX18.84 ms active(35.68 ms span)
Bamboo18.91 ms active(35.32 ms span)
@yak/react (styled API) foldStatic: false19.01 ms active(38.09 ms span)
Goober19.17 ms active(33.81 ms span)
StyleX without CSS layers (:not() specificity hack)19.71 ms active(32.31 ms span)
Panda (css fn)21.61 ms active(31.58 ms span)
styled-components22.05 ms active(47.89 ms span)
cn (shadcn-ui)22.20 ms active(37.47 ms span)
Panda (style props)22.44 ms active(37.21 ms span)
tailwind-merge25.43 ms active(38.99 ms span)
Emotion27.60 ms active(45.82 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.5 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui)Chrome5.9 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui) on SolidChrome4.8 ms1 recalc
Firefox2.0 ms
EmotionChrome5.1 ms1 recalc
Firefox2.0 ms
@yak/react (styled API) foldStatic: falseChrome5.5 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome5.3 ms1 recalc
Firefox2.0 ms
Panda (style props)Chrome5.0 ms1 recalc
Firefox1.0 ms
styled-componentsChrome4.9 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.1 ms1 recalc
Firefox1.0 ms
StyleXChrome5.1 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome5.0 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome4.6 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome4.6 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome5.1 ms1 recalc
Firefox1.0 ms
StyleX on SolidChrome5.0 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome5.1 ms1 recalc
Firefox2.0 ms
GooberChrome4.7 ms1 recalc
Firefox3.0 ms
@yak/react (styled API)Chrome4.5 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)
@yak/react (styled API)4,091 B(1.1 / 0.2 / 2.6 kB)
@yak/react (styled API) foldStatic: false4,232 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: false12,198 B(6.5 / 0.2 / 5.2 kB)
StyleX on Solid12,254 B(5.9 / 0.3 / 5.8 kB)
@yak/solid (styled API, Solid 2)12,837 B(7.1 / 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 Solid19,026 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.97.912161005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoober@yak/react (styled API)@yak/react (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; @yak/react 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)150,015 r/s ±16%(0.83 ms / 1k elems)
cn (shadcn-ui) on Solid140,219 r/s ±10%(0.89 ms / 1k elems)
Plumeria on Solid126,410 r/s ±7%(0.99 ms / 1k elems)
vanilla (hand-written ceiling)90,551 r/s ±3%(1.38 ms / 1k elems)
@yak/react (css prop)85,533 r/s ±3%(1.46 ms / 1k elems)
StyleX79,864 r/s ±4%(1.57 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)77,892 r/s ±6%(1.60 ms / 1k elems)
Plumeria (classStyle prop)73,547 r/s ±2%(1.70 ms / 1k elems)
StyleX on Solid72,668 r/s ±5%(1.72 ms / 1k elems)
@yak/react (css prop) foldStatic: false66,176 r/s ±5%(1.89 ms / 1k elems)
@yak/solid (styled API, Solid 2)65,955 r/s ±5%(1.90 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false54,066 r/s ±4%(2.31 ms / 1k elems)
cn (shadcn-ui)52,209 r/s ±2%(2.39 ms / 1k elems)
@yak/react (styled API)48,875 r/s ±2%(2.56 ms / 1k elems)
@yak/react (styled API) foldStatic: false43,943 r/s ±2%(2.84 ms / 1k elems)
Bamboo39,997 r/s ±3%(3.13 ms / 1k elems)
tailwind-merge36,918 r/s ±1%(3.39 ms / 1k elems)
Goober27,760 r/s ±2%(4.50 ms / 1k elems)
Panda (css fn)22,342 r/s ±2%(5.59 ms / 1k elems)
styled-components22,319 r/s ±2%(5.60 ms / 1k elems)
Emotion14,893 r/s ±1%(8.39 ms / 1k elems)
Panda (style props)13,029 r/s ±2%(9.59 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)733 req/s ±1%
vanilla (hand-written ceiling)481 req/s ±2%
Plumeria on Solid475 req/s ±1%
@yak/react (css prop)424 req/s ±1%
cn (shadcn-ui) on Solid380 req/s ±1%
@yak/solid (styled API, Solid 2)369 req/s ±1%
@yak/react (css prop) foldStatic: false351 req/s ±0%
StyleX without CSS layers (:not() specificity hack)351 req/s ±1%
StyleX350 req/s ±1%
StyleX on Solid327 req/s ±1%
Plumeria (classStyle prop)318 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false297 req/s ±1%
@yak/react (styled API)295 req/s ±1%
@yak/react (styled API) foldStatic: false268 req/s ±0%
cn (shadcn-ui)211 req/s ±0%
Bamboo179 req/s ±1%
tailwind-merge169 req/s ±0%
Goober169 req/s ±1%
styled-components135 req/s ±1%
Panda (css fn)113 req/s ±1%
Emotion113 req/s ±1%
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
vanilla Solid (hand-written ceiling)1.31 ms
Plumeria on Solid1.92 ms
StyleX2.13 ms
cn (shadcn-ui) on Solid2.16 ms
Plumeria (classStyle prop)2.32 ms
StyleX without CSS layers (:not() specificity hack)2.33 ms
vanilla (hand-written ceiling)2.33 ms
@yak/react (css prop)2.43 ms
StyleX on Solid2.49 ms
@yak/react (css prop) foldStatic: false2.60 ms
@yak/solid (styled API, Solid 2)3.14 ms
cn (shadcn-ui)3.35 ms
@yak/react (styled API)3.59 ms
@yak/react (styled API) foldStatic: false3.62 ms
@yak/solid (styled API, Solid 2) foldStatic: false3.76 ms
tailwind-merge4.42 ms
Bamboo5.82 ms
Goober6.48 ms
Panda (css fn)8.91 ms
styled-components8.96 ms
Emotion10.15 ms
Panda (style props)14.12 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.

vanilla (hand-written ceiling)70 ms ±2%
@yak/react (css prop)71 ms ±4%
Plumeria (classStyle prop)73 ms ±3%
StyleX74 ms ±4%
StyleX without CSS layers (:not() specificity hack)76 ms ±4%
Bamboo79 ms ±5%
@yak/react (css prop) foldStatic: false83 ms ±2%
cn (shadcn-ui)87 ms ±6%
@yak/react (styled API) foldStatic: false93 ms ±3%
@yak/react (styled API)93 ms ±3%
Panda (css fn)99 ms ±3%
tailwind-merge100 ms ±4%
vanilla Solid (hand-written ceiling)107 ms ±4%
Goober109 ms ±5%
Plumeria on Solid112 ms ±5%
styled-components114 ms ±3%
cn (shadcn-ui) on Solid117 ms ±2%
StyleX on Solid141 ms ±4%
Emotion147 ms ±3%
Panda (style props)159 ms ±6%
@yak/solid (styled API, Solid 2)193 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false243 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX without CSS layers (:not() specificity hack)16.81 ms active(33.59 ms span)
Plumeria (classStyle prop)17.20 ms active(33.14 ms span)
vanilla (hand-written ceiling)17.74 ms active(37.95 ms span)
StyleX17.76 ms active(27.16 ms span)
Bamboo18.55 ms active(28.86 ms span)
@yak/react (css prop)20.24 ms active(40.67 ms span)
@yak/react (css prop) foldStatic: false20.93 ms active(29.97 ms span)
vanilla Solid (hand-written ceiling)22.01 ms active(22.08 ms span)
@yak/react (styled API) foldStatic: false22.63 ms active(29.64 ms span)
Plumeria on Solid23.50 ms active(36.38 ms span)
Panda (css fn)24.27 ms active(52.39 ms span)
cn (shadcn-ui) on Solid24.34 ms active(34.96 ms span)
cn (shadcn-ui)25.95 ms active(42.57 ms span)
@yak/react (styled API)26.79 ms active(53.34 ms span)
StyleX on Solid28.17 ms active(34.61 ms span)
tailwind-merge29.66 ms active(56.30 ms span)
Goober30.42 ms active(60.69 ms span)
Panda (style props)31.03 ms active(34.56 ms span)
styled-components34.21 ms active(59.41 ms span)
Emotion38.50 ms active(57.83 ms span)
@yak/solid (styled API, Solid 2)39.92 ms active(51.84 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false55.48 ms active(70.51 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 (classStyle prop)176 ms ±4%
StyleX178 ms ±4%
Plumeria on Solid211 ms ±2%
StyleX without CSS layers (:not() specificity hack)212 ms ±7%
StyleX on Solid232 ms ±11%
vanilla Solid (hand-written ceiling)252 ms ±9%
@yak/react (css prop) foldStatic: false274 ms ±8%
@yak/react (css prop)278 ms ±12%
vanilla (hand-written ceiling)279 ms ±6%
cn (shadcn-ui) on Solid287 ms ±1%
@yak/solid (styled API, Solid 2)289 ms ±5%
Goober290 ms ±7%
Panda (css fn)306 ms ±3%
@yak/react (styled API) foldStatic: false306 ms ±5%
@yak/react (styled API)308 ms ±9%
Bamboo309 ms ±5%
tailwind-merge329 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false331 ms ±9%
Emotion331 ms ±4%
styled-components346 ms ±12%
cn (shadcn-ui)349 ms ±7%
Panda (style props)370 ms ±13%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX19.60 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)21.72 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Plumeria (classStyle prop)23.55 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
StyleX on Solid23.65 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false25.26 ms active(29.34 ms span · 29.34 ms profiled median (n=5))
vanilla (hand-written ceiling)25.90 ms active(36.08 ms span · 36.08 ms profiled median (n=5))
Goober26.42 ms active(34.88 ms span · 34.88 ms profiled median (n=5))
Bamboo27.05 ms active(35.92 ms span · 35.92 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false27.72 ms active(35.61 ms span · 35.61 ms profiled median (n=5))
@yak/react (css prop)28.25 ms active(32.50 ms span · 32.50 ms profiled median (n=5))
tailwind-merge28.95 ms active(35.62 ms span · 35.62 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)29.74 ms active(33.15 ms span · 33.15 ms profiled median (n=5))
styled-components29.98 ms active(36.00 ms span · 36.00 ms profiled median (n=5))
Plumeria on Solid30.07 ms active(33.38 ms span · 33.38 ms profiled median (n=5))
Panda (css fn)30.15 ms active(36.02 ms span · 36.02 ms profiled median (n=5))
cn (shadcn-ui)30.49 ms active(35.24 ms span · 35.24 ms profiled median (n=5))
@yak/react (styled API)31.97 ms active(34.63 ms span · 34.63 ms profiled median (n=5))
Emotion32.90 ms active(36.94 ms span · 36.94 ms profiled median (n=5))
Panda (style props)32.93 ms active(35.73 ms span · 35.73 ms profiled median (n=5))
cn (shadcn-ui) on Solid36.67 ms active(49.95 ms span · 49.95 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)44.53 ms active(49.70 ms span · 49.70 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false46.48 ms active(49.88 ms span · 49.88 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)83 ms ±8%
Plumeria on Solid90 ms ±5%
@yak/react (css prop)91 ms ±5%
vanilla (hand-written ceiling)92 ms ±3%
StyleX without CSS layers (:not() specificity hack)94 ms ±6%
StyleX94 ms ±3%
cn (shadcn-ui) on Solid97 ms ±5%
Bamboo98 ms ±3%
Plumeria (classStyle prop)99 ms ±5%
@yak/react (css prop) foldStatic: false100 ms ±2%
cn (shadcn-ui)101 ms ±4%
@yak/react (styled API) foldStatic: false114 ms ±1%
@yak/react (styled API)117 ms ±5%
StyleX on Solid117 ms ±6%
tailwind-merge122 ms ±6%
Panda (css fn)123 ms ±4%
Goober133 ms ±3%
styled-components153 ms ±4%
@yak/solid (styled API, Solid 2)172 ms ±6%
Emotion184 ms ±5%
Panda (style props)193 ms ±7%
@yak/solid (styled API, Solid 2) foldStatic: false204 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)35.34 ms active(35.37 ms span)
Plumeria on Solid39.39 ms active(39.42 ms span)
Plumeria (classStyle prop)41.49 ms active(48.14 ms span)
vanilla (hand-written ceiling)42.88 ms active(50.69 ms span)
StyleX43.26 ms active(52.19 ms span)
StyleX without CSS layers (:not() specificity hack)43.64 ms active(58.46 ms span)
@yak/react (css prop)43.92 ms active(50.56 ms span)
StyleX on Solid44.87 ms active(44.91 ms span)
Bamboo48.16 ms active(63.69 ms span)
@yak/react (styled API)51.09 ms active(53.11 ms span)
@yak/react (css prop) foldStatic: false51.64 ms active(56.71 ms span)
Panda (css fn)53.46 ms active(58.98 ms span)
@yak/react (styled API) foldStatic: false54.74 ms active(57.68 ms span)
cn (shadcn-ui)54.77 ms active(60.78 ms span)
cn (shadcn-ui) on Solid55.30 ms active(55.36 ms span)
styled-components56.27 ms active(67.15 ms span)
tailwind-merge57.29 ms active(63.31 ms span)
Goober59.39 ms active(70.80 ms span)
@yak/solid (styled API, Solid 2)59.56 ms active(59.65 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false64.28 ms active(64.39 ms span)
Emotion65.02 ms active(75.97 ms span)
Panda (style props)66.11 ms active(72.89 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/react (css prop)Chrome21.1 ms1 recalc
Firefox13.0 ms
vanilla Solid (hand-written ceiling)Chrome18.2 ms1 recalc
Firefox11.8 ms
StyleX on SolidChrome18.9 ms1 recalc
Firefox11.0 ms
vanilla (hand-written ceiling)Chrome19.4 ms1 recalc
Firefox11.0 ms
@yak/react (styled API) foldStatic: falseChrome20.5 ms1 recalc
Firefox13.0 ms
@yak/solid (styled API, Solid 2)Chrome20.4 ms1 recalc
Firefox12.0 ms
EmotionChrome18.8 ms1 recalc
Firefox13.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome20.4 ms1 recalc
Firefox12.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome20.4 ms1 recalc
Firefox13.0 ms
styled-componentsChrome17.4 ms1 recalc
Firefox13.0 ms
BambooChrome22.4 ms1 recalc
Firefox14.0 ms
Plumeria on SolidChrome21.2 ms1 recalc
Firefox12.0 ms
Plumeria (classStyle prop)Chrome19.1 ms1 recalc
Firefox14.0 ms
StyleXChrome17.1 ms1 recalc
Firefox14.0 ms
cn (shadcn-ui) on SolidChrome24.8 ms1 recalc
Firefox17.0 ms
@yak/react (css prop) foldStatic: falseChrome21.0 ms1 recalc
Firefox14.0 ms
Panda (css fn)Chrome18.3 ms1 recalc
Firefox15.0 ms
Panda (style props)Chrome20.0 ms1 recalc
Firefox17.0 ms
@yak/react (styled API)Chrome20.0 ms1 recalc
Firefox14.0 ms
cn (shadcn-ui)Chrome25.0 ms1 recalc
Firefox15.0 ms
tailwind-mergeChrome25.8 ms1 recalc
Firefox14.1 ms
GooberChrome24.3 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,692 B(0.0 / 1.0 / 1.6 kB)
@yak/react (css prop) foldStatic: false2,811 B(0.4 / 0.6 / 1.7 kB)
@yak/react (css prop)2,830 B(0.5 / 0.6 / 1.7 kB)
@yak/react (styled API)3,524 B(1.1 / 0.6 / 1.7 kB)
@yak/react (styled API) foldStatic: false3,657 B(1.3 / 0.6 / 1.7 kB)
vanilla Solid (hand-written ceiling)5,089 B(0.0 / 1.0 / 3.9 kB)
Goober6,446 B(3.9 / 0.7 / 1.7 kB)
Plumeria (classStyle prop)6,835 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,764 B(0.4 / 0.9 / 8.2 kB)
@yak/solid (styled API, Solid 2)11,846 B(7.1 / 0.6 / 3.8 kB)
Bamboo12,172 B(2.0 / 1.3 / 8.6 kB)
@yak/solid (styled API, Solid 2) foldStatic: false13,318 B(6.5 / 0.6 / 5.9 kB)
StyleX on Solid15,257 B(5.9 / 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,810 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.0911822733641005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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 @yak/react'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)136,868 r/s ±20%(0.91 ms / 1k elems)
cn (shadcn-ui) on Solid133,492 r/s ±18%(0.94 ms / 1k elems)
Plumeria on Solid120,248 r/s ±6%(1.04 ms / 1k elems)
vanilla (hand-written ceiling)89,068 r/s ±4%(1.40 ms / 1k elems)
@yak/react (css prop)79,457 r/s ±2%(1.57 ms / 1k elems)
StyleX79,352 r/s ±6%(1.58 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)77,505 r/s ±6%(1.61 ms / 1k elems)
StyleX on Solid71,612 r/s ±5%(1.75 ms / 1k elems)
Plumeria (classStyle prop)67,682 r/s ±9%(1.85 ms / 1k elems)
@yak/solid (styled API, Solid 2)62,549 r/s ±8%(2.00 ms / 1k elems)
@yak/react (css prop) foldStatic: false62,266 r/s ±5%(2.01 ms / 1k elems)
cn (shadcn-ui)52,163 r/s ±2%(2.40 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false51,388 r/s ±8%(2.43 ms / 1k elems)
@yak/react (styled API)48,231 r/s ±3%(2.59 ms / 1k elems)
@yak/react (styled API) foldStatic: false43,512 r/s ±2%(2.87 ms / 1k elems)
Bamboo41,438 r/s ±2%(3.02 ms / 1k elems)
tailwind-merge36,854 r/s ±2%(3.39 ms / 1k elems)
Goober27,333 r/s ±1%(4.57 ms / 1k elems)
Panda (css fn)22,594 r/s ±2%(5.53 ms / 1k elems)
styled-components21,640 r/s ±3%(5.78 ms / 1k elems)
Emotion14,858 r/s ±2%(8.41 ms / 1k elems)
Panda (style props)12,822 r/s ±3%(9.75 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)730 req/s ±0%
vanilla (hand-written ceiling)490 req/s ±1%
Plumeria on Solid472 req/s ±1%
@yak/react (css prop)402 req/s ±2%
cn (shadcn-ui) on Solid382 req/s ±1%
@yak/solid (styled API, Solid 2)355 req/s ±1%
StyleX344 req/s ±0%
StyleX without CSS layers (:not() specificity hack)344 req/s ±2%
@yak/react (css prop) foldStatic: false340 req/s ±1%
StyleX on Solid333 req/s ±1%
Plumeria (classStyle prop)320 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false302 req/s ±1%
@yak/react (styled API)293 req/s ±2%
@yak/react (styled API) foldStatic: false264 req/s ±2%
cn (shadcn-ui)210 req/s ±1%
Bamboo179 req/s ±2%
tailwind-merge169 req/s ±1%
Goober168 req/s ±1%
styled-components137 req/s ±2%
Panda (css fn)112 req/s ±2%
Emotion110 req/s ±1%
Panda (style props)74 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.73 ms
vanilla Solid (hand-written ceiling)1.77 ms
vanilla (hand-written ceiling)1.98 ms
StyleX2.12 ms
StyleX without CSS layers (:not() specificity hack)2.32 ms
cn (shadcn-ui) on Solid2.33 ms
Plumeria (classStyle prop)2.35 ms
@yak/react (css prop)2.48 ms
StyleX on Solid2.59 ms
@yak/react (css prop) foldStatic: false2.88 ms
@yak/solid (styled API, Solid 2)3.00 ms
@yak/react (styled API)3.52 ms
@yak/react (styled API) foldStatic: false3.54 ms
cn (shadcn-ui)3.72 ms
Bamboo4.13 ms
@yak/solid (styled API, Solid 2) foldStatic: false4.19 ms
tailwind-merge5.01 ms
Goober6.25 ms
styled-components8.24 ms
Panda (css fn)8.47 ms
Emotion10.25 ms
Panda (style props)13.70 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.

vanilla (hand-written ceiling)69 ms ±2%
@yak/react (css prop)72 ms ±4%
Plumeria (classStyle prop)72 ms ±3%
StyleX73 ms ±5%
StyleX without CSS layers (:not() specificity hack)74 ms ±5%
Bamboo80 ms ±4%
@yak/react (css prop) foldStatic: false84 ms ±4%
cn (shadcn-ui)87 ms ±6%
@yak/react (styled API)91 ms ±7%
@yak/react (styled API) foldStatic: false94 ms ±4%
tailwind-merge97 ms ±3%
Panda (css fn)98 ms ±3%
Goober106 ms ±5%
vanilla Solid (hand-written ceiling)108 ms ±5%
Plumeria on Solid108 ms ±4%
cn (shadcn-ui) on Solid114 ms ±2%
styled-components119 ms ±3%
StyleX on Solid141 ms ±6%
Emotion159 ms ±4%
Panda (style props)167 ms ±4%
@yak/solid (styled API, Solid 2)188 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false253 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria (classStyle prop)16.84 ms active(40.40 ms span)
StyleX16.90 ms active(39.20 ms span)
Bamboo17.52 ms active(33.39 ms span)
@yak/react (css prop)17.58 ms active(28.41 ms span)
vanilla (hand-written ceiling)17.77 ms active(34.57 ms span)
StyleX without CSS layers (:not() specificity hack)18.47 ms active(39.84 ms span)
cn (shadcn-ui)22.20 ms active(28.70 ms span)
@yak/react (css prop) foldStatic: false22.43 ms active(38.36 ms span)
Plumeria on Solid22.59 ms active(36.77 ms span)
vanilla Solid (hand-written ceiling)22.82 ms active(25.31 ms span)
Panda (css fn)24.42 ms active(31.83 ms span)
@yak/react (styled API)25.75 ms active(35.78 ms span)
tailwind-merge26.65 ms active(35.26 ms span)
styled-components27.54 ms active(28.68 ms span)
Goober28.75 ms active(54.98 ms span)
@yak/react (styled API) foldStatic: false28.88 ms active(59.78 ms span)
StyleX on Solid29.62 ms active(34.34 ms span)
cn (shadcn-ui) on Solid29.83 ms active(43.86 ms span)
Panda (style props)31.96 ms active(55.87 ms span)
Emotion41.00 ms active(58.13 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false46.98 ms active(55.55 ms span)
@yak/solid (styled API, Solid 2)47.11 ms active(62.72 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.

StyleX164 ms ±7%
Plumeria (classStyle prop)170 ms ±7%
StyleX on Solid214 ms ±8%
Plumeria on Solid216 ms ±7%
StyleX without CSS layers (:not() specificity hack)233 ms ±3%
vanilla Solid (hand-written ceiling)261 ms ±6%
vanilla (hand-written ceiling)271 ms ±7%
@yak/solid (styled API, Solid 2)293 ms ±6%
cn (shadcn-ui) on Solid297 ms ±8%
@yak/react (css prop) foldStatic: false301 ms ±6%
@yak/react (styled API) foldStatic: false301 ms ±3%
@yak/react (css prop)302 ms ±8%
@yak/solid (styled API, Solid 2) foldStatic: false303 ms ±6%
@yak/react (styled API)305 ms ±7%
Bamboo310 ms ±2%
Panda (css fn)312 ms ±5%
Goober313 ms ±7%
cn (shadcn-ui)328 ms ±7%
tailwind-merge339 ms ±5%
Panda (style props)342 ms ±5%
styled-components347 ms ±5%
Emotion353 ms ±8%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX19.75 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)21.82 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Plumeria (classStyle prop)23.39 ms active(33.13 ms span · 33.13 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)23.75 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/react (css prop)24.07 ms active(31.02 ms span · 31.02 ms profiled median (n=5))
@yak/react (styled API)25.79 ms active(36.01 ms span · 36.01 ms profiled median (n=5))
StyleX on Solid26.20 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
tailwind-merge27.01 ms active(35.83 ms span · 35.83 ms profiled median (n=5))
Panda (css fn)27.84 ms active(35.76 ms span · 35.76 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false27.99 ms active(35.37 ms span · 35.37 ms profiled median (n=5))
Bamboo28.38 ms active(35.61 ms span · 35.61 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false28.47 ms active(35.49 ms span · 35.49 ms profiled median (n=5))
styled-components28.55 ms active(35.98 ms span · 35.98 ms profiled median (n=5))
vanilla (hand-written ceiling)28.67 ms active(33.85 ms span · 33.85 ms profiled median (n=5))
Goober29.43 ms active(34.47 ms span · 34.47 ms profiled median (n=5))
Emotion30.02 ms active(34.98 ms span · 34.98 ms profiled median (n=5))
Plumeria on Solid30.43 ms active(33.42 ms span · 33.42 ms profiled median (n=5))
Panda (style props)32.78 ms active(35.78 ms span · 35.78 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)33.77 ms active(33.81 ms span · 33.81 ms profiled median (n=5))
cn (shadcn-ui)34.31 ms active(35.34 ms span · 35.34 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false35.16 ms active(49.89 ms span · 49.89 ms profiled median (n=5))
cn (shadcn-ui) on Solid46.48 ms active(49.79 ms span · 49.79 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)83 ms ±4%
Plumeria on Solid89 ms ±8%
@yak/react (css prop)93 ms ±7%
vanilla (hand-written ceiling)93 ms ±2%
StyleX94 ms ±2%
Bamboo96 ms ±5%
StyleX without CSS layers (:not() specificity hack)97 ms ±3%
cn (shadcn-ui) on Solid98 ms ±6%
Plumeria (classStyle prop)98 ms ±5%
cn (shadcn-ui)102 ms ±4%
@yak/react (css prop) foldStatic: false105 ms ±4%
StyleX on Solid115 ms ±8%
@yak/react (styled API)115 ms ±5%
@yak/react (styled API) foldStatic: false119 ms ±4%
Panda (css fn)121 ms ±4%
tailwind-merge121 ms ±6%
Goober136 ms ±4%
styled-components150 ms ±6%
@yak/solid (styled API, Solid 2)178 ms ±5%
Emotion182 ms ±5%
Panda (style props)193 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false203 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX38.25 ms active(38.38 ms span)
vanilla Solid (hand-written ceiling)38.86 ms active(38.89 ms span)
Plumeria on Solid39.20 ms active(39.24 ms span)
vanilla (hand-written ceiling)40.37 ms active(52.92 ms span)
Plumeria (classStyle prop)40.84 ms active(55.29 ms span)
StyleX on Solid41.45 ms active(41.48 ms span)
@yak/react (css prop)43.53 ms active(52.49 ms span)
Bamboo45.85 ms active(54.01 ms span)
StyleX without CSS layers (:not() specificity hack)51.43 ms active(59.73 ms span)
Goober51.63 ms active(57.09 ms span)
@yak/react (styled API)51.92 ms active(54.20 ms span)
Panda (css fn)52.28 ms active(63.03 ms span)
cn (shadcn-ui) on Solid53.72 ms active(53.79 ms span)
styled-components53.73 ms active(67.67 ms span)
@yak/react (css prop) foldStatic: false54.29 ms active(64.41 ms span)
@yak/react (styled API) foldStatic: false56.21 ms active(69.97 ms span)
cn (shadcn-ui)57.45 ms active(69.89 ms span)
@yak/solid (styled API, Solid 2)58.43 ms active(58.46 ms span)
tailwind-merge59.62 ms active(66.08 ms span)
Panda (style props)61.42 ms active(68.19 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false62.10 ms active(62.16 ms span)
Emotion74.25 ms active(81.12 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/react (styled API) foldStatic: falseChrome23.7 ms1 recalc
Firefox10.0 ms
EmotionChrome19.3 ms1 recalc
Firefox14.0 ms
BambooChrome20.4 ms1 recalc
Firefox14.0 ms
styled-componentsChrome17.3 ms1 recalc
Firefox9.0 ms
Panda (style props)Chrome20.1 ms1 recalc
Firefox11.0 ms
Plumeria on SolidChrome20.9 ms1 recalc
Firefox11.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome21.9 ms1 recalc
Firefox10.0 ms
StyleXChrome16.1 ms1 recalc
Firefox8.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome19.1 ms1 recalc
Firefox15.0 ms
vanilla (hand-written ceiling)Chrome18.6 ms1 recalc
Firefox10.0 ms
StyleX on SolidChrome15.3 ms1 recalc
Firefox11.0 ms
Panda (css fn)Chrome21.1 ms1 recalc
Firefox14.0 ms
@yak/solid (styled API, Solid 2)Chrome18.6 ms1 recalc
Firefox13.0 ms
vanilla Solid (hand-written ceiling)Chrome18.3 ms1 recalc
Firefox11.0 ms
Plumeria (classStyle prop)Chrome19.3 ms1 recalc
Firefox13.0 ms
@yak/react (css prop)Chrome20.5 ms1 recalc
Firefox12.0 ms
@yak/react (css prop) foldStatic: falseChrome22.8 ms1 recalc
Firefox11.0 ms
@yak/react (styled API)Chrome19.7 ms1 recalc
Firefox14.0 ms
cn (shadcn-ui) on SolidChrome24.6 ms1 recalc
Firefox15.0 ms
cn (shadcn-ui)Chrome27.0 ms1 recalc
Firefox16.0 ms
tailwind-mergeChrome23.1 ms1 recalc
Firefox19.0 ms
GooberChrome18.5 ms1 recalc
Firefox16.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,692 B(0.0 / 1.0 / 1.6 kB)
@yak/react (css prop) foldStatic: false2,811 B(0.4 / 0.6 / 1.7 kB)
@yak/react (css prop)2,831 B(0.5 / 0.6 / 1.7 kB)
@yak/react (styled API)3,529 B(1.1 / 0.6 / 1.7 kB)
@yak/react (styled API) foldStatic: false3,657 B(1.3 / 0.6 / 1.7 kB)
vanilla Solid (hand-written ceiling)5,089 B(0.0 / 1.0 / 3.9 kB)
Goober6,446 B(3.9 / 0.7 / 1.7 kB)
Plumeria (classStyle prop)6,835 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,764 B(0.4 / 0.9 / 8.2 kB)
@yak/solid (styled API, Solid 2)11,849 B(7.1 / 0.6 / 3.8 kB)
Bamboo12,172 B(2.0 / 1.3 / 8.6 kB)
@yak/solid (styled API, Solid 2) foldStatic: false13,310 B(6.5 / 0.6 / 5.9 kB)
StyleX on Solid15,257 B(5.9 / 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,810 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.0921842773691005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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 @yak/react 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,223,873 r/s ±5%(0.45 ms / 1k elems)
Plumeria on Solid1,900,089 r/s ±5%(0.53 ms / 1k elems)
vanilla (hand-written ceiling)1,295,486 r/s ±1%(0.77 ms / 1k elems)
Plumeria (classStyle prop)1,194,576 r/s ±5%(0.84 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false1,092,328 r/s ±3%(0.92 ms / 1k elems)
@yak/solid (styled API, Solid 2)1,055,684 r/s ±8%(0.95 ms / 1k elems)
@yak/react (css prop)937,882 r/s ±5%(1.07 ms / 1k elems)
@yak/react (css prop) foldStatic: false936,160 r/s ±4%(1.07 ms / 1k elems)
StyleX776,147 r/s ±2%(1.29 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)762,002 r/s ±2%(1.31 ms / 1k elems)
StyleX on Solid651,314 r/s ±2%(1.54 ms / 1k elems)
@yak/react (styled API) foldStatic: false545,905 r/s ±1%(1.83 ms / 1k elems)
@yak/react (styled API)530,224 r/s ±2%(1.89 ms / 1k elems)
Emotion231,330 r/s ±2%(4.32 ms / 1k elems)
styled-components163,528 r/s ±3%(6.12 ms / 1k elems)
Goober142,544 r/s ±3%(7.02 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,604 req/s ±1%
vanilla Solid (hand-written ceiling)1,569 req/s ±2%
Plumeria (classStyle prop)1,118 req/s ±1%
vanilla (hand-written ceiling)1,115 req/s ±2%
@yak/solid (styled API, Solid 2)1,023 req/s ±4%
@yak/solid (styled API, Solid 2) foldStatic: false993 req/s ±3%
@yak/react (css prop) foldStatic: false953 req/s ±0%
@yak/react (css prop)936 req/s ±0%
StyleX without CSS layers (:not() specificity hack)749 req/s ±1%
StyleX742 req/s ±0%
@yak/react (styled API)637 req/s ±3%
@yak/react (styled API) foldStatic: false625 req/s ±2%
StyleX on Solid567 req/s ±2%
Emotion270 req/s ±3%
styled-components176 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
Plumeria on Solid0.81 ms
vanilla Solid (hand-written ceiling)1.09 ms
@yak/react (css prop) foldStatic: false1.10 ms
@yak/react (css prop)1.13 ms
vanilla (hand-written ceiling)1.21 ms
@yak/solid (styled API, Solid 2)1.23 ms
StyleX1.43 ms
StyleX without CSS layers (:not() specificity hack)1.45 ms
Plumeria (classStyle prop)1.46 ms
@yak/react (styled API) foldStatic: false1.56 ms
@yak/solid (styled API, Solid 2) foldStatic: false1.72 ms
@yak/react (styled API)2.07 ms
StyleX on Solid2.23 ms
Emotion4.11 ms
styled-components7.95 ms
Goober8.19 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.

vanilla (hand-written ceiling)67 ms ±4%
Plumeria (classStyle prop)68 ms ±4%
@yak/react (css prop) foldStatic: false75 ms ±3%
StyleX without CSS layers (:not() specificity hack)76 ms ±4%
StyleX76 ms ±4%
@yak/react (css prop)77 ms ±4%
@yak/react (styled API)77 ms ±4%
@yak/react (styled API) foldStatic: false78 ms ±3%
vanilla Solid (hand-written ceiling)78 ms ±5%
Plumeria on Solid79 ms ±3%
styled-components95 ms ±4%
Emotion95 ms ±4%
StyleX on Solid101 ms ±3%
@yak/solid (styled API, Solid 2)119 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false122 ms ±4%
Goober126 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)8.06 ms active(18.59 ms span)
Plumeria on Solid8.09 ms active(25.74 ms span)
StyleX on Solid9.94 ms active(25.95 ms span)
@yak/solid (styled API, Solid 2)11.27 ms active(26.62 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false11.62 ms active(12.04 ms span)
@yak/react (css prop)11.78 ms active(35.96 ms span)
@yak/react (styled API) foldStatic: false12.04 ms active(27.13 ms span)
StyleX without CSS layers (:not() specificity hack)12.37 ms active(34.69 ms span)
@yak/react (css prop) foldStatic: false12.38 ms active(43.69 ms span)
StyleX12.40 ms active(25.77 ms span)
@yak/react (styled API)12.43 ms active(32.08 ms span)
Emotion13.76 ms active(22.82 ms span)
Plumeria (classStyle prop)13.86 ms active(29.77 ms span)
styled-components14.46 ms active(26.17 ms span)
vanilla (hand-written ceiling)14.67 ms active(34.20 ms span)
Goober14.95 ms active(23.51 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)62 ms ±3%
vanilla Solid (hand-written ceiling)62 ms ±6%
Plumeria on Solid73 ms ±9%
Plumeria (classStyle prop)77 ms ±7%
@yak/react (css prop)78 ms ±9%
@yak/solid (styled API, Solid 2) foldStatic: false79 ms ±10%
@yak/react (css prop) foldStatic: false80 ms ±10%
Emotion81 ms ±1%
@yak/react (styled API) foldStatic: false82 ms ±12%
@yak/solid (styled API, Solid 2)83 ms ±14%
@yak/react (styled API)84 ms ±10%
styled-components85 ms ±5%
Goober88 ms ±2%
StyleX without CSS layers (:not() specificity hack)88 ms ±10%
StyleX on Solid88 ms ±10%
StyleX91 ms ±12%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX without CSS layers (:not() specificity hack)1.55 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false1.99 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/react (css prop)2.00 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Plumeria on Solid2.03 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
vanilla (hand-written ceiling)2.06 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.21 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
Goober2.24 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
Plumeria (classStyle prop)2.26 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Emotion2.30 ms active(33.25 ms span · 33.25 ms profiled median (n=5))
StyleX on Solid2.37 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
StyleX2.49 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false2.65 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/react (styled API)2.85 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false2.94 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
styled-components3.38 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)3.52 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.

vanilla Solid (hand-written ceiling)57 ms ±8%
Plumeria on Solid61 ms ±5%
vanilla (hand-written ceiling)74 ms ±5%
Plumeria (classStyle prop)77 ms ±3%
@yak/react (css prop) foldStatic: false84 ms ±3%
StyleX on Solid87 ms ±6%
StyleX87 ms ±3%
@yak/react (css prop)87 ms ±5%
@yak/react (styled API) foldStatic: false89 ms ±4%
StyleX without CSS layers (:not() specificity hack)91 ms ±5%
@yak/react (styled API)95 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false98 ms ±4%
@yak/solid (styled API, Solid 2)108 ms ±8%
Emotion157 ms ±7%
styled-components167 ms ±3%
Goober2,352 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)9.09 ms active(22.67 ms span)
Plumeria on Solid11.20 ms active(20.38 ms span)
StyleX on Solid11.35 ms active(26.57 ms span)
@yak/solid (styled API, Solid 2)13.19 ms active(24.33 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false13.20 ms active(24.66 ms span)
Plumeria (classStyle prop)15.85 ms active(31.91 ms span)
@yak/react (styled API)16.19 ms active(33.48 ms span)
vanilla (hand-written ceiling)16.93 ms active(38.46 ms span)
@yak/react (styled API) foldStatic: false17.55 ms active(38.86 ms span)
@yak/react (css prop) foldStatic: false17.66 ms active(38.62 ms span)
StyleX19.18 ms active(31.94 ms span)
StyleX without CSS layers (:not() specificity hack)19.40 ms active(39.25 ms span)
@yak/react (css prop)19.83 ms active(47.61 ms span)
Goober22.54 ms active(47.27 ms span)
styled-components25.90 ms active(50.42 ms span)
Emotion27.39 ms active(41.10 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.4 ms1 recalc
Firefox1.0 ms
@yak/react (css prop) foldStatic: falseChrome3.1 ms1 recalc
Firefox1.0 ms
@yak/react (styled API) foldStatic: falseChrome3.3 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome3.7 ms1 recalc
Firefox1.0 ms
StyleXChrome3.4 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome3.1 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome3.6 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome3.1 ms1 recalc
Firefox1.0 ms
Plumeria on SolidChrome3.3 ms1 recalc
Firefox1.0 ms
@yak/react (css prop)Chrome3.3 ms1 recalc
Firefox1.0 ms
styled-componentsChrome3.5 ms1 recalc
Firefox1.0 ms
Plumeria (classStyle prop)Chrome3.0 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome3.4 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome3.0 ms1 recalc
Firefox1.0 ms
@yak/react (styled API)Chrome2.9 ms1 recalc
Firefox1.0 ms
GooberChrome3.2 ms1 recalc
Firefox2.9 ms

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

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads: 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)
@yak/react (css prop) foldStatic: false5,788 B(0.4 / 0.1 / 5.1 kB)
@yak/react (css prop)5,811 B(0.5 / 0.1 / 5.1 kB)
@yak/react (styled API)6,409 B(1.1 / 0.1 / 5.0 kB)
@yak/react (styled API) foldStatic: false6,536 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,857 B(0.4 / 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 Solid14,504 B(5.9 / 0.1 / 8.1 kB)
@yak/solid (styled API, Solid 2) foldStatic: false14,580 B(6.5 / 0.1 / 7.7 kB)
@yak/solid (styled API, Solid 2)15,220 B(7.1 / 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.01072153224291005001,0002,0004,000
EmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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 yak-react 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,160,084 r/s ±7%(0.46 ms / 1k elems)
vanilla Solid (hand-written ceiling)2,124,053 r/s ±5%(0.47 ms / 1k elems)
Plumeria on Solid1,969,190 r/s ±7%(0.51 ms / 1k elems)
cn (shadcn-ui) on Solid1,763,413 r/s ±5%(0.57 ms / 1k elems)
vanilla (hand-written ceiling)1,298,519 r/s ±4%(0.77 ms / 1k elems)
@yak/react (styled API)1,267,079 r/s ±3%(0.79 ms / 1k elems)
cn (shadcn-ui)1,256,081 r/s ±3%(0.80 ms / 1k elems)
Bamboo1,243,677 r/s ±2%(0.80 ms / 1k elems)
@yak/react (css prop)1,226,911 r/s ±3%(0.82 ms / 1k elems)
tailwind-merge1,223,664 r/s ±6%(0.82 ms / 1k elems)
Plumeria (classStyle prop)1,102,374 r/s ±20%(0.91 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false1,059,829 r/s ±4%(0.94 ms / 1k elems)
@yak/react (css prop) foldStatic: false993,075 r/s ±2%(1.01 ms / 1k elems)
@yak/react (styled API) foldStatic: false776,413 r/s ±3%(1.29 ms / 1k elems)
StyleX763,920 r/s ±2%(1.31 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)751,733 r/s ±3%(1.33 ms / 1k elems)
Panda (css fn)726,196 r/s ±3%(1.38 ms / 1k elems)
Goober644,404 r/s ±2%(1.55 ms / 1k elems)
StyleX on Solid640,510 r/s ±3%(1.56 ms / 1k elems)
styled-components505,945 r/s ±1%(1.98 ms / 1k elems)
Panda (style props)428,530 r/s ±2%(2.33 ms / 1k elems)
Emotion287,924 r/s ±1%(3.47 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,601 req/s ±2%
vanilla Solid (hand-written ceiling)1,600 req/s ±1%
@yak/solid (styled API, Solid 2)1,540 req/s ±2%
cn (shadcn-ui) on Solid1,319 req/s ±2%
@yak/react (css prop)1,144 req/s ±1%
Plumeria (classStyle prop)1,128 req/s ±2%
@yak/react (styled API)1,126 req/s ±2%
vanilla (hand-written ceiling)1,125 req/s ±1%
cn (shadcn-ui)1,105 req/s ±1%
Bamboo1,094 req/s ±1%
tailwind-merge1,062 req/s ±2%
@yak/solid (styled API, Solid 2) foldStatic: false1,014 req/s ±1%
@yak/react (css prop) foldStatic: false960 req/s ±1%
@yak/react (styled API) foldStatic: false764 req/s ±1%
StyleX748 req/s ±1%
StyleX without CSS layers (:not() specificity hack)735 req/s ±2%
Goober634 req/s ±1%
Panda (css fn)613 req/s ±1%
StyleX on Solid572 req/s ±2%
styled-components517 req/s ±1%
Panda (style props)461 req/s ±1%
Emotion337 req/s ±3%

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)0.59 ms
Plumeria on Solid0.76 ms
cn (shadcn-ui) on Solid0.88 ms
@yak/react (css prop) foldStatic: false1.07 ms
@yak/solid (styled API, Solid 2)1.10 ms
cn (shadcn-ui)1.12 ms
tailwind-merge1.14 ms
vanilla (hand-written ceiling)1.25 ms
StyleX1.37 ms
Bamboo1.44 ms
Plumeria (classStyle prop)1.44 ms
StyleX without CSS layers (:not() specificity hack)1.45 ms
@yak/solid (styled API, Solid 2) foldStatic: false1.59 ms
@yak/react (styled API) foldStatic: false1.67 ms
Goober1.68 ms
@yak/react (styled API)1.74 ms
@yak/react (css prop)1.77 ms
Panda (style props)2.30 ms
Panda (css fn)2.37 ms
StyleX on Solid2.45 ms
styled-components2.51 ms
Emotion3.81 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/react (css prop)66 ms ±5%
vanilla (hand-written ceiling)67 ms ±4%
@yak/react (styled API)67 ms ±3%
Panda (css fn)67 ms ±2%
Bamboo68 ms ±3%
Plumeria (classStyle prop)68 ms ±4%
cn (shadcn-ui)69 ms ±5%
@yak/react (css prop) foldStatic: false69 ms ±6%
@yak/react (styled API) foldStatic: false72 ms ±3%
StyleX without CSS layers (:not() specificity hack)75 ms ±2%
StyleX76 ms ±5%
@yak/solid (styled API, Solid 2)77 ms ±4%
vanilla Solid (hand-written ceiling)78 ms ±5%
Plumeria on Solid78 ms ±3%
cn (shadcn-ui) on Solid79 ms ±4%
Goober83 ms ±3%
styled-components83 ms ±4%
tailwind-merge90 ms ±2%
Panda (style props)92 ms ±4%
Emotion93 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false98 ms ±3%
StyleX on Solid102 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)7.82 ms active(24.66 ms span)
Plumeria on Solid8.30 ms active(25.27 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false9.78 ms active(23.56 ms span)
@yak/solid (styled API, Solid 2)9.85 ms active(24.65 ms span)
StyleX on Solid10.29 ms active(24.56 ms span)
@yak/react (css prop)11.57 ms active(34.27 ms span)
Bamboo11.59 ms active(34.89 ms span)
cn (shadcn-ui) on Solid11.92 ms active(14.74 ms span)
@yak/react (styled API) foldStatic: false12.15 ms active(29.92 ms span)
@yak/react (css prop) foldStatic: false12.35 ms active(32.52 ms span)
styled-components12.43 ms active(30.69 ms span)
Plumeria (classStyle prop)12.51 ms active(35.95 ms span)
Panda (css fn)12.54 ms active(27.95 ms span)
StyleX without CSS layers (:not() specificity hack)12.58 ms active(32.32 ms span)
Goober13.38 ms active(29.87 ms span)
cn (shadcn-ui)13.82 ms active(20.07 ms span)
@yak/react (styled API)14.07 ms active(35.32 ms span)
Panda (style props)14.38 ms active(26.71 ms span)
StyleX14.55 ms active(24.21 ms span)
Emotion14.85 ms active(37.09 ms span)
vanilla (hand-written ceiling)14.91 ms active(30.57 ms span)
tailwind-merge21.22 ms active(36.74 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.

Bamboo58 ms ±6%
vanilla (hand-written ceiling)60 ms ±8%
cn (shadcn-ui)61 ms ±4%
cn (shadcn-ui) on Solid63 ms ±7%
Panda (css fn)63 ms ±5%
@yak/react (styled API) foldStatic: false65 ms ±3%
@yak/react (css prop) foldStatic: false65 ms ±5%
@yak/solid (styled API, Solid 2)65 ms ±6%
vanilla Solid (hand-written ceiling)66 ms ±5%
@yak/react (css prop)67 ms ±5%
@yak/react (styled API)68 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false68 ms ±8%
styled-components73 ms ±4%
Plumeria on Solid74 ms ±7%
Goober74 ms ±7%
tailwind-merge74 ms ±5%
Emotion77 ms ±3%
Plumeria (classStyle prop)80 ms ±6%
Panda (style props)82 ms ±3%
StyleX without CSS layers (:not() specificity hack)92 ms ±12%
StyleX92 ms ±11%
StyleX on Solid98 ms ±8%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/react (css prop)1.64 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
tailwind-merge1.73 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false1.74 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false1.77 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
Plumeria (classStyle prop)1.78 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)1.79 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
styled-components1.90 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false2.01 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
vanilla (hand-written ceiling)2.03 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
StyleX2.13 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
cn (shadcn-ui) on Solid2.17 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
StyleX on Solid2.32 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
Panda (css fn)2.36 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
Plumeria on Solid2.42 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Bamboo2.43 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Panda (style props)2.44 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)2.45 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
@yak/react (styled API)2.63 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.67 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
Goober2.71 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
cn (shadcn-ui)2.74 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
Emotion3.11 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)56 ms ±5%
vanilla Solid (hand-written ceiling)58 ms ±6%
Plumeria on Solid59 ms ±5%
cn (shadcn-ui) on Solid63 ms ±10%
@yak/react (css prop)74 ms ±3%
Plumeria (classStyle prop)74 ms ±4%
vanilla (hand-written ceiling)75 ms ±4%
Bamboo76 ms ±5%
@yak/react (css prop) foldStatic: false77 ms ±3%
@yak/react (styled API)79 ms ±5%
@yak/react (styled API) foldStatic: false80 ms ±4%
Panda (css fn)80 ms ±4%
cn (shadcn-ui)81 ms ±4%
StyleX on Solid83 ms ±8%
@yak/solid (styled API, Solid 2) foldStatic: false84 ms ±4%
StyleX without CSS layers (:not() specificity hack)87 ms ±5%
StyleX88 ms ±7%
Goober89 ms ±4%
tailwind-merge97 ms ±4%
styled-components98 ms ±2%
Panda (style props)105 ms ±5%
Emotion108 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)9.04 ms active(22.30 ms span)
Plumeria on Solid9.37 ms active(21.73 ms span)
@yak/solid (styled API, Solid 2)10.98 ms active(20.14 ms span)
StyleX on Solid11.51 ms active(22.33 ms span)
cn (shadcn-ui) on Solid11.74 ms active(11.77 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false13.79 ms active(15.11 ms span)
vanilla (hand-written ceiling)15.33 ms active(34.64 ms span)
Bamboo15.64 ms active(42.71 ms span)
Plumeria (classStyle prop)15.82 ms active(29.51 ms span)
@yak/react (css prop)16.36 ms active(32.75 ms span)
@yak/react (styled API) foldStatic: false16.48 ms active(33.74 ms span)
StyleX16.52 ms active(33.29 ms span)
@yak/react (styled API)16.66 ms active(44.46 ms span)
@yak/react (css prop) foldStatic: false17.10 ms active(30.00 ms span)
StyleX without CSS layers (:not() specificity hack)17.10 ms active(32.27 ms span)
Panda (css fn)17.15 ms active(42.88 ms span)
styled-components18.45 ms active(31.45 ms span)
Goober18.58 ms active(37.26 ms span)
cn (shadcn-ui)18.90 ms active(36.82 ms span)
Emotion21.05 ms active(33.67 ms span)
Panda (style props)24.08 ms active(36.06 ms span)
tailwind-merge27.36 ms active(43.80 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)
GooberChrome3.4 ms1 recalc
Firefox1.0 ms
@yak/react (styled API)Chrome3.2 ms1 recalc
Firefox0.0 ms
@yak/react (css prop)Chrome3.1 ms1 recalc
Firefox1.0 ms
@yak/react (css prop) foldStatic: falseChrome3.5 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome2.9 ms1 recalc
Firefox1.0 ms
StyleXChrome3.2 ms1 recalc
Firefox0.0 ms
StyleX on SolidChrome3.1 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome2.8 ms1 recalc
Firefox0.0 ms
@yak/solid (styled API, Solid 2)Chrome3.2 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome3.2 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome3.4 ms1 recalc
Firefox1.0 ms
Plumeria on SolidChrome3.2 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome3.3 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome3.1 ms1 recalc
Firefox2.0 ms
Plumeria (classStyle prop)Chrome3.1 ms1 recalc
Firefox1.0 ms
styled-componentsChrome2.9 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome3.5 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui)Chrome3.6 ms1 recalc
Firefox2.0 ms
@yak/react (styled API) foldStatic: falseChrome2.9 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome3.4 ms1 recalc
Firefox2.0 ms
EmotionChrome3.5 ms1 recalc
Firefox1.1 ms
BambooChrome3.1 ms1 recalc
Firefox3.0 ms

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

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads: 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)
@yak/react (css prop) foldStatic: false5,880 B(0.4 / 0.1 / 5.2 kB)
@yak/react (css prop)5,901 B(0.5 / 0.1 / 5.2 kB)
@yak/react (styled API)6,599 B(1.1 / 0.1 / 5.2 kB)
@yak/react (styled API) foldStatic: false6,725 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,857 B(0.4 / 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 Solid14,504 B(5.9 / 0.1 / 8.1 kB)
@yak/solid (styled API, Solid 2) foldStatic: false14,724 B(6.5 / 0.1 / 7.9 kB)
Panda (style props)15,202 B(9.2 / 0.1 / 5.5 kB)
@yak/solid (styled API, Solid 2)15,387 B(7.1 / 0.1 / 7.9 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,078 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.511151005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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.

StyleX1,412,461 r/s ±11%(0.71 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,356,157 r/s ±35%(0.74 ms / 1k elems)
cn (shadcn-ui)1,257,887 r/s ±25%(0.79 ms / 1k elems)
@yak/react (styled API)1,246,330 r/s ±35%(0.80 ms / 1k elems)
@yak/solid (styled API, Solid 2)1,185,908 r/s ±3%(0.84 ms / 1k elems)
Panda (recipe)1,025,435 r/s ±29%(0.98 ms / 1k elems)
StyleX on Solid995,997 r/s ±11%(1.00 ms / 1k elems)
Plumeria (classStyle prop)955,446 r/s ±22%(1.05 ms / 1k elems)
tailwind-merge955,334 r/s ±12%(1.05 ms / 1k elems)
Plumeria on Solid932,940 r/s ±15%(1.07 ms / 1k elems)
vanilla (hand-written ceiling)925,999 r/s ±28%(1.08 ms / 1k elems)
cn (shadcn-ui) on Solid743,611 r/s ±40%(1.34 ms / 1k elems)
vanilla Solid (hand-written ceiling)722,073 r/s ±8%(1.38 ms / 1k elems)
@yak/react (css prop) foldStatic: false699,277 r/s ±33%(1.43 ms / 1k elems)
@yak/react (css prop)669,791 r/s ±49%(1.49 ms / 1k elems)
Bamboo612,207 r/s ±49%(1.63 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false586,310 r/s ±26%(1.71 ms / 1k elems)
Panda (css fn)553,817 r/s ±27%(1.81 ms / 1k elems)
Panda (style props)378,435 r/s ±3%(2.64 ms / 1k elems)
styled-components376,540 r/s ±6%(2.66 ms / 1k elems)
Goober354,116 r/s ±18%(2.82 ms / 1k elems)
@yak/react (styled API) foldStatic: false323,685 r/s ±29%(3.09 ms / 1k elems)
Emotion218,558 r/s ±4%(4.58 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,282 req/s ±1%
@yak/react (styled API)1,483 req/s ±4%
StyleX without CSS layers (:not() specificity hack)1,281 req/s ±1%
StyleX1,273 req/s ±1%
vanilla (hand-written ceiling)1,270 req/s ±2%
@yak/react (css prop)1,266 req/s ±1%
vanilla Solid (hand-written ceiling)1,093 req/s ±1%
Plumeria (classStyle prop)1,084 req/s ±1%
cn (shadcn-ui)952 req/s ±2%
Bamboo916 req/s ±1%
Plumeria on Solid912 req/s ±1%
StyleX on Solid888 req/s ±0%
@yak/react (css prop) foldStatic: false874 req/s ±1%
cn (shadcn-ui) on Solid794 req/s ±2%
tailwind-merge782 req/s ±2%
Panda (recipe)780 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false743 req/s ±1%
@yak/react (styled API) foldStatic: false598 req/s ±1%
Panda (css fn)553 req/s ±2%
styled-components446 req/s ±0%
Panda (style props)397 req/s ±1%
Goober389 req/s ±1%
Emotion296 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
StyleX0.80 ms
@yak/solid (styled API, Solid 2)0.89 ms
vanilla Solid (hand-written ceiling)0.98 ms
@yak/react (styled API)1.06 ms
cn (shadcn-ui)1.08 ms
Plumeria (classStyle prop)1.13 ms
StyleX on Solid1.13 ms
@yak/react (css prop)1.13 ms
Plumeria on Solid1.20 ms
@yak/solid (styled API, Solid 2) foldStatic: false1.47 ms
Bamboo1.48 ms
vanilla (hand-written ceiling)1.48 ms
cn (shadcn-ui) on Solid1.51 ms
Panda (recipe)1.63 ms
Panda (css fn)1.68 ms
StyleX without CSS layers (:not() specificity hack)1.77 ms
@yak/react (css prop) foldStatic: false1.79 ms
tailwind-merge1.82 ms
@yak/react (styled API) foldStatic: false2.04 ms
Panda (style props)2.51 ms
Goober2.80 ms
styled-components3.25 ms
Emotion4.09 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/react (styled API)53 ms ±3%
@yak/react (css prop)64 ms ±1%
Bamboo66 ms ±3%
Plumeria (classStyle prop)68 ms ±3%
vanilla (hand-written ceiling)68 ms ±3%
StyleX without CSS layers (:not() specificity hack)69 ms ±6%
StyleX71 ms ±4%
@yak/solid (styled API, Solid 2)75 ms ±4%
cn (shadcn-ui)77 ms ±3%
Panda (recipe)78 ms ±3%
@yak/react (css prop) foldStatic: false78 ms ±3%
Panda (css fn)80 ms ±3%
vanilla Solid (hand-written ceiling)81 ms ±5%
Plumeria on Solid83 ms ±3%
styled-components86 ms ±3%
Goober86 ms ±4%
@yak/react (styled API) foldStatic: false86 ms ±4%
cn (shadcn-ui) on Solid87 ms ±4%
StyleX on Solid92 ms ±4%
tailwind-merge96 ms ±5%
Emotion99 ms ±4%
Panda (style props)100 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false133 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)7.85 ms active(14.36 ms span)
Plumeria on Solid8.29 ms active(14.93 ms span)
vanilla Solid (hand-written ceiling)8.44 ms active(19.28 ms span)
StyleX on Solid9.15 ms active(19.69 ms span)
cn (shadcn-ui) on Solid10.89 ms active(23.82 ms span)
StyleX without CSS layers (:not() specificity hack)11.37 ms active(23.82 ms span)
@yak/react (styled API)11.39 ms active(27.51 ms span)
Bamboo11.58 ms active(33.71 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false12.05 ms active(12.21 ms span)
Plumeria (classStyle prop)12.16 ms active(37.48 ms span)
@yak/react (css prop)12.45 ms active(25.07 ms span)
@yak/react (css prop) foldStatic: false12.62 ms active(37.82 ms span)
StyleX13.27 ms active(31.09 ms span)
Panda (recipe)13.37 ms active(32.21 ms span)
@yak/react (styled API) foldStatic: false13.91 ms active(36.36 ms span)
vanilla (hand-written ceiling)14.33 ms active(27.78 ms span)
Goober14.91 ms active(27.50 ms span)
cn (shadcn-ui)15.10 ms active(45.06 ms span)
Panda (css fn)16.13 ms active(43.52 ms span)
styled-components16.30 ms active(33.98 ms span)
Panda (style props)16.46 ms active(44.56 ms span)
Emotion16.57 ms active(27.19 ms span)
tailwind-merge18.51 ms active(25.55 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/react (css prop)72 ms ±1%
@yak/solid (styled API, Solid 2)72 ms ±3%
cn (shadcn-ui)73 ms ±6%
vanilla (hand-written ceiling)73 ms ±2%
@yak/react (styled API)75 ms ±2%
vanilla Solid (hand-written ceiling)75 ms ±2%
Bamboo76 ms ±3%
cn (shadcn-ui) on Solid76 ms ±2%
StyleX76 ms ±2%
@yak/react (css prop) foldStatic: false78 ms ±4%
tailwind-merge78 ms ±3%
Plumeria (classStyle prop)79 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false80 ms ±1%
Panda (css fn)80 ms ±2%
@yak/react (styled API) foldStatic: false80 ms ±9%
StyleX without CSS layers (:not() specificity hack)80 ms ±1%
Goober80 ms ±3%
Panda (recipe)81 ms ±5%
Plumeria on Solid81 ms ±7%
StyleX on Solid82 ms ±6%
styled-components82 ms ±3%
Panda (style props)85 ms ±7%
Emotion91 ms ±8%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
cn (shadcn-ui) on Solid2.09 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.26 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
cn (shadcn-ui)2.55 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX on Solid2.62 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.63 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
StyleX2.70 ms active(33.25 ms span · 33.25 ms profiled median (n=5))
Bamboo2.70 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Emotion2.72 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
vanilla (hand-written ceiling)2.90 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
styled-components2.97 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false3.25 ms active(33.19 ms span · 33.19 ms profiled median (n=5))
@yak/react (styled API)3.34 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
tailwind-merge3.36 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false3.37 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Plumeria on Solid3.39 ms active(33.38 ms span · 33.38 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false3.46 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)3.48 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
Plumeria (classStyle prop)3.48 ms active(33.19 ms span · 33.19 ms profiled median (n=5))
@yak/react (css prop)3.54 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Goober3.57 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
Panda (recipe)3.60 ms active(33.25 ms span · 33.25 ms profiled median (n=5))
Panda (css fn)3.64 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
Panda (style props)4.43 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)49 ms ±4%
Plumeria on Solid59 ms ±2%
vanilla Solid (hand-written ceiling)59 ms ±5%
@yak/react (styled API)60 ms ±5%
cn (shadcn-ui) on Solid65 ms ±6%
StyleX on Solid66 ms ±5%
vanilla (hand-written ceiling)72 ms ±3%
@yak/react (css prop)73 ms ±3%
StyleX without CSS layers (:not() specificity hack)73 ms ±4%
Bamboo73 ms ±6%
Plumeria (classStyle prop)75 ms ±4%
StyleX76 ms ±4%
cn (shadcn-ui)79 ms ±7%
@yak/react (css prop) foldStatic: false81 ms ±3%
Panda (css fn)83 ms ±3%
Panda (recipe)90 ms ±4%
@yak/react (styled API) foldStatic: false92 ms ±4%
Goober93 ms ±6%
tailwind-merge99 ms ±3%
styled-components100 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false104 ms ±6%
Panda (style props)108 ms ±4%
Emotion114 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)11.98 ms active(28.72 ms span)
Plumeria on Solid12.12 ms active(26.13 ms span)
@yak/solid (styled API, Solid 2)13.46 ms active(20.02 ms span)
cn (shadcn-ui) on Solid14.32 ms active(20.95 ms span)
StyleX on Solid15.62 ms active(26.07 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false16.28 ms active(22.56 ms span)
@yak/react (styled API)17.89 ms active(34.22 ms span)
vanilla (hand-written ceiling)18.32 ms active(39.52 ms span)
StyleX18.52 ms active(30.54 ms span)
Bamboo18.81 ms active(33.28 ms span)
Plumeria (classStyle prop)19.22 ms active(42.80 ms span)
StyleX without CSS layers (:not() specificity hack)19.32 ms active(31.93 ms span)
Panda (css fn)19.38 ms active(38.08 ms span)
@yak/react (css prop) foldStatic: false19.60 ms active(43.01 ms span)
@yak/react (styled API) foldStatic: false19.96 ms active(34.46 ms span)
@yak/react (css prop)19.96 ms active(33.85 ms span)
Goober20.17 ms active(34.41 ms span)
Panda (recipe)20.95 ms active(42.78 ms span)
cn (shadcn-ui)22.38 ms active(35.89 ms span)
Panda (style props)22.62 ms active(47.06 ms span)
styled-components24.06 ms active(39.55 ms span)
tailwind-merge26.35 ms active(41.12 ms span)
Emotion29.62 ms active(36.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.8 ms1 recalc
Firefox1.0 ms
@yak/react (styled API)Chrome5.0 ms1 recalc
Firefox2.0 ms
@yak/react (css prop)Chrome5.0 ms1 recalc
Firefox1.0 ms
@yak/react (css prop) foldStatic: falseChrome5.4 ms1 recalc
Firefox3.0 ms
@yak/react (styled API) foldStatic: falseChrome5.7 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome5.2 ms1 recalc
Firefox2.0 ms
Panda (style props)Chrome5.1 ms1 recalc
Firefox1.0 ms
styled-componentsChrome5.6 ms1 recalc
Firefox2.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.1 ms1 recalc
Firefox2.0 ms
StyleXChrome5.4 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome5.9 ms1 recalc
Firefox2.0 ms
tailwind-mergeChrome5.3 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome5.1 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2)Chrome5.7 ms1 recalc
Firefox1.0 ms
Plumeria on SolidChrome5.2 ms1 recalc
Firefox1.0 ms
Plumeria (classStyle prop)Chrome5.3 ms1 recalc
Firefox2.0 ms
EmotionChrome5.7 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome5.5 ms1 recalc
Firefox2.0 ms
Panda (recipe)Chrome5.0 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome5.4 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui) on SolidChrome5.1 ms1 recalc
Firefox3.0 ms
GooberChrome5.0 ms1 recalc
Firefox3.0 ms
BambooChrome5.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)3,789 B(0.0 / 0.5 / 3.2 kB)
@yak/react (css prop) foldStatic: false3,843 B(0.4 / 0.2 / 3.1 kB)
@yak/react (css prop)3,863 B(0.5 / 0.2 / 3.1 kB)
@yak/react (styled API)4,565 B(1.1 / 0.2 / 3.1 kB)
@yak/react (styled API) foldStatic: false4,686 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)
Panda (recipe)6,472 B(1.4 / 0.3 / 4.6 kB)
Plumeria on Solid6,548 B(0.4 / 0.3 / 5.7 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: false12,030 B(6.5 / 0.2 / 5.1 kB)
StyleX on Solid12,396 B(5.9 / 0.3 / 5.9 kB)
@yak/solid (styled API, Solid 2)12,848 B(7.1 / 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,935 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.58.913181005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on SolidEmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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 @yak/react'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)4,801,644 r/s ±15%(0.21 ms / 1k elems)
vanilla Solid (hand-written ceiling)2,970,400 r/s ±8%(0.34 ms / 1k elems)
Plumeria on Solid2,904,883 r/s ±31%(0.34 ms / 1k elems)
StyleX on Solid2,780,868 r/s ±7%(0.36 ms / 1k elems)
cn (shadcn-ui) on Solid2,089,917 r/s ±6%(0.48 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false1,918,970 r/s ±41%(0.52 ms / 1k elems)
@yak/react (styled API)1,905,782 r/s ±3%(0.52 ms / 1k elems)
vanilla (hand-written ceiling)1,608,946 r/s ±2%(0.62 ms / 1k elems)
StyleX1,585,671 r/s ±2%(0.63 ms / 1k elems)
Bamboo1,546,932 r/s ±2%(0.65 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,530,498 r/s ±2%(0.65 ms / 1k elems)
@yak/react (css prop)1,506,144 r/s ±3%(0.66 ms / 1k elems)
tailwind-merge1,502,314 r/s ±5%(0.67 ms / 1k elems)
Plumeria (classStyle prop)1,494,065 r/s ±6%(0.67 ms / 1k elems)
cn (shadcn-ui)1,447,895 r/s ±2%(0.69 ms / 1k elems)
@yak/react (css prop) foldStatic: false1,247,435 r/s ±3%(0.80 ms / 1k elems)
@yak/react (styled API) foldStatic: false1,053,802 r/s ±1%(0.95 ms / 1k elems)
Panda (css fn)882,214 r/s ±8%(1.13 ms / 1k elems)
Goober800,598 r/s ±1%(1.25 ms / 1k elems)
styled-components605,436 r/s ±1%(1.65 ms / 1k elems)
Panda (style props)492,915 r/s ±1%(2.03 ms / 1k elems)
Emotion323,948 r/s ±1%(3.09 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,422 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false2,596 req/s ±1%
vanilla Solid (hand-written ceiling)2,358 req/s ±0%
StyleX on Solid2,278 req/s ±3%
Plumeria on Solid2,266 req/s ±1%
cn (shadcn-ui) on Solid1,690 req/s ±2%
@yak/react (styled API)1,688 req/s ±1%
@yak/react (css prop)1,463 req/s ±1%
vanilla (hand-written ceiling)1,456 req/s ±2%
StyleX without CSS layers (:not() specificity hack)1,433 req/s ±0%
StyleX1,425 req/s ±1%
Plumeria (classStyle prop)1,403 req/s ±2%
Bamboo1,377 req/s ±0%
cn (shadcn-ui)1,347 req/s ±1%
tailwind-merge1,311 req/s ±0%
@yak/react (css prop) foldStatic: false1,263 req/s ±1%
@yak/react (styled API) foldStatic: false1,119 req/s ±2%
Goober837 req/s ±1%
styled-components646 req/s ±0%
Panda (css fn)580 req/s ±0%
Panda (style props)508 req/s ±0%
Emotion393 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 Solid0.51 ms
@yak/solid (styled API, Solid 2) foldStatic: false0.54 ms
@yak/react (styled API)0.65 ms
@yak/solid (styled API, Solid 2)0.81 ms
cn (shadcn-ui)0.87 ms
vanilla Solid (hand-written ceiling)0.87 ms
StyleX on Solid0.88 ms
StyleX0.97 ms
cn (shadcn-ui) on Solid1.00 ms
@yak/react (css prop) foldStatic: false1.04 ms
StyleX without CSS layers (:not() specificity hack)1.07 ms
@yak/react (css prop)1.17 ms
Plumeria (classStyle prop)1.27 ms
Bamboo1.33 ms
@yak/react (styled API) foldStatic: false1.39 ms
vanilla (hand-written ceiling)1.49 ms
Goober1.56 ms
tailwind-merge1.66 ms
Panda (style props)1.77 ms
Panda (css fn)2.08 ms
styled-components2.90 ms
Emotion3.05 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/react (styled API)53 ms ±3%
@yak/react (css prop)55 ms ±2%
Bamboo55 ms ±2%
vanilla (hand-written ceiling)56 ms ±3%
StyleX57 ms ±4%
StyleX without CSS layers (:not() specificity hack)58 ms ±4%
Plumeria (classStyle prop)58 ms ±4%
@yak/solid (styled API, Solid 2)69 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false69 ms ±4%
@yak/react (css prop) foldStatic: false70 ms ±3%
Panda (css fn)70 ms ±2%
@yak/react (styled API) foldStatic: false70 ms ±4%
cn (shadcn-ui)72 ms ±3%
StyleX on Solid73 ms ±3%
Plumeria on Solid75 ms ±4%
vanilla Solid (hand-written ceiling)75 ms ±3%
cn (shadcn-ui) on Solid78 ms ±3%
Goober79 ms ±4%
styled-components81 ms ±2%
tailwind-merge87 ms ±3%
Emotion90 ms ±3%
Panda (style props)97 ms ±7%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)7.19 ms active(10.47 ms span)
vanilla Solid (hand-written ceiling)7.40 ms active(18.79 ms span)
StyleX on Solid7.69 ms active(13.99 ms span)
Plumeria on Solid7.73 ms active(25.46 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false7.79 ms active(16.12 ms span)
cn (shadcn-ui) on Solid10.33 ms active(20.49 ms span)
@yak/react (css prop)11.35 ms active(34.66 ms span)
Plumeria (classStyle prop)11.52 ms active(30.04 ms span)
StyleX11.93 ms active(26.33 ms span)
vanilla (hand-written ceiling)12.04 ms active(27.00 ms span)
Panda (css fn)12.06 ms active(26.68 ms span)
StyleX without CSS layers (:not() specificity hack)12.08 ms active(33.17 ms span)
@yak/react (styled API)12.13 ms active(34.99 ms span)
@yak/react (css prop) foldStatic: false12.51 ms active(27.19 ms span)
Emotion13.23 ms active(33.48 ms span)
@yak/react (styled API) foldStatic: false13.73 ms active(30.95 ms span)
cn (shadcn-ui)14.82 ms active(46.04 ms span)
Panda (style props)14.94 ms active(33.84 ms span)
Bamboo14.95 ms active(31.91 ms span)
Goober15.03 ms active(22.05 ms span)
styled-components16.10 ms active(25.35 ms span)
tailwind-merge19.21 ms active(30.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.

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.

Bamboo59 ms ±3%
cn (shadcn-ui) on Solid59 ms ±3%
vanilla Solid (hand-written ceiling)59 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false60 ms ±5%
cn (shadcn-ui)60 ms ±3%
@yak/solid (styled API, Solid 2)61 ms ±2%
vanilla (hand-written ceiling)61 ms ±2%
StyleX on Solid63 ms ±6%
Plumeria on Solid63 ms ±5%
StyleX without CSS layers (:not() specificity hack)64 ms ±5%
@yak/react (css prop) foldStatic: false64 ms ±5%
@yak/react (styled API) foldStatic: false65 ms ±3%
tailwind-merge66 ms ±3%
@yak/react (styled API)66 ms ±4%
Panda (css fn)67 ms ±5%
@yak/react (css prop)67 ms ±5%
StyleX67 ms ±4%
Goober70 ms ±5%
Panda (style props)72 ms ±8%
Plumeria (classStyle prop)73 ms ±7%
Emotion74 ms ±6%
styled-components74 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/react (styled API) foldStatic: false1.64 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)1.65 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/react (css prop)1.77 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
@yak/react (styled API)1.78 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Bamboo1.81 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
Panda (css fn)1.86 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Goober1.92 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
vanilla (hand-written ceiling)1.94 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
cn (shadcn-ui) on Solid1.97 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)2.06 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
StyleX on Solid2.06 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
Panda (style props)2.12 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.13 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.16 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
StyleX2.18 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Plumeria (classStyle prop)2.47 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
cn (shadcn-ui)2.48 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
tailwind-merge2.62 ms active(33.19 ms span · 33.19 ms profiled median (n=5))
styled-components2.91 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Plumeria on Solid2.98 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false3.21 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Emotion3.37 ms active(33.27 ms span · 33.27 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)35 ms ±4%
StyleX on Solid47 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false48 ms ±4%
Plumeria on Solid48 ms ±5%
vanilla Solid (hand-written ceiling)49 ms ±5%
@yak/react (styled API)57 ms ±5%
Bamboo59 ms ±3%
StyleX without CSS layers (:not() specificity hack)60 ms ±3%
vanilla (hand-written ceiling)61 ms ±3%
StyleX61 ms ±2%
Plumeria (classStyle prop)61 ms ±3%
cn (shadcn-ui) on Solid62 ms ±16%
@yak/react (css prop)62 ms ±4%
@yak/react (styled API) foldStatic: false74 ms ±6%
Panda (css fn)74 ms ±2%
@yak/react (css prop) foldStatic: false75 ms ±5%
cn (shadcn-ui)75 ms ±4%
Goober79 ms ±5%
styled-components91 ms ±4%
tailwind-merge95 ms ±4%
Emotion100 ms ±3%
Panda (style props)100 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)10.39 ms active(17.47 ms span)
Plumeria on Solid10.61 ms active(25.76 ms span)
@yak/solid (styled API, Solid 2)10.70 ms active(21.49 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false10.78 ms active(18.43 ms span)
StyleX on Solid11.31 ms active(24.29 ms span)
cn (shadcn-ui) on Solid13.10 ms active(24.33 ms span)
@yak/react (css prop)16.78 ms active(39.73 ms span)
@yak/react (styled API) foldStatic: false17.34 ms active(29.38 ms span)
StyleX17.58 ms active(43.74 ms span)
@yak/react (styled API)18.02 ms active(35.42 ms span)
StyleX without CSS layers (:not() specificity hack)18.06 ms active(32.31 ms span)
Plumeria (classStyle prop)18.34 ms active(33.24 ms span)
@yak/react (css prop) foldStatic: false18.79 ms active(34.91 ms span)
vanilla (hand-written ceiling)19.48 ms active(41.13 ms span)
Bamboo19.53 ms active(40.84 ms span)
Goober19.84 ms active(37.09 ms span)
cn (shadcn-ui)20.28 ms active(41.90 ms span)
styled-components20.42 ms active(44.22 ms span)
Panda (css fn)22.13 ms active(37.16 ms span)
Emotion22.19 ms active(33.42 ms span)
Panda (style props)22.44 ms active(48.26 ms span)
tailwind-merge26.10 ms active(48.72 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.0 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui)Chrome4.8 ms1 recalc
Firefox1.0 ms
EmotionChrome5.2 ms1 recalc
Firefox2.0 ms
@yak/react (css prop)Chrome4.7 ms1 recalc
Firefox1.0 ms
@yak/react (styled API) foldStatic: falseChrome4.7 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome5.5 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.2 ms1 recalc
Firefox1.0 ms
StyleXChrome4.6 ms1 recalc
Firefox1.0 ms
StyleX on SolidChrome5.0 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome4.9 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome5.7 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome4.6 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome5.1 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.7 ms1 recalc
Firefox1.0 ms
Plumeria (classStyle prop)Chrome5.4 ms1 recalc
Firefox1.0 ms
GooberChrome5.3 ms1 recalc
Firefox2.0 ms
@yak/react (css prop) foldStatic: falseChrome4.8 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui) on SolidChrome4.9 ms1 recalc
Firefox2.0 ms
styled-componentsChrome5.2 ms1 recalc
Firefox2.0 ms
Plumeria on SolidChrome4.7 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome5.5 ms1 recalc
Firefox2.0 ms
@yak/react (styled API)Chrome5.2 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)
@yak/react (css prop) foldStatic: false3,134 B(0.4 / 0.1 / 2.5 kB)
@yak/react (css prop)3,155 B(0.5 / 0.1 / 2.5 kB)
@yak/react (styled API)3,855 B(1.1 / 0.1 / 2.5 kB)
@yak/react (styled API) foldStatic: false3,984 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,937 B(0.4 / 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 Solid11,591 B(5.9 / 0.2 / 5.2 kB)
@yak/solid (styled API, Solid 2) foldStatic: false11,801 B(6.5 / 0.1 / 4.9 kB)
@yak/solid (styled API, Solid 2)12,570 B(7.1 / 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,492 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 SolidEmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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. @yak/react 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)5,005,782 r/s ±13%(0.20 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false3,414,251 r/s ±4%(0.29 ms / 1k elems)
@yak/react (styled API)1,948,289 r/s ±5%(0.51 ms / 1k elems)
cn (shadcn-ui) on Solid1,132,403 r/s ±5%(0.88 ms / 1k elems)
vanilla Solid (hand-written ceiling)1,131,031 r/s ±7%(0.88 ms / 1k elems)
vanilla (hand-written ceiling)1,083,121 r/s ±2%(0.92 ms / 1k elems)
Bamboo1,042,240 r/s ±4%(0.96 ms / 1k elems)
cn (shadcn-ui)1,024,667 r/s ±2%(0.98 ms / 1k elems)
@yak/react (styled API) foldStatic: false917,947 r/s ±1%(1.09 ms / 1k elems)
StyleX811,383 r/s ±2%(1.23 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)792,629 r/s ±2%(1.26 ms / 1k elems)
StyleX on Solid703,583 r/s ±4%(1.42 ms / 1k elems)
@yak/react (css prop) foldStatic: false686,630 r/s ±1%(1.46 ms / 1k elems)
tailwind-merge653,964 r/s ±3%(1.53 ms / 1k elems)
@yak/react (css prop)632,305 r/s ±3%(1.58 ms / 1k elems)
styled-components552,657 r/s ±1%(1.81 ms / 1k elems)
Panda (style props)525,204 r/s ±1%(1.90 ms / 1k elems)
Goober377,130 r/s ±1%(2.65 ms / 1k elems)
Panda (css fn)362,848 r/s ±2%(2.76 ms / 1k elems)
Emotion284,571 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.

@yak/solid (styled API, Solid 2)3,326 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false2,521 req/s ±1%
@yak/react (styled API)1,681 req/s ±3%
vanilla (hand-written ceiling)1,041 req/s ±2%
vanilla Solid (hand-written ceiling)993 req/s ±1%
@yak/react (styled API) foldStatic: false920 req/s ±2%
Bamboo807 req/s ±3%
cn (shadcn-ui)807 req/s ±1%
StyleX797 req/s ±1%
StyleX without CSS layers (:not() specificity hack)794 req/s ±2%
cn (shadcn-ui) on Solid767 req/s ±0%
@yak/react (css prop) foldStatic: false729 req/s ±1%
@yak/react (css prop)711 req/s ±1%
StyleX on Solid643 req/s ±1%
styled-components588 req/s ±1%
tailwind-merge566 req/s ±1%
Panda (style props)510 req/s ±1%
Goober422 req/s ±1%
Emotion337 req/s ±1%
Panda (css fn)327 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.56 ms
@yak/solid (styled API, Solid 2)0.72 ms
cn (shadcn-ui) on Solid1.04 ms
Bamboo1.06 ms
@yak/react (styled API)1.09 ms
vanilla (hand-written ceiling)1.28 ms
StyleX1.34 ms
vanilla Solid (hand-written ceiling)1.40 ms
StyleX without CSS layers (:not() specificity hack)1.43 ms
@yak/react (css prop) foldStatic: false1.57 ms
StyleX on Solid1.57 ms
cn (shadcn-ui)1.65 ms
@yak/react (css prop)1.70 ms
tailwind-merge1.76 ms
@yak/react (styled API) foldStatic: false1.92 ms
Panda (style props)2.01 ms
styled-components2.67 ms
Goober2.75 ms
Panda (css fn)3.17 ms
Emotion3.44 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/react (styled API)54 ms ±4%
@yak/solid (styled API, Solid 2)68 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false70 ms ±4%
@yak/react (styled API) foldStatic: false71 ms ±3%
vanilla (hand-written ceiling)71 ms ±14%
Bamboo74 ms ±5%
@yak/react (css prop) foldStatic: false75 ms ±3%
@yak/react (css prop)76 ms ±4%
StyleX79 ms ±10%
cn (shadcn-ui)80 ms ±4%
StyleX without CSS layers (:not() specificity hack)80 ms ±4%
styled-components82 ms ±3%
vanilla Solid (hand-written ceiling)85 ms ±3%
Goober90 ms ±2%
Panda (style props)92 ms ±7%
cn (shadcn-ui) on Solid94 ms ±4%
tailwind-merge95 ms ±4%
StyleX on Solid96 ms ±4%
Emotion97 ms ±5%
Panda (css fn)98 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)7.58 ms active(11.38 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false7.62 ms active(21.50 ms span)
vanilla Solid (hand-written ceiling)8.34 ms active(10.38 ms span)
cn (shadcn-ui) on Solid9.97 ms active(27.04 ms span)
StyleX on Solid9.98 ms active(16.97 ms span)
Bamboo11.62 ms active(33.35 ms span)
@yak/react (css prop)11.83 ms active(20.74 ms span)
@yak/react (styled API) foldStatic: false11.84 ms active(22.12 ms span)
@yak/react (css prop) foldStatic: false12.29 ms active(28.08 ms span)
@yak/react (styled API)12.73 ms active(21.01 ms span)
styled-components12.79 ms active(35.58 ms span)
StyleX13.20 ms active(33.78 ms span)
vanilla (hand-written ceiling)13.36 ms active(34.06 ms span)
Goober13.83 ms active(36.88 ms span)
Emotion14.05 ms active(24.37 ms span)
StyleX without CSS layers (:not() specificity hack)14.87 ms active(31.52 ms span)
cn (shadcn-ui)14.90 ms active(41.70 ms span)
Panda (css fn)15.07 ms active(27.01 ms span)
Panda (style props)17.74 ms active(47.12 ms span)
tailwind-merge19.59 ms active(35.36 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 Solid61 ms ±5%
@yak/solid (styled API, Solid 2)62 ms ±3%
StyleX on Solid63 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false64 ms ±3%
vanilla (hand-written ceiling)65 ms ±4%
@yak/react (styled API)65 ms ±4%
Bamboo65 ms ±4%
tailwind-merge67 ms ±5%
@yak/react (styled API) foldStatic: false68 ms ±2%
vanilla Solid (hand-written ceiling)68 ms ±4%
cn (shadcn-ui)68 ms ±4%
StyleX without CSS layers (:not() specificity hack)71 ms ±4%
Panda (style props)72 ms ±5%
@yak/react (css prop) foldStatic: false73 ms ±5%
Emotion74 ms ±7%
@yak/react (css prop)74 ms ±3%
styled-components74 ms ±4%
StyleX75 ms ±3%
Goober78 ms ±2%
Panda (css fn)79 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
cn (shadcn-ui) on Solid1.90 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (styled API)1.96 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
cn (shadcn-ui)2.04 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Goober2.11 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.17 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Bamboo2.23 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.23 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
Panda (css fn)2.27 ms active(33.25 ms span · 33.25 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)2.32 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
styled-components2.36 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
StyleX on Solid2.42 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
tailwind-merge2.61 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.67 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
@yak/react (css prop)2.86 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX2.86 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Emotion3.43 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false3.51 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false3.53 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
Panda (style props)4.13 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
vanilla (hand-written ceiling)5.98 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)35 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false47 ms ±4%
@yak/react (styled API)59 ms ±6%
cn (shadcn-ui) on Solid68 ms ±3%
StyleX on Solid73 ms ±4%
vanilla Solid (hand-written ceiling)74 ms ±14%
@yak/react (styled API) foldStatic: false76 ms ±2%
Bamboo77 ms ±2%
vanilla (hand-written ceiling)77 ms ±6%
@yak/react (css prop)82 ms ±3%
cn (shadcn-ui)82 ms ±9%
@yak/react (css prop) foldStatic: false83 ms ±5%
StyleX without CSS layers (:not() specificity hack)84 ms ±4%
StyleX86 ms ±4%
styled-components92 ms ±4%
Goober95 ms ±4%
Panda (css fn)102 ms ±3%
Panda (style props)102 ms ±4%
tailwind-merge103 ms ±4%
Emotion106 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)11.06 ms active(11.08 ms span)
vanilla Solid (hand-written ceiling)11.53 ms active(22.31 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false13.11 ms active(20.45 ms span)
cn (shadcn-ui) on Solid13.33 ms active(14.87 ms span)
StyleX on Solid13.88 ms active(22.95 ms span)
vanilla (hand-written ceiling)17.46 ms active(30.68 ms span)
@yak/react (styled API)17.52 ms active(28.66 ms span)
@yak/react (css prop)18.09 ms active(29.29 ms span)
Bamboo18.48 ms active(42.00 ms span)
StyleX without CSS layers (:not() specificity hack)18.51 ms active(33.80 ms span)
@yak/react (css prop) foldStatic: false18.93 ms active(30.36 ms span)
Goober20.26 ms active(32.92 ms span)
@yak/react (styled API) foldStatic: false21.31 ms active(45.97 ms span)
styled-components21.80 ms active(31.97 ms span)
Panda (css fn)22.05 ms active(36.82 ms span)
StyleX22.09 ms active(39.96 ms span)
cn (shadcn-ui)22.85 ms active(40.30 ms span)
Emotion23.69 ms active(38.26 ms span)
Panda (style props)25.41 ms active(46.69 ms span)
tailwind-merge29.81 ms active(50.18 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.4 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui)Chrome5.4 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui) on SolidChrome4.6 ms1 recalc
Firefox1.0 ms
GooberChrome5.0 ms1 recalc
Firefox1.0 ms
@yak/react (styled API)Chrome4.8 ms1 recalc
Firefox1.0 ms
@yak/react (css prop)Chrome5.0 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome5.2 ms1 recalc
Firefox2.0 ms
styled-componentsChrome5.3 ms1 recalc
Firefox1.0 ms
StyleXChrome5.7 ms1 recalc
Firefox1.0 ms
StyleX on SolidChrome5.6 ms1 recalc
Firefox2.0 ms
tailwind-mergeChrome5.0 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome4.9 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome5.0 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome5.1 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome5.6 ms1 recalc
Firefox2.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.0 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome6.2 ms1 recalc
Firefox3.0 ms
@yak/react (css prop) foldStatic: falseChrome5.1 ms1 recalc
Firefox2.0 ms
@yak/react (styled API) foldStatic: falseChrome5.1 ms1 recalc
Firefox2.0 ms
EmotionChrome5.6 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,046 B(0.0 / 0.4 / 2.6 kB)
@yak/react (css prop) foldStatic: false3,254 B(0.4 / 0.2 / 2.6 kB)
@yak/react (css prop)3,273 B(0.5 / 0.2 / 2.6 kB)
@yak/react (styled API)3,973 B(1.1 / 0.2 / 2.6 kB)
@yak/react (styled API) foldStatic: false4,101 B(1.3 / 0.2 / 2.6 kB)
vanilla Solid (hand-written ceiling)5,193 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,918 B(5.9 / 0.3 / 5.5 kB)
@yak/solid (styled API, Solid 2) foldStatic: false12,058 B(6.5 / 0.2 / 5.1 kB)
@yak/solid (styled API, Solid 2)12,663 B(7.1 / 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,971 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 SolidEmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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 @yak/react'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,656,269 r/s ±6%(0.18 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false3,551,489 r/s ±4%(0.28 ms / 1k elems)
@yak/react (styled API)1,946,048 r/s ±2%(0.51 ms / 1k elems)
@yak/react (styled API) foldStatic: false854,358 r/s ±1%(1.17 ms / 1k elems)
vanilla (hand-written ceiling)732,276 r/s ±2%(1.37 ms / 1k elems)
Bamboo713,141 r/s ±2%(1.40 ms / 1k elems)
cn (shadcn-ui)676,691 r/s ±1%(1.48 ms / 1k elems)
vanilla Solid (hand-written ceiling)642,538 r/s ±7%(1.56 ms / 1k elems)
cn (shadcn-ui) on Solid615,327 r/s ±4%(1.63 ms / 1k elems)
StyleX567,683 r/s ±1%(1.76 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)552,028 r/s ±1%(1.81 ms / 1k elems)
styled-components502,376 r/s ±1%(1.99 ms / 1k elems)
StyleX on Solid456,516 r/s ±4%(2.19 ms / 1k elems)
@yak/react (css prop) foldStatic: false428,657 r/s ±1%(2.33 ms / 1k elems)
@yak/react (css prop)423,935 r/s ±2%(2.36 ms / 1k elems)
Panda (style props)399,357 r/s ±2%(2.50 ms / 1k elems)
tailwind-merge369,718 r/s ±5%(2.70 ms / 1k elems)
Emotion231,755 r/s ±1%(4.31 ms / 1k elems)
Goober210,760 r/s ±1%(4.74 ms / 1k elems)
Panda (css fn)175,425 r/s ±2%(5.70 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,212 req/s ±0%
@yak/solid (styled API, Solid 2) foldStatic: false2,436 req/s ±2%
@yak/react (styled API)1,609 req/s ±1%
@yak/react (styled API) foldStatic: false859 req/s ±1%
vanilla (hand-written ceiling)755 req/s ±0%
vanilla Solid (hand-written ceiling)603 req/s ±1%
cn (shadcn-ui)599 req/s ±3%
StyleX without CSS layers (:not() specificity hack)571 req/s ±1%
StyleX557 req/s ±3%
Bamboo551 req/s ±1%
styled-components519 req/s ±0%
cn (shadcn-ui) on Solid483 req/s ±1%
@yak/react (css prop) foldStatic: false468 req/s ±1%
@yak/react (css prop)450 req/s ±3%
StyleX on Solid427 req/s ±1%
Panda (style props)394 req/s ±1%
tailwind-merge332 req/s ±2%
Emotion290 req/s ±2%
Goober231 req/s ±2%
Panda (css fn)168 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.53 ms
@yak/solid (styled API, Solid 2)0.73 ms
@yak/react (styled API)1.05 ms
Bamboo1.47 ms
vanilla Solid (hand-written ceiling)1.63 ms
vanilla (hand-written ceiling)1.63 ms
@yak/react (styled API) foldStatic: false1.69 ms
cn (shadcn-ui)1.80 ms
StyleX without CSS layers (:not() specificity hack)1.89 ms
cn (shadcn-ui) on Solid1.90 ms
StyleX2.08 ms
@yak/react (css prop) foldStatic: false2.40 ms
Panda (style props)2.50 ms
StyleX on Solid2.60 ms
@yak/react (css prop)2.85 ms
tailwind-merge2.94 ms
styled-components3.28 ms
Emotion3.86 ms
Goober4.84 ms
Panda (css fn)6.71 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/react (styled API)54 ms ±5%
@yak/solid (styled API, Solid 2)69 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false70 ms ±5%
@yak/react (styled API) foldStatic: false72 ms ±3%
vanilla (hand-written ceiling)75 ms ±3%
Bamboo76 ms ±4%
StyleX without CSS layers (:not() specificity hack)84 ms ±3%
styled-components85 ms ±5%
StyleX86 ms ±4%
@yak/react (css prop) foldStatic: false86 ms ±3%
cn (shadcn-ui)86 ms ±5%
@yak/react (css prop)87 ms ±3%
vanilla Solid (hand-written ceiling)98 ms ±3%
Panda (style props)101 ms ±7%
tailwind-merge104 ms ±3%
cn (shadcn-ui) on Solid105 ms ±4%
Emotion105 ms ±5%
Goober112 ms ±4%
StyleX on Solid113 ms ±4%
Panda (css fn)127 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false7.21 ms active(19.33 ms span)
@yak/solid (styled API, Solid 2)7.31 ms active(20.04 ms span)
vanilla Solid (hand-written ceiling)8.80 ms active(21.70 ms span)
StyleX on Solid10.20 ms active(13.51 ms span)
@yak/react (styled API)11.52 ms active(25.25 ms span)
Bamboo12.20 ms active(22.09 ms span)
styled-components12.44 ms active(33.77 ms span)
vanilla (hand-written ceiling)12.51 ms active(27.36 ms span)
StyleX12.86 ms active(28.93 ms span)
StyleX without CSS layers (:not() specificity hack)12.96 ms active(29.70 ms span)
@yak/react (css prop)13.01 ms active(31.24 ms span)
cn (shadcn-ui) on Solid13.96 ms active(24.01 ms span)
@yak/react (styled API) foldStatic: false13.96 ms active(31.17 ms span)
@yak/react (css prop) foldStatic: false14.32 ms active(28.40 ms span)
Emotion14.88 ms active(26.04 ms span)
Goober15.19 ms active(38.39 ms span)
Panda (style props)15.95 ms active(26.79 ms span)
cn (shadcn-ui)16.27 ms active(46.00 ms span)
tailwind-merge20.58 ms active(27.05 ms span)
Panda (css fn)21.19 ms active(21.41 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)59 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false61 ms ±3%
cn (shadcn-ui) on Solid64 ms ±9%
@yak/react (styled API)64 ms ±4%
StyleX on Solid66 ms ±3%
vanilla Solid (hand-written ceiling)67 ms ±5%
cn (shadcn-ui)69 ms ±3%
@yak/react (styled API) foldStatic: false69 ms ±2%
vanilla (hand-written ceiling)71 ms ±5%
Bamboo71 ms ±3%
tailwind-merge74 ms ±3%
StyleX without CSS layers (:not() specificity hack)76 ms ±3%
StyleX76 ms ±2%
@yak/react (css prop) foldStatic: false78 ms ±6%
Panda (style props)78 ms ±8%
styled-components78 ms ±8%
@yak/react (css prop)79 ms ±4%
Emotion79 ms ±5%
Panda (css fn)92 ms ±4%
Goober95 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)1.63 ms active(33.39 ms span · 33.39 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false2.02 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
cn (shadcn-ui) on Solid2.04 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.06 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.26 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
StyleX on Solid2.33 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
StyleX2.41 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
@yak/react (styled API)2.43 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Bamboo2.46 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Panda (style props)2.48 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.61 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
tailwind-merge2.62 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
cn (shadcn-ui)2.74 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false2.82 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
@yak/react (css prop)3.01 ms active(33.40 ms span · 33.40 ms profiled median (n=5))
Panda (css fn)3.03 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
vanilla (hand-written ceiling)3.13 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
styled-components3.17 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
Goober3.31 ms active(33.23 ms span · 33.23 ms profiled median (n=5))
Emotion3.47 ms active(33.37 ms span · 33.37 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)35 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false49 ms ±3%
@yak/react (styled API)58 ms ±3%
vanilla Solid (hand-written ceiling)72 ms ±3%
cn (shadcn-ui) on Solid76 ms ±3%
@yak/react (styled API) foldStatic: false76 ms ±3%
Bamboo82 ms ±5%
vanilla (hand-written ceiling)86 ms ±3%
StyleX on Solid88 ms ±4%
StyleX without CSS layers (:not() specificity hack)90 ms ±3%
cn (shadcn-ui)91 ms ±2%
StyleX93 ms ±5%
@yak/react (css prop)93 ms ±3%
@yak/react (css prop) foldStatic: false96 ms ±4%
styled-components97 ms ±5%
Panda (style props)110 ms ±6%
Emotion112 ms ±4%
tailwind-merge114 ms ±5%
Goober119 ms ±3%
Panda (css fn)135 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)10.50 ms active(26.08 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false11.49 ms active(11.51 ms span)
vanilla Solid (hand-written ceiling)13.23 ms active(16.51 ms span)
StyleX on Solid13.71 ms active(16.42 ms span)
cn (shadcn-ui) on Solid15.15 ms active(25.73 ms span)
@yak/react (styled API)15.99 ms active(29.43 ms span)
@yak/react (css prop)17.40 ms active(29.65 ms span)
Bamboo18.10 ms active(42.45 ms span)
StyleX18.25 ms active(39.21 ms span)
vanilla (hand-written ceiling)18.37 ms active(34.23 ms span)
@yak/react (styled API) foldStatic: false19.46 ms active(44.85 ms span)
@yak/react (css prop) foldStatic: false20.77 ms active(32.17 ms span)
styled-components21.42 ms active(36.55 ms span)
cn (shadcn-ui)22.23 ms active(40.63 ms span)
StyleX without CSS layers (:not() specificity hack)23.32 ms active(46.10 ms span)
Emotion23.76 ms active(41.29 ms span)
Panda (css fn)23.77 ms active(41.06 ms span)
Panda (style props)24.21 ms active(36.32 ms span)
Goober24.56 ms active(37.74 ms span)
tailwind-merge31.21 ms active(52.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.

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.0 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui)Chrome5.4 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome4.8 ms1 recalc
Firefox1.0 ms
EmotionChrome5.1 ms1 recalc
Firefox1.0 ms
GooberChrome5.2 ms1 recalc
Firefox1.0 ms
@yak/react (styled API)Chrome4.5 ms1 recalc
Firefox1.0 ms
@yak/react (css prop)Chrome4.5 ms1 recalc
Firefox1.0 ms
@yak/react (css prop) foldStatic: falseChrome5.3 ms1 recalc
Firefox1.0 ms
@yak/react (styled API) foldStatic: falseChrome5.7 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome5.1 ms1 recalc
Firefox2.0 ms
styled-componentsChrome4.9 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.4 ms1 recalc
Firefox2.0 ms
StyleXChrome4.6 ms1 recalc
Firefox1.0 ms
StyleX on SolidChrome4.9 ms1 recalc
Firefox2.0 ms
tailwind-mergeChrome5.1 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome4.9 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome4.8 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome5.4 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome4.8 ms1 recalc
Firefox2.0 ms
Panda (style props)Chrome5.6 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,078 B(0.0 / 0.4 / 2.6 kB)
@yak/react (css prop) foldStatic: false3,407 B(0.4 / 0.2 / 2.7 kB)
@yak/react (css prop)3,432 B(0.5 / 0.2 / 2.7 kB)
@yak/react (styled API)4,131 B(1.1 / 0.2 / 2.7 kB)
@yak/react (styled API) foldStatic: false4,258 B(1.3 / 0.2 / 2.7 kB)
vanilla Solid (hand-written ceiling)5,673 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,961 B(5.9 / 0.3 / 5.5 kB)
@yak/solid (styled API, Solid 2) foldStatic: false12,187 B(6.5 / 0.2 / 5.2 kB)
@yak/solid (styled API, Solid 2)12,789 B(7.1 / 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,971 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 SolidEmotionGoober@yak/react (styled API)@yak/react (css prop)@yak/react (css prop) foldStatic: false@yak/react (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)104 msnone · warm 109 ms
Goober131 msruntime · warm 127 ms
vanilla (hand-written ceiling)133 msnone · warm 131 ms
cn (shadcn-ui) on Solid137 msutility · warm 135 ms
Emotion137 msruntime · warm 140 ms
styled-components142 msruntime · warm 141 ms
cn (shadcn-ui)145 msutility · warm 152 ms
tailwind-merge169 msutility · warm 159 ms
@yak/react (styled API) foldStatic: false209 msextracted · warm 212 ms
@yak/react (styled API)210 msextracted · warm 209 ms
@yak/solid (styled API, Solid 2) foldStatic: false214 msextracted · warm 225 ms
@yak/solid (styled API, Solid 2)222 msextracted · warm 225 ms
@yak/react (css prop)223 msextracted · warm 221 ms
@yak/react (css prop) foldStatic: false233 msextracted · warm 227 ms
Plumeria on Solid394 msatomic · warm 402 ms
Plumeria (classStyle prop)402 msatomic · warm 387 ms
Panda (recipe)421 msatomic · warm 424 ms
Panda (css fn)430 msatomic · warm 429 ms
Panda (style props)449 msatomic · warm 453 ms
StyleX without CSS layers (:not() specificity hack)495 msatomic · warm 500 ms
StyleX on Solid496 msatomic · warm 517 ms
StyleX499 msatomic · warm 487 ms
Bamboo1,380 msatomic · warm 1,372 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.