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)507,619 r/s ±19%(0.15 ms / 1k elems)
Plumeria on Solid466,653 r/s ±21%(0.16 ms / 1k elems)
cn (shadcn-ui) on Solid449,470 r/s ±4%(0.16 ms / 1k elems)
@yak/solid (styled API, Solid 2)337,062 r/s ±20%(0.22 ms / 1k elems)
StyleX on Solid228,305 r/s ±5%(0.32 ms / 1k elems)
vanilla (hand-written ceiling)148,495 r/s ±1%(0.50 ms / 1k elems)
Bamboo146,100 r/s ±2%(0.51 ms / 1k elems)
Plumeria (classStyle prop)133,767 r/s ±8%(0.55 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)131,418 r/s ±2%(0.56 ms / 1k elems)
@yak/react (css prop)131,282 r/s ±3%(0.56 ms / 1k elems)
StyleX129,868 r/s ±3%(0.57 ms / 1k elems)
cn (shadcn-ui)121,511 r/s ±1%(0.61 ms / 1k elems)
@yak/react (styled API)120,638 r/s ±1%(0.61 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false114,193 r/s ±3%(0.65 ms / 1k elems)
@yak/react (css prop) foldStatic: false112,500 r/s ±3%(0.66 ms / 1k elems)
tailwind-merge94,426 r/s ±1%(0.78 ms / 1k elems)
@yak/react (styled API) foldStatic: false64,581 r/s ±1%(1.15 ms / 1k elems)
Panda (css fn)63,502 r/s ±1%(1.17 ms / 1k elems)
Goober38,181 r/s ±1%(1.94 ms / 1k elems)
styled-components37,090 r/s ±1%(2.00 ms / 1k elems)
Panda (style props)25,543 r/s ±1%(2.90 ms / 1k elems)
Emotion21,264 r/s ±1%(3.48 ms / 1k elems)

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

vanilla Solid (hand-written ceiling)736 req/s ±1%
@yak/solid (styled API, Solid 2)556 req/s ±1%
Plumeria on Solid512 req/s ±1%
cn (shadcn-ui) on Solid374 req/s ±0%
StyleX on Solid344 req/s ±0%
vanilla (hand-written ceiling)232 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false221 req/s ±1%
@yak/react (css prop)218 req/s ±1%
@yak/react (styled API)204 req/s ±2%
@yak/react (css prop) foldStatic: false191 req/s ±0%
Plumeria (classStyle prop)169 req/s ±0%
StyleX169 req/s ±1%
StyleX without CSS layers (:not() specificity hack)166 req/s ±1%
Bamboo153 req/s ±1%
@yak/react (styled API) foldStatic: false137 req/s ±2%
cn (shadcn-ui)119 req/s ±0%
tailwind-merge105 req/s ±0%
Goober85 req/s ±0%
Panda (css fn)83 req/s ±1%
styled-components78 req/s ±2%
Emotion58 req/s ±0%
Panda (style props)46 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.38 ms
Plumeria on Solid1.40 ms
cn (shadcn-ui) on Solid1.65 ms
@yak/solid (styled API, Solid 2)1.95 ms
StyleX on Solid2.51 ms
vanilla (hand-written ceiling)3.13 ms
Bamboo3.36 ms
Plumeria (classStyle prop)3.51 ms
@yak/react (css prop)4.08 ms
StyleX4.14 ms
cn (shadcn-ui)4.22 ms
@yak/react (styled API)4.26 ms
StyleX without CSS layers (:not() specificity hack)4.66 ms
@yak/solid (styled API, Solid 2) foldStatic: false4.96 ms
@yak/react (css prop) foldStatic: false5.21 ms
tailwind-merge5.66 ms
@yak/react (styled API) foldStatic: false6.96 ms
Panda (css fn)7.63 ms
Goober12.13 ms
styled-components12.96 ms
Emotion19.45 ms
Panda (style props)19.96 ms

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

vanilla (hand-written ceiling)74 ms ±5%
Bamboo74 ms ±3%
Plumeria (classStyle prop)74 ms ±4%
@yak/react (css prop)80 ms ±1%
StyleX83 ms ±5%
StyleX without CSS layers (:not() specificity hack)84 ms ±4%
@yak/react (styled API)89 ms ±5%
cn (shadcn-ui)90 ms ±3%
@yak/react (css prop) foldStatic: false91 ms ±4%
Panda (css fn)96 ms ±2%
tailwind-merge100 ms ±3%
@yak/react (styled API) foldStatic: false110 ms ±4%
Plumeria on Solid124 ms ±4%
vanilla Solid (hand-written ceiling)126 ms ±3%
styled-components127 ms ±5%
cn (shadcn-ui) on Solid136 ms ±2%
Goober155 ms ±4%
@yak/solid (styled API, Solid 2)165 ms ±4%
StyleX on Solid171 ms ±3%
Panda (style props)184 ms ±4%
Emotion188 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false294 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Bamboo15.21 ms active(30.46 ms span)
Plumeria (classStyle prop)15.63 ms active(40.41 ms span)
StyleX without CSS layers (:not() specificity hack)16.50 ms active(32.69 ms span)
Plumeria on Solid16.54 ms active(25.38 ms span)
@yak/react (css prop)16.99 ms active(41.04 ms span)
vanilla (hand-written ceiling)17.25 ms active(43.75 ms span)
StyleX17.96 ms active(32.78 ms span)
@yak/react (styled API)18.38 ms active(43.16 ms span)
vanilla Solid (hand-written ceiling)18.46 ms active(18.49 ms span)
Panda (css fn)19.54 ms active(37.24 ms span)
cn (shadcn-ui)22.04 ms active(40.19 ms span)
cn (shadcn-ui) on Solid22.16 ms active(32.42 ms span)
styled-components22.66 ms active(28.57 ms span)
@yak/react (css prop) foldStatic: false23.19 ms active(45.96 ms span)
@yak/solid (styled API, Solid 2)24.49 ms active(24.51 ms span)
tailwind-merge24.66 ms active(56.24 ms span)
StyleX on Solid24.97 ms active(25.00 ms span)
Goober29.17 ms active(34.78 ms span)
@yak/react (styled API) foldStatic: false30.38 ms active(44.13 ms span)
Emotion31.02 ms active(33.36 ms span)
Panda (style props)31.89 ms active(57.40 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false34.30 ms active(42.34 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)239 ms ±10%
Plumeria on Solid251 ms ±7%
@yak/react (styled API)260 ms ±4%
@yak/solid (styled API, Solid 2)267 ms ±4%
Bamboo267 ms ±5%
vanilla (hand-written ceiling)269 ms ±7%
Panda (css fn)270 ms ±4%
Plumeria (classStyle prop)273 ms ±6%
StyleX on Solid273 ms ±10%
@yak/react (css prop)275 ms ±5%
StyleX276 ms ±8%
@yak/react (css prop) foldStatic: false279 ms ±7%
@yak/solid (styled API, Solid 2) foldStatic: false282 ms ±6%
@yak/react (styled API) foldStatic: false290 ms ±4%
Goober306 ms ±7%
cn (shadcn-ui) on Solid311 ms ±6%
styled-components311 ms ±5%
StyleX without CSS layers (:not() specificity hack)322 ms ±6%
Panda (style props)331 ms ±3%
tailwind-merge333 ms ±8%
cn (shadcn-ui)348 ms ±10%
Emotion349 ms ±9%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false9.73 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)10.10 ms active(33.15 ms span · 33.15 ms profiled median (n=5))
styled-components10.66 ms active(33.87 ms span · 33.87 ms profiled median (n=5))
Bamboo10.99 ms active(32.50 ms span · 32.50 ms profiled median (n=5))
Plumeria on Solid11.15 ms active(33.01 ms span · 33.01 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)11.21 ms active(33.10 ms span · 33.10 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false11.42 ms active(34.01 ms span · 34.01 ms profiled median (n=5))
Plumeria (classStyle prop)11.78 ms active(33.93 ms span · 33.93 ms profiled median (n=5))
StyleX on Solid11.84 ms active(33.09 ms span · 33.09 ms profiled median (n=5))
@yak/react (css prop)11.88 ms active(32.36 ms span · 32.36 ms profiled median (n=5))
Panda (css fn)11.88 ms active(32.19 ms span · 32.19 ms profiled median (n=5))
StyleX11.99 ms active(32.32 ms span · 32.32 ms profiled median (n=5))
vanilla (hand-written ceiling)12.05 ms active(32.61 ms span · 32.61 ms profiled median (n=5))
Goober12.21 ms active(32.49 ms span · 32.49 ms profiled median (n=5))
@yak/react (styled API)12.24 ms active(32.46 ms span · 32.46 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)13.14 ms active(32.21 ms span · 32.21 ms profiled median (n=5))
Emotion13.53 ms active(34.01 ms span · 34.01 ms profiled median (n=5))
cn (shadcn-ui) on Solid13.65 ms active(33.04 ms span · 33.04 ms profiled median (n=5))
Panda (style props)14.48 ms active(32.33 ms span · 32.33 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false16.37 ms active(32.88 ms span · 32.88 ms profiled median (n=5))
cn (shadcn-ui)17.73 ms active(30.89 ms span · 30.89 ms profiled median (n=5))
tailwind-merge19.55 ms active(30.39 ms span · 30.39 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 (hand-written ceiling)96 ms ±3%
Bamboo97 ms ±3%
Plumeria (classStyle prop)97 ms ±1%
vanilla Solid (hand-written ceiling)99 ms ±7%
@yak/react (css prop)102 ms ±2%
Plumeria on Solid106 ms ±4%
StyleX107 ms ±4%
StyleX without CSS layers (:not() specificity hack)107 ms ±7%
@yak/react (styled API)109 ms ±3%
cn (shadcn-ui)113 ms ±3%
@yak/react (css prop) foldStatic: false117 ms ±5%
cn (shadcn-ui) on Solid122 ms ±5%
Panda (css fn)123 ms ±5%
tailwind-merge126 ms ±4%
@yak/solid (styled API, Solid 2)137 ms ±3%
@yak/react (styled API) foldStatic: false144 ms ±5%
StyleX on Solid150 ms ±3%
styled-components169 ms ±5%
Goober190 ms ±3%
Panda (style props)217 ms ±2%
Emotion220 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false223 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)29.28 ms active(29.32 ms span)
@yak/solid (styled API, Solid 2)34.05 ms active(34.09 ms span)
@yak/react (css prop)36.35 ms active(46.45 ms span)
cn (shadcn-ui) on Solid36.42 ms active(36.47 ms span)
vanilla (hand-written ceiling)36.64 ms active(46.54 ms span)
@yak/react (styled API)37.70 ms active(47.70 ms span)
Plumeria on Solid37.78 ms active(37.81 ms span)
StyleX without CSS layers (:not() specificity hack)40.19 ms active(47.45 ms span)
StyleX40.72 ms active(56.23 ms span)
@yak/react (styled API) foldStatic: false41.61 ms active(51.60 ms span)
Panda (css fn)42.37 ms active(49.31 ms span)
cn (shadcn-ui)42.73 ms active(48.22 ms span)
StyleX on Solid43.25 ms active(43.29 ms span)
@yak/react (css prop) foldStatic: false44.26 ms active(54.03 ms span)
Plumeria (classStyle prop)44.30 ms active(57.78 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false44.48 ms active(44.53 ms span)
Bamboo49.99 ms active(64.69 ms span)
tailwind-merge50.85 ms active(57.00 ms span)
Panda (style props)52.23 ms active(57.70 ms span)
styled-components54.17 ms active(68.43 ms span)
Goober54.25 ms active(60.55 ms span)
Emotion55.77 ms active(61.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.

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

style recalclayoutpaint (Chrome)
@yak/solid (styled API, Solid 2)Chrome15.9 ms1 recalc
Firefox12.0 ms
styled-componentsChrome16.2 ms1 recalc
Firefox10.0 ms
@yak/react (styled API)Chrome16.8 ms1 recalc
Firefox9.0 ms
vanilla Solid (hand-written ceiling)Chrome15.8 ms1 recalc
Firefox11.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome18.4 ms1 recalc
Firefox11.0 ms
vanilla (hand-written ceiling)Chrome16.7 ms1 recalc
Firefox11.0 ms
@yak/react (css prop)Chrome16.4 ms1 recalc
Firefox11.0 ms
StyleXChrome19.2 ms1 recalc
Firefox15.0 ms
@yak/react (css prop) foldStatic: falseChrome16.3 ms1 recalc
Firefox10.0 ms
Panda (style props)Chrome17.3 ms1 recalc
Firefox12.0 ms
@yak/react (styled API) foldStatic: falseChrome16.9 ms1 recalc
Firefox12.1 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome16.1 ms1 recalc
Firefox10.0 ms
Plumeria on SolidChrome21.8 ms1 recalc
Firefox11.0 ms
BambooChrome20.2 ms1 recalc
Firefox11.0 ms
Panda (css fn)Chrome16.6 ms1 recalc
Firefox13.0 ms
Plumeria (classStyle prop)Chrome19.0 ms1 recalc
Firefox12.0 ms
StyleX on SolidChrome16.9 ms1 recalc
Firefox15.0 ms
EmotionChrome16.6 ms1 recalc
Firefox14.0 ms
GooberChrome16.1 ms1 recalc
Firefox14.0 ms
cn (shadcn-ui)Chrome18.0 ms1 recalc
Firefox16.1 ms
cn (shadcn-ui) on SolidChrome18.1 ms1 recalc
Firefox15.6 ms
tailwind-mergeChrome18.0 ms1 recalc
Firefox19.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: false7,647 B(0.5 / 0.9 / 6.1 kB)
@yak/react (css prop)7,664 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,347 B(1.2 / 0.9 / 6.1 kB)
@yak/react (styled API) foldStatic: false8,475 B(1.3 / 0.9 / 6.1 kB)
vanilla Solid (hand-written ceiling)9,916 B(0.0 / 1.5 / 8.2 kB)
Plumeria (classStyle prop)11,287 B(1.1 / 1.4 / 8.5 kB)
Goober11,550 B(3.9 / 1.2 / 6.2 kB)
Plumeria on Solid13,009 B(0.4 / 1.4 / 10.9 kB)
StyleX14,112 B(3.5 / 1.5 / 8.8 kB)
StyleX without CSS layers (:not() specificity hack)14,182 B(3.5 / 1.6 / 8.8 kB)
Bamboo16,550 B(2.8 / 1.8 / 11.5 kB)
StyleX on Solid18,639 B(5.9 / 1.5 / 10.8 kB)
@yak/solid (styled API, Solid 2)18,950 B(7.1 / 0.9 / 10.5 kB)
Emotion20,671 B(12.9 / 1.2 / 6.1 kB)
styled-components21,329 B(13.1 / 1.4 / 6.3 kB)
Panda (css fn)22,136 B(4.8 / 1.5 / 15.3 kB)
Panda (style props)26,667 B(9.2 / 1.5 / 15.3 kB)
tailwind-merge27,084 B(10.4 / 2.0 / 14.0 kB)
cn (shadcn-ui)29,622 B(12.9 / 2.0 / 14.0 kB)
@yak/solid (styled API, Solid 2) foldStatic: false30,697 B(6.5 / 0.9 / 22.6 kB)
cn (shadcn-ui) on Solid31,875 B(12.4 / 2.0 / 16.7 kB)

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

0.049971461951005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on 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.

Plumeria on Solid419,502 r/s ±25%(0.18 ms / 1k elems)
vanilla Solid (hand-written ceiling)375,375 r/s ±20%(0.20 ms / 1k elems)
cn (shadcn-ui) on Solid329,973 r/s ±7%(0.22 ms / 1k elems)
Bamboo148,409 r/s ±1%(0.50 ms / 1k elems)
vanilla (hand-written ceiling)147,832 r/s ±1%(0.50 ms / 1k elems)
@yak/react (css prop)140,393 r/s ±3%(0.53 ms / 1k elems)
Plumeria (classStyle prop)133,893 r/s ±5%(0.55 ms / 1k elems)
cn (shadcn-ui)119,606 r/s ±3%(0.62 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false112,743 r/s ±4%(0.66 ms / 1k elems)
@yak/react (css prop) foldStatic: false112,212 r/s ±10%(0.66 ms / 1k elems)
@yak/solid (styled API, Solid 2)111,667 r/s ±6%(0.66 ms / 1k elems)
StyleX on Solid103,945 r/s ±5%(0.71 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)100,112 r/s ±3%(0.74 ms / 1k elems)
StyleX99,156 r/s ±3%(0.75 ms / 1k elems)
tailwind-merge93,502 r/s ±1%(0.79 ms / 1k elems)
@yak/react (styled API)65,415 r/s ±1%(1.13 ms / 1k elems)
@yak/react (styled API) foldStatic: false62,969 r/s ±1%(1.18 ms / 1k elems)
Panda (css fn)62,271 r/s ±1%(1.19 ms / 1k elems)
styled-components36,667 r/s ±1%(2.02 ms / 1k elems)
Goober35,297 r/s ±1%(2.10 ms / 1k elems)
Panda (style props)25,525 r/s ±2%(2.90 ms / 1k elems)
Emotion21,105 r/s ±1%(3.51 ms / 1k elems)

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

vanilla Solid (hand-written ceiling)594 req/s ±0%
Plumeria on Solid516 req/s ±1%
cn (shadcn-ui) on Solid330 req/s ±1%
vanilla (hand-written ceiling)232 req/s ±1%
@yak/react (css prop)220 req/s ±1%
@yak/solid (styled API, Solid 2)216 req/s ±0%
@yak/solid (styled API, Solid 2) foldStatic: false216 req/s ±0%
@yak/react (css prop) foldStatic: false192 req/s ±0%
StyleX on Solid181 req/s ±1%
Plumeria (classStyle prop)170 req/s ±0%
Bamboo153 req/s ±0%
StyleX without CSS layers (:not() specificity hack)145 req/s ±0%
StyleX144 req/s ±0%
@yak/react (styled API)134 req/s ±0%
@yak/react (styled API) foldStatic: false134 req/s ±1%
cn (shadcn-ui)120 req/s ±0%
tailwind-merge106 req/s ±0%
Panda (css fn)82 req/s ±1%
styled-components79 req/s ±1%
Goober77 req/s ±0%
Emotion58 req/s ±0%
Panda (style props)46 req/s ±0%

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

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

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
Plumeria on Solid1.51 ms
vanilla Solid (hand-written ceiling)1.54 ms
cn (shadcn-ui) on Solid1.97 ms
vanilla (hand-written ceiling)3.25 ms
@yak/react (css prop)3.72 ms
Plumeria (classStyle prop)3.81 ms
cn (shadcn-ui)4.28 ms
@yak/solid (styled API, Solid 2) foldStatic: false4.68 ms
@yak/react (css prop) foldStatic: false4.79 ms
Bamboo4.81 ms
@yak/solid (styled API, Solid 2)5.04 ms
StyleX5.07 ms
StyleX on Solid5.15 ms
StyleX without CSS layers (:not() specificity hack)5.63 ms
@yak/react (styled API) foldStatic: false6.74 ms
@yak/react (styled API)7.08 ms
tailwind-merge7.26 ms
Panda (css fn)8.47 ms
styled-components13.26 ms
Goober13.53 ms
Panda (style props)19.90 ms
Emotion21.89 ms

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

Plumeria (classStyle prop)73 ms ±2%
vanilla (hand-written ceiling)75 ms ±2%
Bamboo77 ms ±5%
@yak/react (css prop)81 ms ±2%
cn (shadcn-ui)89 ms ±2%
StyleX91 ms ±4%
@yak/react (css prop) foldStatic: false93 ms ±3%
StyleX without CSS layers (:not() specificity hack)94 ms ±5%
Panda (css fn)96 ms ±5%
tailwind-merge103 ms ±4%
@yak/react (styled API)111 ms ±3%
@yak/react (styled API) foldStatic: false112 ms ±4%
Plumeria on Solid126 ms ±4%
vanilla Solid (hand-written ceiling)128 ms ±3%
styled-components131 ms ±3%
cn (shadcn-ui) on Solid144 ms ±5%
Goober162 ms ±3%
Panda (style props)185 ms ±4%
Emotion192 ms ±4%
StyleX on Solid210 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false294 ms ±2%
@yak/solid (styled API, Solid 2)294 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)15.84 ms active(33.31 ms span)
Plumeria (classStyle prop)17.08 ms active(47.93 ms span)
vanilla Solid (hand-written ceiling)18.06 ms active(27.71 ms span)
@yak/react (css prop)18.23 ms active(41.32 ms span)
StyleX without CSS layers (:not() specificity hack)18.67 ms active(34.47 ms span)
Plumeria on Solid18.81 ms active(21.31 ms span)
Bamboo19.09 ms active(38.47 ms span)
Panda (css fn)19.70 ms active(24.95 ms span)
@yak/react (styled API) foldStatic: false20.16 ms active(40.38 ms span)
@yak/react (css prop) foldStatic: false20.33 ms active(31.86 ms span)
cn (shadcn-ui)20.69 ms active(33.14 ms span)
@yak/react (styled API)21.23 ms active(42.56 ms span)
StyleX21.93 ms active(31.34 ms span)
styled-components24.16 ms active(40.95 ms span)
cn (shadcn-ui) on Solid26.02 ms active(26.05 ms span)
tailwind-merge26.48 ms active(58.50 ms span)
Goober27.23 ms active(56.68 ms span)
Panda (style props)34.49 ms active(34.56 ms span)
Emotion35.69 ms active(67.29 ms span)
StyleX on Solid35.84 ms active(35.90 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false39.89 ms active(55.42 ms span)
@yak/solid (styled API, Solid 2)42.52 ms active(55.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)249 ms ±6%
Panda (css fn)259 ms ±4%
vanilla (hand-written ceiling)265 ms ±8%
Plumeria (classStyle prop)266 ms ±3%
@yak/react (css prop)267 ms ±8%
Plumeria on Solid269 ms ±4%
Bamboo272 ms ±4%
StyleX on Solid296 ms ±11%
StyleX without CSS layers (:not() specificity hack)297 ms ±6%
@yak/react (css prop) foldStatic: false298 ms ±6%
@yak/react (styled API)302 ms ±6%
StyleX304 ms ±7%
@yak/react (styled API) foldStatic: false306 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false308 ms ±5%
@yak/solid (styled API, Solid 2)310 ms ±6%
styled-components321 ms ±6%
cn (shadcn-ui) on Solid329 ms ±11%
Goober331 ms ±6%
Panda (style props)333 ms ±3%
Emotion349 ms ±5%
cn (shadcn-ui)372 ms ±9%
tailwind-merge374 ms ±11%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)9.62 ms active(33.20 ms span · 33.20 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)10.03 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false10.56 ms active(33.13 ms span · 33.13 ms profiled median (n=5))
@yak/react (css prop)11.08 ms active(32.36 ms span · 32.36 ms profiled median (n=5))
Bamboo11.31 ms active(32.39 ms span · 32.39 ms profiled median (n=5))
Plumeria on Solid11.47 ms active(33.21 ms span · 33.21 ms profiled median (n=5))
vanilla (hand-written ceiling)11.88 ms active(32.48 ms span · 32.48 ms profiled median (n=5))
@yak/react (styled API)12.01 ms active(34.01 ms span · 34.01 ms profiled median (n=5))
Emotion12.09 ms active(33.67 ms span · 33.67 ms profiled median (n=5))
cn (shadcn-ui) on Solid12.35 ms active(33.13 ms span · 33.13 ms profiled median (n=5))
Plumeria (classStyle prop)12.41 ms active(32.52 ms span · 32.52 ms profiled median (n=5))
StyleX on Solid12.50 ms active(33.20 ms span · 33.20 ms profiled median (n=5))
StyleX12.65 ms active(32.42 ms span · 32.42 ms profiled median (n=5))
Panda (css fn)12.73 ms active(32.35 ms span · 32.35 ms profiled median (n=5))
styled-components12.98 ms active(32.41 ms span · 32.41 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false13.09 ms active(32.33 ms span · 32.33 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false13.30 ms active(32.60 ms span · 32.60 ms profiled median (n=5))
Goober13.50 ms active(32.24 ms span · 32.24 ms profiled median (n=5))
Panda (style props)14.45 ms active(32.41 ms span · 32.41 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)14.77 ms active(34.37 ms span · 34.37 ms profiled median (n=5))
cn (shadcn-ui)17.75 ms active(31.01 ms span · 31.01 ms profiled median (n=5))
tailwind-merge19.68 ms active(30.89 ms span · 30.89 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 (hand-written ceiling)95 ms ±4%
Plumeria (classStyle prop)97 ms ±4%
Bamboo100 ms ±5%
@yak/react (css prop)103 ms ±3%
Plumeria on Solid103 ms ±4%
vanilla Solid (hand-written ceiling)109 ms ±5%
cn (shadcn-ui)113 ms ±3%
@yak/react (css prop) foldStatic: false117 ms ±3%
StyleX119 ms ±2%
Panda (css fn)123 ms ±5%
StyleX without CSS layers (:not() specificity hack)124 ms ±3%
tailwind-merge125 ms ±3%
cn (shadcn-ui) on Solid135 ms ±5%
@yak/react (styled API)137 ms ±4%
@yak/react (styled API) foldStatic: false140 ms ±4%
styled-components166 ms ±2%
Goober190 ms ±2%
StyleX on Solid192 ms ±3%
Panda (style props)221 ms ±5%
@yak/solid (styled API, Solid 2)222 ms ±4%
Emotion227 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false229 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)32.62 ms active(35.64 ms span)
Bamboo37.03 ms active(49.86 ms span)
Plumeria (classStyle prop)37.67 ms active(47.72 ms span)
Plumeria on Solid40.32 ms active(40.38 ms span)
StyleX41.63 ms active(55.49 ms span)
vanilla (hand-written ceiling)41.66 ms active(49.73 ms span)
@yak/react (css prop)42.15 ms active(47.81 ms span)
cn (shadcn-ui) on Solid43.13 ms active(43.17 ms span)
@yak/react (styled API)44.14 ms active(51.83 ms span)
StyleX on Solid44.51 ms active(44.55 ms span)
StyleX without CSS layers (:not() specificity hack)45.08 ms active(50.90 ms span)
cn (shadcn-ui)45.17 ms active(50.49 ms span)
@yak/react (css prop) foldStatic: false45.46 ms active(57.45 ms span)
tailwind-merge46.30 ms active(52.65 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false46.80 ms active(46.87 ms span)
Panda (css fn)46.98 ms active(53.89 ms span)
@yak/react (styled API) foldStatic: false47.65 ms active(50.27 ms span)
styled-components49.34 ms active(61.13 ms span)
@yak/solid (styled API, Solid 2)51.58 ms active(51.65 ms span)
Goober55.70 ms active(61.68 ms span)
Panda (style props)58.28 ms active(67.01 ms span)
Emotion63.46 ms active(77.09 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: falseChrome17.1 ms1 recalc
Firefox8.0 ms
EmotionChrome16.8 ms1 recalc
Firefox12.0 ms
@yak/react (css prop) foldStatic: falseChrome17.2 ms1 recalc
Firefox12.0 ms
vanilla Solid (hand-written ceiling)Chrome17.6 ms1 recalc
Firefox11.0 ms
Panda (style props)Chrome17.7 ms1 recalc
Firefox12.0 ms
Panda (css fn)Chrome19.6 ms1 recalc
Firefox12.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome17.9 ms1 recalc
Firefox11.0 ms
@yak/react (css prop)Chrome16.5 ms1 recalc
Firefox13.0 ms
StyleXChrome18.5 ms1 recalc
Firefox12.0 ms
StyleX on SolidChrome16.8 ms1 recalc
Firefox14.0 ms
@yak/react (styled API)Chrome16.3 ms1 recalc
Firefox14.0 ms
styled-componentsChrome15.5 ms1 recalc
Firefox11.0 ms
@yak/solid (styled API, Solid 2)Chrome16.5 ms1 recalc
Firefox10.0 ms
vanilla (hand-written ceiling)Chrome16.7 ms1 recalc
Firefox11.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome16.7 ms1 recalc
Firefox11.0 ms
Plumeria (classStyle prop)Chrome18.0 ms1 recalc
Firefox10.6 ms
BambooChrome17.3 ms1 recalc
Firefox14.0 ms
GooberChrome18.9 ms1 recalc
Firefox14.0 ms
Plumeria on SolidChrome21.3 ms1 recalc
Firefox12.0 ms
cn (shadcn-ui) on SolidChrome18.8 ms1 recalc
Firefox13.0 ms
tailwind-mergeChrome18.7 ms1 recalc
Firefox14.0 ms
cn (shadcn-ui)Chrome19.4 ms1 recalc
Firefox16.6 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,635 B(0.5 / 0.9 / 6.1 kB)
@yak/react (css prop)7,656 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,482 B(1.2 / 0.9 / 6.2 kB)
@yak/react (styled API) foldStatic: false8,605 B(1.3 / 0.9 / 6.2 kB)
vanilla Solid (hand-written ceiling)9,916 B(0.0 / 1.5 / 8.2 kB)
Plumeria (classStyle prop)11,287 B(1.1 / 1.4 / 8.5 kB)
Goober11,401 B(3.9 / 1.1 / 6.1 kB)
Plumeria on Solid13,011 B(0.4 / 1.4 / 10.9 kB)
StyleX without CSS layers (:not() specificity hack)14,065 B(3.5 / 1.5 / 8.7 kB)
StyleX14,109 B(3.5 / 1.5 / 8.8 kB)
Bamboo16,546 B(2.8 / 1.8 / 11.5 kB)
StyleX on Solid18,628 B(5.9 / 1.6 / 10.7 kB)
Emotion20,665 B(12.9 / 1.2 / 6.1 kB)
styled-components21,387 B(13.1 / 1.4 / 6.4 kB)
Panda (css fn)22,134 B(4.8 / 1.5 / 15.3 kB)
Panda (style props)26,665 B(9.2 / 1.5 / 15.3 kB)
tailwind-merge27,082 B(10.4 / 2.0 / 14.0 kB)
cn (shadcn-ui)29,620 B(12.9 / 2.0 / 14.0 kB)
@yak/solid (styled API, Solid 2) foldStatic: false30,713 B(6.5 / 0.9 / 22.6 kB)
@yak/solid (styled API, Solid 2)31,354 B(7.1 / 0.9 / 22.6 kB)
cn (shadcn-ui) on Solid31,874 B(12.4 / 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.049971461941005001,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,521,218 r/s ±9%(0.26 ms / 1k elems)
cn (shadcn-ui) on Solid1,435,944 r/s ±9%(0.28 ms / 1k elems)
Plumeria on Solid1,405,857 r/s ±8%(0.28 ms / 1k elems)
@yak/solid (styled API, Solid 2)1,372,940 r/s ±9%(0.29 ms / 1k elems)
StyleX on Solid821,816 r/s ±5%(0.49 ms / 1k elems)
Bamboo649,987 r/s ±2%(0.62 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)629,569 r/s ±5%(0.64 ms / 1k elems)
StyleX627,958 r/s ±4%(0.64 ms / 1k elems)
@yak/react (styled API)614,467 r/s ±2%(0.65 ms / 1k elems)
@yak/react (css prop)570,838 r/s ±3%(0.70 ms / 1k elems)
Plumeria (classStyle prop)519,958 r/s ±1%(0.77 ms / 1k elems)
vanilla (hand-written ceiling)515,356 r/s ±1%(0.78 ms / 1k elems)
@yak/react (css prop) foldStatic: false500,339 r/s ±5%(0.80 ms / 1k elems)
cn (shadcn-ui)486,735 r/s ±9%(0.82 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false468,833 r/s ±4%(0.85 ms / 1k elems)
tailwind-merge315,001 r/s ±5%(1.27 ms / 1k elems)
Panda (css fn)262,267 r/s ±1%(1.53 ms / 1k elems)
@yak/react (styled API) foldStatic: false261,940 r/s ±1%(1.53 ms / 1k elems)
styled-components158,121 r/s ±2%(2.53 ms / 1k elems)
Goober153,327 r/s ±1%(2.61 ms / 1k elems)
Emotion102,999 r/s ±1%(3.88 ms / 1k elems)
Panda (style props)90,367 r/s ±2%(4.43 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)773 req/s ±1%
vanilla Solid (hand-written ceiling)752 req/s ±0%
Plumeria on Solid539 req/s ±0%
@yak/react (css prop)449 req/s ±0%
@yak/react (styled API)446 req/s ±4%
cn (shadcn-ui) on Solid430 req/s ±1%
StyleX on Solid424 req/s ±0%
@yak/react (css prop) foldStatic: false389 req/s ±1%
vanilla (hand-written ceiling)382 req/s ±1%
StyleX368 req/s ±0%
@yak/solid (styled API, Solid 2) foldStatic: false366 req/s ±1%
StyleX without CSS layers (:not() specificity hack)362 req/s ±1%
Bamboo347 req/s ±1%
Plumeria (classStyle prop)326 req/s ±0%
cn (shadcn-ui)268 req/s ±0%
@yak/react (styled API) foldStatic: false247 req/s ±0%
tailwind-merge207 req/s ±2%
Panda (css fn)159 req/s ±0%
styled-components147 req/s ±1%
Goober145 req/s ±1%
Emotion115 req/s ±1%
Panda (style props)76 req/s ±0%

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

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

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
@yak/solid (styled API, Solid 2)0.80 ms
cn (shadcn-ui) on Solid0.91 ms
Plumeria on Solid1.00 ms
vanilla Solid (hand-written ceiling)1.33 ms
Bamboo1.81 ms
StyleX1.90 ms
StyleX without CSS layers (:not() specificity hack)2.05 ms
@yak/react (css prop)2.08 ms
StyleX on Solid2.10 ms
@yak/react (styled API)2.20 ms
vanilla (hand-written ceiling)2.48 ms
@yak/react (css prop) foldStatic: false2.52 ms
Plumeria (classStyle prop)2.53 ms
@yak/solid (styled API, Solid 2) foldStatic: false2.58 ms
cn (shadcn-ui)2.66 ms
tailwind-merge3.76 ms
@yak/react (styled API) foldStatic: false4.03 ms
Panda (css fn)4.91 ms
Goober7.56 ms
styled-components7.70 ms
Emotion10.13 ms
Panda (style props)12.53 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 ±3%
StyleX69 ms ±5%
@yak/react (styled API)69 ms ±5%
Plumeria (classStyle prop)70 ms ±2%
Bamboo70 ms ±4%
Panda (css fn)71 ms ±5%
@yak/react (css prop)72 ms ±3%
StyleX without CSS layers (:not() specificity hack)73 ms ±4%
@yak/react (css prop) foldStatic: false78 ms ±3%
cn (shadcn-ui)85 ms ±4%
@yak/react (styled API) foldStatic: false90 ms ±3%
tailwind-merge96 ms ±3%
styled-components104 ms ±5%
Goober110 ms ±5%
Plumeria on Solid118 ms ±3%
@yak/solid (styled API, Solid 2)118 ms ±4%
vanilla Solid (hand-written ceiling)123 ms ±4%
StyleX on Solid132 ms ±6%
cn (shadcn-ui) on Solid134 ms ±6%
Panda (style props)134 ms ±4%
Emotion139 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false205 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)10.55 ms active(25.07 ms span)
@yak/react (styled API)11.51 ms active(21.42 ms span)
@yak/solid (styled API, Solid 2)11.54 ms active(16.59 ms span)
Plumeria on Solid11.78 ms active(25.49 ms span)
Plumeria (classStyle prop)11.81 ms active(29.83 ms span)
StyleX12.12 ms active(25.79 ms span)
Panda (css fn)12.13 ms active(26.00 ms span)
StyleX without CSS layers (:not() specificity hack)12.40 ms active(38.26 ms span)
Bamboo12.54 ms active(38.84 ms span)
StyleX on Solid12.61 ms active(18.17 ms span)
@yak/react (css prop) foldStatic: false12.64 ms active(33.30 ms span)
@yak/react (css prop)13.46 ms active(33.61 ms span)
@yak/react (styled API) foldStatic: false14.08 ms active(27.34 ms span)
vanilla (hand-written ceiling)14.47 ms active(36.10 ms span)
Goober15.69 ms active(33.48 ms span)
Emotion16.05 ms active(33.27 ms span)
cn (shadcn-ui) on Solid16.35 ms active(32.41 ms span)
styled-components17.46 ms active(33.72 ms span)
cn (shadcn-ui)20.05 ms active(26.18 ms span)
Panda (style props)20.12 ms active(47.59 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false20.75 ms active(35.25 ms span)
tailwind-merge21.12 ms active(32.20 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)231 ms ±8%
@yak/react (styled API)234 ms ±3%
vanilla Solid (hand-written ceiling)239 ms ±9%
@yak/solid (styled API, Solid 2)246 ms ±7%
@yak/react (css prop)247 ms ±8%
@yak/react (css prop) foldStatic: false249 ms ±14%
styled-components256 ms ±5%
cn (shadcn-ui) on Solid261 ms ±5%
@yak/react (styled API) foldStatic: false263 ms ±4%
StyleX263 ms ±4%
Goober263 ms ±6%
tailwind-merge264 ms ±8%
Bamboo265 ms ±5%
Panda (css fn)267 ms ±7%
Plumeria (classStyle prop)267 ms ±6%
Emotion276 ms ±5%
StyleX on Solid278 ms ±5%
cn (shadcn-ui)279 ms ±5%
Plumeria on Solid285 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false289 ms ±6%
StyleX without CSS layers (:not() specificity hack)294 ms ±7%
Panda (style props)296 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla (hand-written ceiling)3.86 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)3.96 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
cn (shadcn-ui) on Solid4.00 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false4.09 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
styled-components4.22 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Panda (css fn)4.35 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false4.60 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)4.61 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
cn (shadcn-ui)4.72 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Bamboo4.75 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/react (css prop)4.77 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX4.84 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Goober4.89 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Plumeria (classStyle prop)5.20 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)5.39 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Panda (style props)5.47 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false5.61 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (styled API)5.68 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
tailwind-merge5.92 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Plumeria on Solid6.36 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Emotion6.67 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
StyleX on Solid8.07 ms active(33.34 ms span · 33.34 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)83 ms ±2%
Bamboo85 ms ±4%
Plumeria (classStyle prop)86 ms ±4%
@yak/react (styled API)86 ms ±3%
vanilla (hand-written ceiling)86 ms ±4%
StyleX without CSS layers (:not() specificity hack)87 ms ±5%
StyleX88 ms ±4%
@yak/solid (styled API, Solid 2)91 ms ±5%
Panda (css fn)92 ms ±4%
vanilla Solid (hand-written ceiling)93 ms ±4%
Plumeria on Solid93 ms ±4%
@yak/react (css prop) foldStatic: false96 ms ±5%
cn (shadcn-ui)101 ms ±3%
@yak/react (styled API) foldStatic: false110 ms ±8%
tailwind-merge110 ms ±6%
cn (shadcn-ui) on Solid111 ms ±4%
StyleX on Solid113 ms ±5%
styled-components133 ms ±3%
Goober135 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false152 ms ±5%
Panda (style props)160 ms ±2%
Emotion164 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)15.04 ms active(30.71 ms span)
StyleX on Solid16.47 ms active(22.99 ms span)
Plumeria on Solid16.47 ms active(30.76 ms span)
@yak/solid (styled API, Solid 2)17.65 ms active(23.23 ms span)
Panda (css fn)19.26 ms active(39.96 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false19.69 ms active(24.48 ms span)
cn (shadcn-ui) on Solid20.08 ms active(31.08 ms span)
@yak/react (styled API)20.20 ms active(35.20 ms span)
vanilla (hand-written ceiling)20.30 ms active(39.97 ms span)
StyleX without CSS layers (:not() specificity hack)20.54 ms active(40.66 ms span)
Plumeria (classStyle prop)20.58 ms active(29.27 ms span)
Bamboo20.73 ms active(45.35 ms span)
@yak/react (styled API) foldStatic: false20.96 ms active(39.66 ms span)
@yak/react (css prop) foldStatic: false21.47 ms active(41.14 ms span)
@yak/react (css prop)23.15 ms active(43.67 ms span)
StyleX24.93 ms active(39.90 ms span)
cn (shadcn-ui)25.65 ms active(45.19 ms span)
Goober27.32 ms active(40.49 ms span)
Panda (style props)27.38 ms active(37.71 ms span)
styled-components29.07 ms active(39.05 ms span)
tailwind-merge29.98 ms active(48.34 ms span)
Emotion31.30 ms active(42.65 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: falseChrome5.8 ms1 recalc
Firefox2.0 ms
styled-componentsChrome6.2 ms1 recalc
Firefox2.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome6.5 ms1 recalc
Firefox2.0 ms
StyleXChrome7.1 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome5.8 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome5.7 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2)Chrome6.2 ms1 recalc
Firefox2.0 ms
Plumeria on SolidChrome6.8 ms1 recalc
Firefox3.0 ms
GooberChrome5.7 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome5.9 ms1 recalc
Firefox3.0 ms
vanilla (hand-written ceiling)Chrome5.7 ms1 recalc
Firefox2.0 ms
@yak/react (styled API)Chrome6.0 ms1 recalc
Firefox3.0 ms
Panda (style props)Chrome5.8 ms1 recalc
Firefox3.0 ms
BambooChrome6.2 ms1 recalc
Firefox3.0 ms
@yak/react (css prop) foldStatic: falseChrome6.4 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui) on SolidChrome5.8 ms1 recalc
Firefox3.0 ms
EmotionChrome5.9 ms1 recalc
Firefox3.0 ms
tailwind-mergeChrome5.8 ms1 recalc
Firefox3.0 ms
Plumeria (classStyle prop)Chrome6.8 ms1 recalc
Firefox2.0 ms
@yak/react (css prop)Chrome6.7 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome5.7 ms1 recalc
Firefox3.0 ms
cn (shadcn-ui)Chrome6.0 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,439 B(0.5 / 0.5 / 3.4 kB)
vanilla (hand-written ceiling)4,444 B(0.0 / 0.9 / 3.5 kB)
@yak/react (css prop)4,463 B(0.5 / 0.5 / 3.4 kB)
@yak/react (styled API)5,020 B(1.2 / 0.5 / 3.2 kB)
@yak/react (styled API) foldStatic: false5,151 B(1.3 / 0.5 / 3.2 kB)
Plumeria (classStyle prop)7,559 B(1.1 / 0.8 / 5.4 kB)
Goober8,037 B(3.9 / 0.6 / 3.4 kB)
StyleX10,073 B(3.5 / 0.9 / 5.4 kB)
StyleX without CSS layers (:not() specificity hack)10,128 B(3.5 / 1.0 / 5.4 kB)
vanilla Solid (hand-written ceiling)10,730 B(0.0 / 0.9 / 9.6 kB)
Bamboo11,948 B(2.8 / 1.2 / 7.7 kB)
Plumeria on Solid13,306 B(0.4 / 0.8 / 11.7 kB)
@yak/solid (styled API, Solid 2) foldStatic: false16,736 B(6.5 / 0.5 / 9.3 kB)
Emotion17,156 B(12.9 / 0.6 / 3.2 kB)
Panda (css fn)17,427 B(4.8 / 1.0 / 11.3 kB)
styled-components17,439 B(13.1 / 0.6 / 3.3 kB)
@yak/solid (styled API, Solid 2)17,550 B(7.1 / 0.5 / 9.5 kB)
StyleX on Solid18,963 B(5.9 / 0.9 / 11.7 kB)
tailwind-merge21,733 B(10.4 / 1.3 / 9.5 kB)
Panda (style props)21,953 B(9.2 / 1.0 / 11.3 kB)
cn (shadcn-ui)24,271 B(12.9 / 1.3 / 9.5 kB)
cn (shadcn-ui) on Solid29,181 B(12.4 / 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.0112234451005001,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)5,002,752 r/s ±44%(0.20 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false3,634,474 r/s ±7%(0.28 ms / 1k elems)
cn (shadcn-ui) on Solid2,248,919 r/s ±8%(0.44 ms / 1k elems)
@yak/react (styled API)2,031,533 r/s ±2%(0.49 ms / 1k elems)
vanilla Solid (hand-written ceiling)1,938,939 r/s ±5%(0.52 ms / 1k elems)
Bamboo1,664,467 r/s ±3%(0.60 ms / 1k elems)
@yak/react (css prop)1,584,638 r/s ±27%(0.63 ms / 1k elems)
cn (shadcn-ui)1,548,177 r/s ±3%(0.65 ms / 1k elems)
Plumeria on Solid1,457,724 r/s ±4%(0.69 ms / 1k elems)
vanilla (hand-written ceiling)1,394,762 r/s ±3%(0.72 ms / 1k elems)
@yak/react (css prop) foldStatic: false1,277,472 r/s ±3%(0.78 ms / 1k elems)
StyleX on Solid1,142,094 r/s ±3%(0.88 ms / 1k elems)
@yak/react (styled API) foldStatic: false1,063,497 r/s ±1%(0.94 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)989,552 r/s ±2%(1.01 ms / 1k elems)
StyleX981,054 r/s ±3%(1.02 ms / 1k elems)
Plumeria (classStyle prop)944,625 r/s ±3%(1.06 ms / 1k elems)
Panda (css fn)899,141 r/s ±2%(1.11 ms / 1k elems)
tailwind-merge878,796 r/s ±3%(1.14 ms / 1k elems)
Panda (style props)656,332 r/s ±1%(1.52 ms / 1k elems)
Goober447,934 r/s ±1%(2.23 ms / 1k elems)
styled-components398,054 r/s ±2%(2.51 ms / 1k elems)
Emotion234,397 r/s ±1%(4.27 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,579 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false2,661 req/s ±1%
@yak/react (styled API)1,755 req/s ±0%
vanilla Solid (hand-written ceiling)1,552 req/s ±1%
@yak/react (css prop)1,505 req/s ±0%
Plumeria on Solid1,428 req/s ±2%
vanilla (hand-written ceiling)1,312 req/s ±0%
@yak/react (css prop) foldStatic: false1,274 req/s ±1%
@yak/react (styled API) foldStatic: false1,147 req/s ±1%
cn (shadcn-ui) on Solid1,123 req/s ±2%
Plumeria (classStyle prop)1,064 req/s ±0%
cn (shadcn-ui)990 req/s ±0%
Bamboo944 req/s ±1%
StyleX without CSS layers (:not() specificity hack)775 req/s ±1%
StyleX769 req/s ±0%
StyleX on Solid764 req/s ±0%
tailwind-merge665 req/s ±1%
Panda (css fn)642 req/s ±1%
Panda (style props)561 req/s ±1%
Goober487 req/s ±1%
styled-components404 req/s ±0%
Emotion288 req/s ±2%

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

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

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
@yak/solid (styled API, Solid 2) foldStatic: false0.58 ms
vanilla Solid (hand-written ceiling)0.62 ms
@yak/solid (styled API, Solid 2)0.77 ms
Plumeria on Solid0.88 ms
@yak/react (styled API)1.05 ms
cn (shadcn-ui)1.05 ms
StyleX on Solid1.10 ms
@yak/react (styled API) foldStatic: false1.10 ms
cn (shadcn-ui) on Solid1.11 ms
StyleX1.19 ms
StyleX without CSS layers (:not() specificity hack)1.28 ms
@yak/react (css prop)1.30 ms
Bamboo1.33 ms
@yak/react (css prop) foldStatic: false1.35 ms
Panda (css fn)1.46 ms
Plumeria (classStyle prop)1.47 ms
Panda (style props)1.51 ms
vanilla (hand-written ceiling)1.65 ms
tailwind-merge1.75 ms
Goober2.25 ms
styled-components2.79 ms
Emotion3.94 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)48 ms ±2%
Plumeria (classStyle prop)50 ms ±4%
Bamboo50 ms ±3%
@yak/react (css prop)51 ms ±3%
vanilla (hand-written ceiling)53 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false55 ms ±3%
@yak/solid (styled API, Solid 2)56 ms ±5%
@yak/react (css prop) foldStatic: false56 ms ±3%
@yak/react (styled API) foldStatic: false58 ms ±3%
Plumeria on Solid58 ms ±5%
StyleX59 ms ±4%
StyleX without CSS layers (:not() specificity hack)59 ms ±5%
cn (shadcn-ui)59 ms ±3%
vanilla Solid (hand-written ceiling)61 ms ±3%
Panda (css fn)63 ms ±3%
styled-components65 ms ±3%
Goober68 ms ±2%
cn (shadcn-ui) on Solid69 ms ±5%
StyleX on Solid73 ms ±3%
Panda (style props)74 ms ±2%
tailwind-merge77 ms ±4%
Emotion94 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false6.63 ms active(31.66 ms span)
@yak/solid (styled API, Solid 2)6.89 ms active(31.84 ms span)
Plumeria on Solid7.19 ms active(16.53 ms span)
vanilla Solid (hand-written ceiling)7.54 ms active(26.70 ms span)
StyleX on Solid8.70 ms active(20.78 ms span)
cn (shadcn-ui) on Solid9.53 ms active(19.25 ms span)
@yak/react (css prop)10.29 ms active(24.91 ms span)
@yak/react (styled API)10.48 ms active(34.52 ms span)
Plumeria (classStyle prop)10.59 ms active(23.72 ms span)
vanilla (hand-written ceiling)11.01 ms active(31.46 ms span)
Panda (css fn)11.02 ms active(28.98 ms span)
Bamboo11.06 ms active(27.77 ms span)
StyleX11.26 ms active(35.10 ms span)
@yak/react (css prop) foldStatic: false11.71 ms active(28.21 ms span)
StyleX without CSS layers (:not() specificity hack)11.84 ms active(32.70 ms span)
cn (shadcn-ui)12.75 ms active(25.39 ms span)
styled-components13.55 ms active(32.59 ms span)
@yak/react (styled API) foldStatic: false13.70 ms active(31.45 ms span)
Panda (style props)13.98 ms active(24.35 ms span)
Goober14.96 ms active(35.28 ms span)
Emotion20.30 ms active(29.11 ms span)
tailwind-merge20.95 ms active(30.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.

cn (shadcn-ui) on Solid55 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false55 ms ±6%
Plumeria on Solid56 ms ±5%
vanilla Solid (hand-written ceiling)56 ms ±3%
@yak/react (styled API) foldStatic: false56 ms ±5%
Bamboo56 ms ±6%
vanilla (hand-written ceiling)57 ms ±3%
@yak/react (css prop) foldStatic: false57 ms ±2%
StyleX on Solid58 ms ±3%
@yak/react (styled API)58 ms ±5%
@yak/solid (styled API, Solid 2)58 ms ±6%
StyleX without CSS layers (:not() specificity hack)58 ms ±3%
cn (shadcn-ui)59 ms ±4%
@yak/react (css prop)59 ms ±5%
StyleX59 ms ±3%
tailwind-merge59 ms ±3%
Panda (style props)60 ms ±3%
Plumeria (classStyle prop)60 ms ±4%
Panda (css fn)61 ms ±2%
styled-components64 ms ±3%
Goober66 ms ±8%
Emotion77 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)1.38 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/react (css prop)1.41 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
cn (shadcn-ui) on Solid1.50 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
StyleX on Solid1.63 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)1.64 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
tailwind-merge1.69 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)1.70 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
cn (shadcn-ui)1.74 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
@yak/react (styled API)1.78 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
Bamboo1.79 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Plumeria on Solid1.81 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Emotion1.82 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX1.88 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
vanilla (hand-written ceiling)1.97 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.09 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
styled-components2.11 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false2.28 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
Panda (css fn)2.30 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Plumeria (classStyle prop)2.42 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Goober2.47 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Panda (style props)2.56 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false2.69 ms active(33.33 ms span · 33.33 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)32 ms ±6%
Plumeria on Solid36 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false38 ms ±3%
vanilla Solid (hand-written ceiling)41 ms ±5%
cn (shadcn-ui) on Solid50 ms ±4%
@yak/react (styled API)51 ms ±4%
Plumeria (classStyle prop)53 ms ±3%
@yak/react (css prop)53 ms ±4%
StyleX on Solid54 ms ±3%
Bamboo55 ms ±4%
vanilla (hand-written ceiling)57 ms ±4%
@yak/react (css prop) foldStatic: false59 ms ±2%
@yak/react (styled API) foldStatic: false59 ms ±3%
cn (shadcn-ui)63 ms ±5%
StyleX without CSS layers (:not() specificity hack)64 ms ±6%
StyleX66 ms ±3%
Panda (css fn)69 ms ±3%
Goober72 ms ±2%
Panda (style props)77 ms ±3%
styled-components80 ms ±5%
tailwind-merge80 ms ±4%
Emotion97 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)10.17 ms active(21.34 ms span)
Plumeria on Solid10.75 ms active(36.08 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false10.95 ms active(28.20 ms span)
vanilla Solid (hand-written ceiling)12.22 ms active(20.26 ms span)
cn (shadcn-ui) on Solid12.80 ms active(12.82 ms span)
StyleX on Solid13.85 ms active(24.78 ms span)
StyleX without CSS layers (:not() specificity hack)16.84 ms active(30.18 ms span)
Bamboo17.00 ms active(28.92 ms span)
@yak/react (css prop) foldStatic: false17.19 ms active(32.08 ms span)
@yak/react (css prop)17.59 ms active(36.79 ms span)
@yak/react (styled API)17.87 ms active(44.46 ms span)
Panda (css fn)17.96 ms active(41.99 ms span)
@yak/react (styled API) foldStatic: false18.15 ms active(41.29 ms span)
vanilla (hand-written ceiling)18.49 ms active(40.80 ms span)
Plumeria (classStyle prop)19.35 ms active(43.46 ms span)
cn (shadcn-ui)19.49 ms active(42.37 ms span)
styled-components19.56 ms active(39.69 ms span)
Panda (style props)19.70 ms active(40.01 ms span)
StyleX20.42 ms active(31.91 ms span)
Goober20.85 ms active(39.20 ms span)
tailwind-merge24.63 ms active(42.75 ms span)
Emotion26.65 ms active(37.68 ms span)

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

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

style recalclayoutpaint (Chrome)
BambooChrome4.7 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui)Chrome4.9 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome4.6 ms1 recalc
Firefox1.0 ms
GooberChrome5.2 ms1 recalc
Firefox1.0 ms
@yak/react (styled API)Chrome4.4 ms1 recalc
Firefox1.0 ms
@yak/react (css prop)Chrome4.9 ms1 recalc
Firefox2.0 ms
@yak/react (css prop) foldStatic: falseChrome4.4 ms1 recalc
Firefox2.0 ms
@yak/react (styled API) foldStatic: falseChrome5.1 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome4.8 ms1 recalc
Firefox2.0 ms
Panda (style props)Chrome4.9 ms1 recalc
Firefox1.0 ms
styled-componentsChrome4.4 ms1 recalc
Firefox2.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome4.5 ms1 recalc
Firefox1.0 ms
StyleXChrome5.3 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome4.6 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome4.7 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome4.5 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome4.4 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.2 ms1 recalc
Firefox1.0 ms
Plumeria (classStyle prop)Chrome5.4 ms1 recalc
Firefox1.0 ms
EmotionChrome5.1 ms1 recalc
Firefox2.0 ms
Plumeria on SolidChrome4.7 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome4.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 (css prop) foldStatic: false3,223 B(0.5 / 0.2 / 2.5 kB)
@yak/react (css prop)3,241 B(0.5 / 0.2 / 2.5 kB)
@yak/react (styled API)3,940 B(1.2 / 0.2 / 2.5 kB)
@yak/react (styled API) foldStatic: false4,073 B(1.3 / 0.2 / 2.5 kB)
Plumeria (classStyle prop)4,556 B(1.1 / 0.3 / 3.1 kB)
vanilla Solid (hand-written ceiling)5,784 B(0.0 / 0.5 / 5.2 kB)
Plumeria on Solid6,637 B(0.4 / 0.3 / 5.8 kB)
Bamboo6,716 B(2.8 / 0.4 / 3.3 kB)
Goober6,805 B(3.9 / 0.2 / 2.6 kB)
StyleX without CSS layers (:not() specificity hack)7,013 B(3.5 / 0.3 / 3.0 kB)
StyleX7,014 B(3.5 / 0.3 / 3.0 kB)
Panda (css fn)8,702 B(4.8 / 0.3 / 3.4 kB)
@yak/solid (styled API, Solid 2) foldStatic: false11,912 B(6.5 / 0.2 / 4.9 kB)
StyleX on Solid12,247 B(5.9 / 0.3 / 5.8 kB)
@yak/solid (styled API, Solid 2)12,694 B(7.1 / 0.2 / 5.1 kB)
Panda (style props)13,229 B(9.2 / 0.3 / 3.4 kB)
tailwind-merge14,645 B(10.4 / 0.7 / 3.2 kB)
Emotion15,991 B(12.9 / 0.3 / 2.4 kB)
styled-components16,332 B(13.1 / 0.3 / 2.6 kB)
cn (shadcn-ui)17,183 B(12.9 / 0.7 / 3.2 kB)
cn (shadcn-ui) on Solid19,365 B(12.4 / 0.7 / 5.8 kB)

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

0.04.69.114181005001,0002,0004,000
Bamboocn (shadcn-ui)cn (shadcn-ui) on 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)3,509,659 r/s ±7%(0.28 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false3,482,112 r/s ±9%(0.29 ms / 1k elems)
cn (shadcn-ui)1,099,811 r/s ±2%(0.91 ms / 1k elems)
vanilla (hand-written ceiling)1,074,392 r/s ±4%(0.93 ms / 1k elems)
cn (shadcn-ui) on Solid1,047,598 r/s ±8%(0.95 ms / 1k elems)
@yak/react (styled API)905,502 r/s ±5%(1.10 ms / 1k elems)
@yak/react (styled API) foldStatic: false869,066 r/s ±2%(1.15 ms / 1k elems)
vanilla Solid (hand-written ceiling)864,627 r/s ±25%(1.16 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)812,785 r/s ±4%(1.23 ms / 1k elems)
StyleX798,491 r/s ±5%(1.25 ms / 1k elems)
StyleX on Solid755,865 r/s ±5%(1.32 ms / 1k elems)
tailwind-merge624,338 r/s ±2%(1.60 ms / 1k elems)
Bamboo604,450 r/s ±53%(1.65 ms / 1k elems)
styled-components549,144 r/s ±1%(1.82 ms / 1k elems)
Panda (style props)530,230 r/s ±2%(1.89 ms / 1k elems)
Panda (css fn)476,207 r/s ±4%(2.10 ms / 1k elems)
Goober351,488 r/s ±1%(2.85 ms / 1k elems)
Emotion262,130 r/s ±1%(3.81 ms / 1k elems)

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

@yak/solid (styled API, Solid 2) foldStatic: false2,652 req/s ±0%
@yak/solid (styled API, Solid 2)2,636 req/s ±1%
vanilla (hand-written ceiling)1,085 req/s ±0%
@yak/react (styled API) foldStatic: false961 req/s ±1%
@yak/react (styled API)961 req/s ±1%
cn (shadcn-ui)808 req/s ±1%
vanilla Solid (hand-written ceiling)785 req/s ±0%
cn (shadcn-ui) on Solid732 req/s ±0%
Bamboo729 req/s ±1%
StyleX without CSS layers (:not() specificity hack)671 req/s ±1%
StyleX667 req/s ±1%
styled-components584 req/s ±1%
StyleX on Solid579 req/s ±0%
tailwind-merge527 req/s ±3%
Panda (style props)492 req/s ±0%
Goober409 req/s ±1%
Panda (css fn)408 req/s ±1%
Emotion316 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.51 ms
@yak/solid (styled API, Solid 2)0.61 ms
vanilla (hand-written ceiling)1.20 ms
Bamboo1.26 ms
StyleX1.39 ms
vanilla Solid (hand-written ceiling)1.46 ms
cn (shadcn-ui)1.48 ms
cn (shadcn-ui) on Solid1.56 ms
@yak/react (styled API) foldStatic: false1.59 ms
StyleX without CSS layers (:not() specificity hack)1.64 ms
StyleX on Solid1.76 ms
Panda (style props)1.81 ms
tailwind-merge1.82 ms
@yak/react (styled API)1.94 ms
styled-components2.49 ms
Goober2.72 ms
Panda (css fn)3.07 ms
Emotion3.67 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)54 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false56 ms ±3%
@yak/react (styled API) foldStatic: false56 ms ±5%
@yak/react (styled API)57 ms ±1%
vanilla (hand-written ceiling)57 ms ±4%
Bamboo59 ms ±5%
StyleX without CSS layers (:not() specificity hack)64 ms ±4%
StyleX65 ms ±5%
styled-components66 ms ±4%
cn (shadcn-ui)68 ms ±3%
Panda (css fn)75 ms ±2%
Panda (style props)77 ms ±3%
Goober78 ms ±2%
cn (shadcn-ui) on Solid80 ms ±5%
tailwind-merge81 ms ±2%
StyleX on Solid82 ms ±5%
Emotion88 ms ±4%
vanilla Solid (hand-written ceiling)90 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false6.95 ms active(24.50 ms span)
@yak/solid (styled API, Solid 2)7.55 ms active(35.74 ms span)
StyleX on Solid9.62 ms active(20.59 ms span)
vanilla Solid (hand-written ceiling)9.75 ms active(17.28 ms span)
@yak/react (styled API) foldStatic: false11.29 ms active(32.56 ms span)
vanilla (hand-written ceiling)11.32 ms active(41.99 ms span)
@yak/react (styled API)11.41 ms active(31.34 ms span)
Bamboo11.44 ms active(27.54 ms span)
StyleX11.59 ms active(35.07 ms span)
cn (shadcn-ui) on Solid11.82 ms active(28.66 ms span)
StyleX without CSS layers (:not() specificity hack)12.22 ms active(38.35 ms span)
Goober12.85 ms active(24.94 ms span)
Panda (css fn)13.26 ms active(26.56 ms span)
Emotion14.07 ms active(22.87 ms span)
styled-components14.32 ms active(38.16 ms span)
cn (shadcn-ui)16.92 ms active(27.15 ms span)
Panda (style props)17.53 ms active(33.71 ms span)
tailwind-merge21.36 ms active(33.65 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)54 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false55 ms ±3%
cn (shadcn-ui) on Solid55 ms ±2%
vanilla Solid (hand-written ceiling)56 ms ±4%
StyleX on Solid57 ms ±3%
@yak/react (styled API) foldStatic: false59 ms ±3%
cn (shadcn-ui)59 ms ±4%
@yak/react (styled API)60 ms ±4%
vanilla (hand-written ceiling)61 ms ±5%
Bamboo61 ms ±8%
Panda (style props)61 ms ±6%
StyleX62 ms ±4%
styled-components63 ms ±3%
tailwind-merge64 ms ±3%
Panda (css fn)66 ms ±2%
StyleX without CSS layers (:not() specificity hack)66 ms ±5%
Goober73 ms ±2%
Emotion73 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false1.46 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
StyleX on Solid1.58 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)1.63 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Panda (css fn)1.75 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)1.77 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
cn (shadcn-ui)1.82 ms active(33.42 ms span · 33.42 ms profiled median (n=5))
cn (shadcn-ui) on Solid1.87 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/react (styled API)1.90 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
tailwind-merge2.07 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Bamboo2.21 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
styled-components2.25 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.39 ms active(33.39 ms span · 33.39 ms profiled median (n=5))
vanilla (hand-written ceiling)2.47 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
StyleX2.48 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
Goober2.49 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false2.53 ms active(33.39 ms span · 33.39 ms profiled median (n=5))
Emotion2.56 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
Panda (style props)3.11 ms active(33.36 ms span · 33.36 ms profiled median (n=5))

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

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

@yak/solid (styled API, Solid 2) foldStatic: false38 ms ±3%
@yak/solid (styled API, Solid 2)39 ms ±6%
cn (shadcn-ui) on Solid56 ms ±3%
vanilla Solid (hand-written ceiling)60 ms ±4%
vanilla (hand-written ceiling)60 ms ±3%
StyleX on Solid61 ms ±4%
@yak/react (styled API) foldStatic: false61 ms ±4%
@yak/react (styled API)61 ms ±5%
Bamboo62 ms ±3%
StyleX65 ms ±2%
StyleX without CSS layers (:not() specificity hack)66 ms ±4%
cn (shadcn-ui)67 ms ±3%
styled-components76 ms ±5%
Panda (css fn)82 ms ±6%
Panda (style props)83 ms ±4%
Goober84 ms ±3%
tailwind-merge85 ms ±4%
Emotion94 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)10.69 ms active(27.02 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false11.40 ms active(14.41 ms span)
vanilla Solid (hand-written ceiling)12.37 ms active(28.91 ms span)
cn (shadcn-ui) on Solid13.61 ms active(17.55 ms span)
StyleX on Solid14.48 ms active(25.21 ms span)
vanilla (hand-written ceiling)17.01 ms active(30.73 ms span)
@yak/react (styled API)17.13 ms active(30.13 ms span)
@yak/react (styled API) foldStatic: false17.14 ms active(38.36 ms span)
StyleX17.64 ms active(40.25 ms span)
StyleX without CSS layers (:not() specificity hack)17.69 ms active(41.44 ms span)
Bamboo18.16 ms active(33.34 ms span)
Goober18.44 ms active(34.77 ms span)
styled-components20.11 ms active(32.27 ms span)
cn (shadcn-ui)20.27 ms active(34.97 ms span)
Panda (css fn)22.68 ms active(33.04 ms span)
Panda (style props)23.80 ms active(46.51 ms span)
Emotion25.34 ms active(37.65 ms span)
tailwind-merge26.84 ms active(51.50 ms span)

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

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

style recalclayoutpaint (Chrome)
cn (shadcn-ui)Chrome4.9 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome4.4 ms1 recalc
Firefox2.0 ms
EmotionChrome4.6 ms1 recalc
Firefox2.0 ms
@yak/react (styled API)Chrome4.8 ms1 recalc
Firefox2.0 ms
@yak/react (styled API) foldStatic: falseChrome4.7 ms1 recalc
Firefox2.0 ms
Panda (css fn)Chrome5.3 ms1 recalc
Firefox2.0 ms
Panda (style props)Chrome4.9 ms1 recalc
Firefox2.0 ms
styled-componentsChrome4.6 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome4.8 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome5.1 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome4.8 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
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.3 ms1 recalc
Firefox1.0 ms
StyleX on SolidChrome5.0 ms1 recalc
Firefox2.0 ms
BambooChrome4.9 ms1 recalc
Firefox2.0 ms
GooberChrome4.7 ms1 recalc
Firefox2.0 ms
StyleXChrome4.8 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,124 B(1.2 / 0.2 / 2.6 kB)
@yak/react (styled API) foldStatic: false4,268 B(1.3 / 0.2 / 2.6 kB)
vanilla Solid (hand-written ceiling)5,954 B(0.0 / 0.5 / 5.4 kB)
Goober6,860 B(3.9 / 0.2 / 2.6 kB)
StyleX without CSS layers (:not() specificity hack)7,013 B(3.5 / 0.3 / 3.0 kB)
StyleX7,014 B(3.5 / 0.3 / 3.0 kB)
Bamboo7,203 B(2.8 / 0.5 / 3.7 kB)
Panda (css fn)8,702 B(4.8 / 0.3 / 3.4 kB)
StyleX on Solid12,247 B(5.9 / 0.3 / 5.8 kB)
@yak/solid (styled API, Solid 2) foldStatic: false12,257 B(6.5 / 0.2 / 5.2 kB)
@yak/solid (styled API, Solid 2)12,900 B(7.1 / 0.2 / 5.2 kB)
Panda (style props)13,229 B(9.2 / 0.3 / 3.4 kB)
tailwind-merge14,645 B(10.4 / 0.7 / 3.2 kB)
Emotion15,998 B(12.9 / 0.3 / 2.4 kB)
styled-components16,562 B(13.1 / 0.3 / 2.7 kB)
cn (shadcn-ui)17,183 B(12.9 / 0.7 / 3.2 kB)
cn (shadcn-ui) on Solid19,365 B(12.4 / 0.7 / 5.8 kB)

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

0.04.08.112161005001,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)151,980 r/s ±17%(0.82 ms / 1k elems)
cn (shadcn-ui) on Solid143,039 r/s ±6%(0.87 ms / 1k elems)
Plumeria on Solid126,881 r/s ±12%(0.99 ms / 1k elems)
vanilla (hand-written ceiling)94,955 r/s ±3%(1.32 ms / 1k elems)
@yak/react (css prop)88,395 r/s ±2%(1.41 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)84,622 r/s ±5%(1.48 ms / 1k elems)
StyleX83,820 r/s ±5%(1.49 ms / 1k elems)
StyleX on Solid82,329 r/s ±6%(1.52 ms / 1k elems)
Bamboo81,549 r/s ±2%(1.53 ms / 1k elems)
Plumeria (classStyle prop)79,004 r/s ±2%(1.58 ms / 1k elems)
@yak/react (css prop) foldStatic: false71,136 r/s ±5%(1.76 ms / 1k elems)
@yak/solid (styled API, Solid 2)69,864 r/s ±4%(1.79 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false55,984 r/s ±4%(2.23 ms / 1k elems)
cn (shadcn-ui)55,171 r/s ±2%(2.27 ms / 1k elems)
@yak/react (styled API)51,944 r/s ±1%(2.41 ms / 1k elems)
@yak/react (styled API) foldStatic: false44,018 r/s ±2%(2.84 ms / 1k elems)
tailwind-merge38,733 r/s ±1%(3.23 ms / 1k elems)
Goober27,516 r/s ±1%(4.54 ms / 1k elems)
Panda (css fn)27,044 r/s ±1%(4.62 ms / 1k elems)
styled-components23,243 r/s ±1%(5.38 ms / 1k elems)
Emotion15,513 r/s ±1%(8.06 ms / 1k elems)
Panda (style props)13,871 r/s ±3%(9.01 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)728 req/s ±0%
vanilla (hand-written ceiling)501 req/s ±0%
Plumeria on Solid480 req/s ±1%
@yak/react (css prop)441 req/s ±0%
cn (shadcn-ui) on Solid393 req/s ±0%
@yak/solid (styled API, Solid 2)372 req/s ±0%
StyleX on Solid366 req/s ±1%
@yak/react (css prop) foldStatic: false364 req/s ±0%
StyleX357 req/s ±1%
StyleX without CSS layers (:not() specificity hack)356 req/s ±1%
Plumeria (classStyle prop)335 req/s ±0%
Bamboo314 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false313 req/s ±0%
@yak/react (styled API)304 req/s ±0%
@yak/react (styled API) foldStatic: false275 req/s ±1%
cn (shadcn-ui)221 req/s ±1%
tailwind-merge176 req/s ±0%
Goober172 req/s ±0%
styled-components139 req/s ±0%
Panda (css fn)131 req/s ±1%
Emotion115 req/s ±0%
Panda (style props)79 req/s ±0%

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

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

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
vanilla Solid (hand-written ceiling)1.37 ms
Plumeria on Solid1.79 ms
StyleX2.02 ms
Bamboo2.13 ms
cn (shadcn-ui) on Solid2.21 ms
Plumeria (classStyle prop)2.27 ms
StyleX without CSS layers (:not() specificity hack)2.36 ms
vanilla (hand-written ceiling)2.40 ms
StyleX on Solid2.41 ms
@yak/react (css prop)2.48 ms
@yak/react (css prop) foldStatic: false2.81 ms
@yak/solid (styled API, Solid 2)2.94 ms
@yak/react (styled API)3.21 ms
@yak/solid (styled API, Solid 2) foldStatic: false3.42 ms
@yak/react (styled API) foldStatic: false3.47 ms
cn (shadcn-ui)3.53 ms
tailwind-merge4.67 ms
Goober6.16 ms
Panda (css fn)7.08 ms
styled-components8.72 ms
Emotion10.12 ms
Panda (style props)13.38 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)55 ms ±2%
StyleX57 ms ±5%
@yak/react (css prop)57 ms ±4%
Plumeria (classStyle prop)57 ms ±3%
StyleX without CSS layers (:not() specificity hack)58 ms ±2%
Bamboo59 ms ±4%
@yak/react (css prop) foldStatic: false69 ms ±5%
cn (shadcn-ui)70 ms ±3%
@yak/react (styled API)73 ms ±5%
@yak/react (styled API) foldStatic: false75 ms ±3%
Panda (css fn)80 ms ±5%
tailwind-merge82 ms ±3%
vanilla Solid (hand-written ceiling)90 ms ±3%
Plumeria on Solid92 ms ±3%
styled-components95 ms ±3%
Goober95 ms ±6%
cn (shadcn-ui) on Solid103 ms ±3%
StyleX on Solid121 ms ±2%
Emotion128 ms ±2%
Panda (style props)136 ms ±4%
@yak/solid (styled API, Solid 2)164 ms ±5%
@yak/solid (styled API, Solid 2) foldStatic: false210 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria (classStyle prop)15.64 ms active(34.51 ms span)
@yak/react (css prop)15.71 ms active(46.99 ms span)
StyleX15.78 ms active(45.98 ms span)
vanilla (hand-written ceiling)16.07 ms active(46.52 ms span)
Bamboo16.69 ms active(41.26 ms span)
StyleX without CSS layers (:not() specificity hack)16.93 ms active(41.01 ms span)
@yak/react (css prop) foldStatic: false19.27 ms active(47.62 ms span)
Plumeria on Solid21.29 ms active(21.34 ms span)
@yak/react (styled API) foldStatic: false21.30 ms active(38.51 ms span)
Panda (css fn)22.27 ms active(52.62 ms span)
@yak/react (styled API)22.53 ms active(42.68 ms span)
cn (shadcn-ui)23.43 ms active(35.08 ms span)
vanilla Solid (hand-written ceiling)23.49 ms active(27.50 ms span)
cn (shadcn-ui) on Solid23.57 ms active(36.22 ms span)
tailwind-merge27.58 ms active(33.94 ms span)
Goober27.79 ms active(31.45 ms span)
StyleX on Solid29.30 ms active(33.18 ms span)
styled-components31.52 ms active(35.69 ms span)
Panda (style props)32.60 ms active(55.34 ms span)
@yak/solid (styled API, Solid 2)38.03 ms active(53.43 ms span)
Emotion38.38 ms active(67.77 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false42.52 ms active(49.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.

StyleX141 ms ±5%
Plumeria (classStyle prop)177 ms ±8%
StyleX on Solid178 ms ±2%
StyleX without CSS layers (:not() specificity hack)188 ms ±3%
Plumeria on Solid189 ms ±10%
vanilla Solid (hand-written ceiling)222 ms ±6%
@yak/solid (styled API, Solid 2)242 ms ±4%
vanilla (hand-written ceiling)251 ms ±6%
@yak/react (css prop)252 ms ±3%
@yak/react (css prop) foldStatic: false259 ms ±12%
Bamboo268 ms ±4%
@yak/react (styled API)268 ms ±9%
Goober271 ms ±8%
styled-components274 ms ±3%
@yak/react (styled API) foldStatic: false275 ms ±7%
@yak/solid (styled API, Solid 2) foldStatic: false275 ms ±10%
cn (shadcn-ui) on Solid279 ms ±8%
Panda (css fn)286 ms ±8%
cn (shadcn-ui)289 ms ±6%
Panda (style props)290 ms ±10%
tailwind-merge297 ms ±9%
Emotion316 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX16.72 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)20.59 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Plumeria (classStyle prop)21.77 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX on Solid23.22 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
@yak/react (css prop)24.29 ms active(33.57 ms span · 33.57 ms profiled median (n=5))
vanilla (hand-written ceiling)24.46 ms active(33.66 ms span · 33.66 ms profiled median (n=5))
@yak/react (styled API)24.52 ms active(32.90 ms span · 32.90 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false24.89 ms active(32.64 ms span · 32.64 ms profiled median (n=5))
Goober25.10 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Panda (css fn)25.32 ms active(35.07 ms span · 35.07 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false25.98 ms active(32.49 ms span · 32.49 ms profiled median (n=5))
Plumeria on Solid27.12 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Emotion27.39 ms active(35.01 ms span · 35.01 ms profiled median (n=5))
cn (shadcn-ui)28.32 ms active(35.16 ms span · 35.16 ms profiled median (n=5))
Bamboo28.89 ms active(32.80 ms span · 32.80 ms profiled median (n=5))
styled-components29.32 ms active(35.72 ms span · 35.72 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)29.66 ms active(33.18 ms span · 33.18 ms profiled median (n=5))
tailwind-merge29.82 ms active(36.07 ms span · 36.07 ms profiled median (n=5))
Panda (style props)31.72 ms active(35.88 ms span · 35.88 ms profiled median (n=5))
cn (shadcn-ui) on Solid32.16 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)34.03 ms active(34.09 ms span · 34.09 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false44.31 ms active(49.84 ms span · 49.84 ms profiled median (n=5))

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

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

vanilla Solid (hand-written ceiling)71 ms ±5%
@yak/react (css prop)72 ms ±5%
vanilla (hand-written ceiling)73 ms ±4%
StyleX74 ms ±3%
Plumeria (classStyle prop)75 ms ±6%
Bamboo76 ms ±4%
StyleX without CSS layers (:not() specificity hack)76 ms ±5%
Plumeria on Solid81 ms ±6%
@yak/react (css prop) foldStatic: false82 ms ±2%
cn (shadcn-ui)86 ms ±4%
cn (shadcn-ui) on Solid86 ms ±5%
@yak/react (styled API)90 ms ±6%
@yak/react (styled API) foldStatic: false93 ms ±5%
Panda (css fn)96 ms ±3%
tailwind-merge104 ms ±4%
StyleX on Solid104 ms ±3%
Goober118 ms ±4%
styled-components120 ms ±4%
@yak/solid (styled API, Solid 2)145 ms ±5%
Panda (style props)158 ms ±5%
Emotion158 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false181 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX36.03 ms active(44.06 ms span)
StyleX without CSS layers (:not() specificity hack)39.11 ms active(45.68 ms span)
@yak/react (css prop)39.44 ms active(45.78 ms span)
vanilla Solid (hand-written ceiling)39.85 ms active(39.90 ms span)
vanilla (hand-written ceiling)40.37 ms active(40.63 ms span)
StyleX on Solid41.89 ms active(41.92 ms span)
Plumeria (classStyle prop)43.27 ms active(58.24 ms span)
@yak/react (css prop) foldStatic: false43.44 ms active(49.57 ms span)
Plumeria on Solid44.06 ms active(44.10 ms span)
cn (shadcn-ui) on Solid44.41 ms active(44.46 ms span)
Bamboo45.69 ms active(51.88 ms span)
@yak/react (styled API)48.68 ms active(50.74 ms span)
cn (shadcn-ui)49.86 ms active(54.28 ms span)
Panda (css fn)52.41 ms active(67.94 ms span)
Goober52.64 ms active(60.05 ms span)
styled-components53.92 ms active(64.52 ms span)
@yak/react (styled API) foldStatic: false54.81 ms active(65.31 ms span)
@yak/solid (styled API, Solid 2)56.52 ms active(56.55 ms span)
Panda (style props)56.70 ms active(61.43 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false57.22 ms active(57.25 ms span)
tailwind-merge58.76 ms active(67.12 ms span)
Emotion69.05 ms active(80.34 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)Chrome19.0 ms1 recalc
Firefox10.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome17.0 ms1 recalc
Firefox10.0 ms
BambooChrome21.1 ms1 recalc
Firefox13.0 ms
Panda (style props)Chrome20.2 ms1 recalc
Firefox12.0 ms
vanilla Solid (hand-written ceiling)Chrome19.0 ms1 recalc
Firefox11.0 ms
tailwind-mergeChrome24.2 ms1 recalc
Firefox13.0 ms
styled-componentsChrome16.7 ms1 recalc
Firefox11.0 ms
StyleXChrome14.8 ms1 recalc
Firefox11.0 ms
@yak/react (styled API)Chrome19.6 ms1 recalc
Firefox11.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome17.9 ms1 recalc
Firefox10.0 ms
Panda (css fn)Chrome22.9 ms1 recalc
Firefox16.0 ms
@yak/react (styled API) foldStatic: falseChrome20.9 ms1 recalc
Firefox9.0 ms
vanilla (hand-written ceiling)Chrome19.1 ms1 recalc
Firefox11.0 ms
@yak/react (css prop) foldStatic: falseChrome17.6 ms1 recalc
Firefox10.0 ms
StyleX on SolidChrome17.9 ms1 recalc
Firefox9.0 ms
Plumeria (classStyle prop)Chrome20.3 ms1 recalc
Firefox11.0 ms
Plumeria on SolidChrome22.6 ms1 recalc
Firefox12.1 ms
cn (shadcn-ui) on SolidChrome22.0 ms1 recalc
Firefox20.0 ms
GooberChrome18.7 ms1 recalc
Firefox16.0 ms
EmotionChrome20.7 ms1 recalc
Firefox12.0 ms
@yak/solid (styled API, Solid 2)Chrome19.3 ms1 recalc
Firefox12.0 ms
cn (shadcn-ui)Chrome23.2 ms1 recalc
Firefox13.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,844 B(0.5 / 0.6 / 1.7 kB)
@yak/react (css prop)2,862 B(0.5 / 0.6 / 1.7 kB)
@yak/react (styled API)3,557 B(1.2 / 0.6 / 1.7 kB)
@yak/react (styled API) foldStatic: false3,693 B(1.3 / 0.6 / 1.7 kB)
vanilla Solid (hand-written ceiling)5,089 B(0.0 / 1.0 / 3.9 kB)
Goober6,445 B(3.9 / 0.7 / 1.7 kB)
Plumeria (classStyle prop)6,835 B(1.1 / 0.9 / 4.7 kB)
StyleX9,095 B(3.5 / 0.9 / 4.5 kB)
StyleX without CSS layers (:not() specificity hack)9,170 B(3.5 / 1.0 / 4.5 kB)
Plumeria on Solid9,743 B(0.4 / 0.9 / 8.2 kB)
Bamboo11,116 B(2.8 / 1.6 / 6.4 kB)
@yak/solid (styled API, Solid 2)11,909 B(7.1 / 0.6 / 3.8 kB)
@yak/solid (styled API, Solid 2) foldStatic: false13,377 B(6.5 / 0.6 / 5.9 kB)
StyleX on Solid15,250 B(5.9 / 0.9 / 8.1 kB)
Emotion15,723 B(12.9 / 0.8 / 1.6 kB)
styled-components16,093 B(13.1 / 0.8 / 1.8 kB)
Panda (css fn)16,512 B(4.8 / 1.2 / 10.1 kB)
Panda (style props)21,376 B(9.2 / 1.2 / 10.5 kB)
tailwind-merge21,696 B(10.4 / 1.6 / 9.2 kB)
cn (shadcn-ui)24,234 B(12.9 / 1.6 / 9.2 kB)
cn (shadcn-ui) on Solid27,149 B(12.4 / 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.0871752623491005001,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)152,117 r/s ±14%(0.82 ms / 1k elems)
cn (shadcn-ui) on Solid141,814 r/s ±7%(0.88 ms / 1k elems)
Plumeria on Solid122,698 r/s ±10%(1.02 ms / 1k elems)
vanilla (hand-written ceiling)93,813 r/s ±3%(1.33 ms / 1k elems)
@yak/react (css prop)87,002 r/s ±2%(1.44 ms / 1k elems)
StyleX83,844 r/s ±8%(1.49 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)83,379 r/s ±5%(1.50 ms / 1k elems)
Bamboo81,295 r/s ±2%(1.54 ms / 1k elems)
StyleX on Solid80,789 r/s ±7%(1.55 ms / 1k elems)
Plumeria (classStyle prop)72,391 r/s ±6%(1.73 ms / 1k elems)
@yak/react (css prop) foldStatic: false67,110 r/s ±4%(1.86 ms / 1k elems)
@yak/solid (styled API, Solid 2)62,072 r/s ±12%(2.01 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false55,062 r/s ±5%(2.27 ms / 1k elems)
cn (shadcn-ui)54,913 r/s ±2%(2.28 ms / 1k elems)
@yak/react (styled API)51,096 r/s ±1%(2.45 ms / 1k elems)
@yak/react (styled API) foldStatic: false44,445 r/s ±1%(2.81 ms / 1k elems)
tailwind-merge38,720 r/s ±2%(3.23 ms / 1k elems)
Goober27,689 r/s ±2%(4.51 ms / 1k elems)
Panda (css fn)27,008 r/s ±1%(4.63 ms / 1k elems)
styled-components23,349 r/s ±1%(5.35 ms / 1k elems)
Emotion15,519 r/s ±1%(8.05 ms / 1k elems)
Panda (style props)14,038 r/s ±2%(8.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.

vanilla Solid (hand-written ceiling)735 req/s ±0%
vanilla (hand-written ceiling)490 req/s ±1%
Plumeria on Solid480 req/s ±1%
@yak/react (css prop)421 req/s ±0%
cn (shadcn-ui) on Solid388 req/s ±1%
@yak/solid (styled API, Solid 2)368 req/s ±1%
StyleX on Solid364 req/s ±0%
StyleX359 req/s ±1%
StyleX without CSS layers (:not() specificity hack)355 req/s ±1%
@yak/react (css prop) foldStatic: false350 req/s ±1%
Plumeria (classStyle prop)336 req/s ±0%
Bamboo313 req/s ±0%
@yak/solid (styled API, Solid 2) foldStatic: false313 req/s ±1%
@yak/react (styled API)305 req/s ±0%
@yak/react (styled API) foldStatic: false278 req/s ±2%
cn (shadcn-ui)220 req/s ±0%
tailwind-merge174 req/s ±0%
Goober172 req/s ±0%
styled-components139 req/s ±1%
Panda (css fn)131 req/s ±0%
Emotion114 req/s ±0%
Panda (style props)78 req/s ±0%

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

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

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
Plumeria on Solid1.54 ms
vanilla Solid (hand-written ceiling)1.88 ms
cn (shadcn-ui) on Solid2.15 ms
StyleX2.16 ms
Bamboo2.20 ms
Plumeria (classStyle prop)2.24 ms
StyleX on Solid2.25 ms
StyleX without CSS layers (:not() specificity hack)2.29 ms
vanilla (hand-written ceiling)2.38 ms
@yak/react (css prop)2.58 ms
@yak/react (css prop) foldStatic: false2.72 ms
@yak/solid (styled API, Solid 2)3.03 ms
@yak/react (styled API) foldStatic: false3.53 ms
cn (shadcn-ui)3.86 ms
@yak/react (styled API)3.87 ms
@yak/solid (styled API, Solid 2) foldStatic: false3.98 ms
tailwind-merge4.50 ms
Goober6.21 ms
Panda (css fn)6.61 ms
styled-components7.65 ms
Emotion10.12 ms
Panda (style props)13.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.

vanilla (hand-written ceiling)57 ms ±2%
StyleX58 ms ±6%
@yak/react (css prop)58 ms ±6%
StyleX without CSS layers (:not() specificity hack)58 ms ±6%
Plumeria (classStyle prop)60 ms ±4%
Bamboo61 ms ±3%
cn (shadcn-ui)70 ms ±2%
@yak/react (css prop) foldStatic: false72 ms ±4%
@yak/react (styled API)73 ms ±2%
@yak/react (styled API) foldStatic: false76 ms ±2%
Panda (css fn)80 ms ±4%
tailwind-merge83 ms ±4%
Plumeria on Solid91 ms ±7%
vanilla Solid (hand-written ceiling)92 ms ±2%
Goober95 ms ±1%
styled-components97 ms ±4%
cn (shadcn-ui) on Solid105 ms ±6%
StyleX on Solid121 ms ±4%
Emotion127 ms ±3%
Panda (style props)135 ms ±4%
@yak/solid (styled API, Solid 2)163 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false206 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/react (css prop)16.11 ms active(36.89 ms span)
StyleX16.15 ms active(41.02 ms span)
Plumeria (classStyle prop)16.19 ms active(39.95 ms span)
vanilla (hand-written ceiling)16.59 ms active(46.89 ms span)
Bamboo17.10 ms active(39.85 ms span)
StyleX without CSS layers (:not() specificity hack)17.18 ms active(30.18 ms span)
Panda (css fn)19.30 ms active(29.29 ms span)
@yak/react (css prop) foldStatic: false21.38 ms active(43.68 ms span)
cn (shadcn-ui) on Solid24.32 ms active(29.42 ms span)
@yak/react (styled API) foldStatic: false24.57 ms active(42.26 ms span)
@yak/react (styled API)24.61 ms active(33.41 ms span)
Goober24.66 ms active(35.53 ms span)
tailwind-merge25.73 ms active(55.30 ms span)
cn (shadcn-ui)25.74 ms active(36.94 ms span)
styled-components26.15 ms active(26.77 ms span)
Plumeria on Solid26.70 ms active(28.90 ms span)
vanilla Solid (hand-written ceiling)27.11 ms active(32.37 ms span)
StyleX on Solid29.13 ms active(30.56 ms span)
Panda (style props)29.38 ms active(32.49 ms span)
Emotion35.56 ms active(63.15 ms span)
@yak/solid (styled API, Solid 2)40.06 ms active(49.08 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false44.84 ms active(59.39 ms span)

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

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.

StyleX136 ms ±9%
Plumeria (classStyle prop)155 ms ±8%
Plumeria on Solid181 ms ±2%
StyleX without CSS layers (:not() specificity hack)182 ms ±5%
StyleX on Solid194 ms ±5%
vanilla Solid (hand-written ceiling)227 ms ±7%
vanilla (hand-written ceiling)250 ms ±7%
@yak/solid (styled API, Solid 2)251 ms ±14%
@yak/react (css prop)253 ms ±6%
@yak/react (styled API)258 ms ±5%
cn (shadcn-ui) on Solid258 ms ±6%
@yak/react (styled API) foldStatic: false259 ms ±5%
@yak/react (css prop) foldStatic: false261 ms ±6%
styled-components264 ms ±9%
Goober267 ms ±6%
@yak/solid (styled API, Solid 2) foldStatic: false267 ms ±11%
Bamboo267 ms ±3%
cn (shadcn-ui)283 ms ±4%
tailwind-merge285 ms ±7%
Panda (style props)296 ms ±3%
Panda (css fn)300 ms ±7%
Emotion312 ms ±7%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX18.84 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)19.79 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
Plumeria (classStyle prop)21.98 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
StyleX on Solid22.52 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (styled API)23.57 ms active(34.86 ms span · 34.86 ms profiled median (n=5))
@yak/react (css prop)24.19 ms active(33.66 ms span · 33.66 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false25.07 ms active(35.28 ms span · 35.28 ms profiled median (n=5))
Goober25.74 ms active(32.13 ms span · 32.13 ms profiled median (n=5))
vanilla (hand-written ceiling)25.77 ms active(34.47 ms span · 34.47 ms profiled median (n=5))
Panda (css fn)26.44 ms active(34.95 ms span · 34.95 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)26.53 ms active(33.18 ms span · 33.18 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false26.59 ms active(35.10 ms span · 35.10 ms profiled median (n=5))
Bamboo26.88 ms active(33.06 ms span · 33.06 ms profiled median (n=5))
cn (shadcn-ui)27.03 ms active(35.09 ms span · 35.09 ms profiled median (n=5))
Plumeria on Solid27.54 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
tailwind-merge27.61 ms active(35.09 ms span · 35.09 ms profiled median (n=5))
Emotion28.26 ms active(35.00 ms span · 35.00 ms profiled median (n=5))
styled-components28.93 ms active(33.03 ms span · 33.03 ms profiled median (n=5))
Panda (style props)29.75 ms active(35.65 ms span · 35.65 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)32.59 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
cn (shadcn-ui) on Solid32.70 ms active(33.22 ms span · 33.22 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false34.18 ms active(34.23 ms span · 34.23 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 (hand-written ceiling)71 ms ±7%
@yak/react (css prop)72 ms ±3%
Plumeria on Solid73 ms ±4%
StyleX without CSS layers (:not() specificity hack)75 ms ±2%
Bamboo75 ms ±4%
Plumeria (classStyle prop)75 ms ±3%
vanilla Solid (hand-written ceiling)76 ms ±6%
StyleX76 ms ±4%
@yak/react (css prop) foldStatic: false84 ms ±2%
cn (shadcn-ui)84 ms ±3%
cn (shadcn-ui) on Solid85 ms ±6%
@yak/react (styled API)90 ms ±3%
Panda (css fn)94 ms ±3%
@yak/react (styled API) foldStatic: false95 ms ±3%
StyleX on Solid98 ms ±3%
tailwind-merge101 ms ±4%
Goober117 ms ±4%
styled-components120 ms ±4%
@yak/solid (styled API, Solid 2)142 ms ±3%
Emotion154 ms ±5%
Panda (style props)160 ms ±6%
@yak/solid (styled API, Solid 2) foldStatic: false176 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
StyleX35.17 ms active(35.81 ms span)
StyleX on Solid36.11 ms active(36.14 ms span)
StyleX without CSS layers (:not() specificity hack)40.45 ms active(52.82 ms span)
vanilla Solid (hand-written ceiling)40.78 ms active(40.81 ms span)
Plumeria on Solid42.33 ms active(42.38 ms span)
Bamboo42.49 ms active(48.84 ms span)
@yak/react (css prop) foldStatic: false43.94 ms active(52.07 ms span)
Plumeria (classStyle prop)44.88 ms active(50.95 ms span)
Panda (css fn)46.64 ms active(61.42 ms span)
@yak/react (styled API) foldStatic: false47.31 ms active(59.09 ms span)
@yak/react (css prop)48.05 ms active(60.71 ms span)
@yak/react (styled API)48.45 ms active(55.16 ms span)
vanilla (hand-written ceiling)48.46 ms active(48.73 ms span)
cn (shadcn-ui) on Solid50.43 ms active(50.48 ms span)
Goober52.71 ms active(58.20 ms span)
cn (shadcn-ui)53.62 ms active(65.62 ms span)
Panda (style props)54.87 ms active(60.98 ms span)
@yak/solid (styled API, Solid 2)55.77 ms active(55.82 ms span)
tailwind-merge57.72 ms active(64.40 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false58.31 ms active(58.34 ms span)
Emotion65.72 ms active(74.36 ms span)
styled-components67.52 ms active(74.19 ms span)

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

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

style recalclayoutpaint (Chrome)
@yak/solid (styled API, Solid 2) foldStatic: falseChrome17.6 ms1 recalc
Firefox12.0 ms
StyleX on SolidChrome14.1 ms1 recalc
Firefox10.0 ms
Panda (style props)Chrome18.2 ms1 recalc
Firefox12.0 ms
@yak/solid (styled API, Solid 2)Chrome18.6 ms1 recalc
Firefox10.0 ms
vanilla (hand-written ceiling)Chrome26.8 ms1 recalc
Firefox12.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome19.2 ms1 recalc
Firefox13.0 ms
@yak/react (styled API)Chrome17.7 ms1 recalc
Firefox13.0 ms
@yak/react (styled API) foldStatic: falseChrome17.7 ms1 recalc
Firefox10.0 ms
@yak/react (css prop)Chrome23.9 ms1 recalc
Firefox11.0 ms
EmotionChrome19.1 ms1 recalc
Firefox11.0 ms
Plumeria (classStyle prop)Chrome21.5 ms1 recalc
Firefox10.0 ms
@yak/react (css prop) foldStatic: falseChrome19.1 ms1 recalc
Firefox13.0 ms
StyleXChrome15.1 ms1 recalc
Firefox10.0 ms
Plumeria on SolidChrome19.2 ms1 recalc
Firefox11.0 ms
GooberChrome18.5 ms1 recalc
Firefox13.0 ms
vanilla Solid (hand-written ceiling)Chrome20.0 ms1 recalc
Firefox12.0 ms
Panda (css fn)Chrome20.0 ms1 recalc
Firefox12.0 ms
cn (shadcn-ui) on SolidChrome22.3 ms1 recalc
Firefox14.0 ms
BambooChrome20.6 ms1 recalc
Firefox13.0 ms
styled-componentsChrome23.9 ms1 recalc
Firefox12.0 ms
cn (shadcn-ui)Chrome23.7 ms1 recalc
Firefox17.0 ms
tailwind-mergeChrome23.5 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,844 B(0.5 / 0.6 / 1.7 kB)
@yak/react (css prop)2,863 B(0.5 / 0.6 / 1.7 kB)
@yak/react (styled API)3,562 B(1.2 / 0.6 / 1.7 kB)
@yak/react (styled API) foldStatic: false3,693 B(1.3 / 0.6 / 1.7 kB)
vanilla Solid (hand-written ceiling)5,089 B(0.0 / 1.0 / 3.9 kB)
Goober6,445 B(3.9 / 0.7 / 1.7 kB)
Plumeria (classStyle prop)6,835 B(1.1 / 0.9 / 4.7 kB)
StyleX9,095 B(3.5 / 0.9 / 4.5 kB)
StyleX without CSS layers (:not() specificity hack)9,170 B(3.5 / 1.0 / 4.5 kB)
Plumeria on Solid9,743 B(0.4 / 0.9 / 8.2 kB)
Bamboo11,116 B(2.8 / 1.6 / 6.4 kB)
@yak/solid (styled API, Solid 2)11,912 B(7.1 / 0.6 / 3.8 kB)
@yak/solid (styled API, Solid 2) foldStatic: false13,369 B(6.5 / 0.6 / 5.9 kB)
StyleX on Solid15,250 B(5.9 / 0.9 / 8.1 kB)
Emotion15,723 B(12.9 / 0.8 / 1.6 kB)
styled-components16,078 B(13.1 / 0.8 / 1.7 kB)
Panda (css fn)16,512 B(4.8 / 1.2 / 10.1 kB)
Panda (style props)21,376 B(9.2 / 1.2 / 10.5 kB)
tailwind-merge21,696 B(10.4 / 1.6 / 9.2 kB)
cn (shadcn-ui)24,234 B(12.9 / 1.6 / 9.2 kB)
cn (shadcn-ui) on Solid27,149 B(12.4 / 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.0871752623501005001,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,246,439 r/s ±7%(0.45 ms / 1k elems)
Plumeria on Solid2,040,708 r/s ±8%(0.49 ms / 1k elems)
vanilla (hand-written ceiling)1,319,646 r/s ±2%(0.76 ms / 1k elems)
Plumeria (classStyle prop)1,311,859 r/s ±4%(0.76 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false1,104,032 r/s ±2%(0.91 ms / 1k elems)
@yak/solid (styled API, Solid 2)1,090,038 r/s ±11%(0.92 ms / 1k elems)
@yak/react (css prop)991,056 r/s ±4%(1.01 ms / 1k elems)
@yak/react (css prop) foldStatic: false969,538 r/s ±4%(1.03 ms / 1k elems)
StyleX on Solid806,834 r/s ±2%(1.24 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)804,896 r/s ±1%(1.24 ms / 1k elems)
StyleX797,675 r/s ±2%(1.25 ms / 1k elems)
@yak/react (styled API) foldStatic: false568,041 r/s ±1%(1.76 ms / 1k elems)
@yak/react (styled API)557,978 r/s ±1%(1.79 ms / 1k elems)
Emotion228,497 r/s ±2%(4.38 ms / 1k elems)
styled-components179,392 r/s ±5%(5.57 ms / 1k elems)
Goober150,061 r/s ±2%(6.66 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)1,668 req/s ±0%
Plumeria on Solid1,660 req/s ±0%
vanilla (hand-written ceiling)1,161 req/s ±0%
Plumeria (classStyle prop)1,152 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false1,061 req/s ±0%
@yak/solid (styled API, Solid 2)1,052 req/s ±2%
@yak/react (css prop) foldStatic: false953 req/s ±3%
@yak/react (css prop)932 req/s ±2%
StyleX without CSS layers (:not() specificity hack)774 req/s ±1%
StyleX760 req/s ±2%
StyleX on Solid687 req/s ±0%
@yak/react (styled API) foldStatic: false658 req/s ±1%
@yak/react (styled API)647 req/s ±1%
Emotion282 req/s ±2%
styled-components181 req/s ±0%
Goober155 req/s ±0%

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

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

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
@yak/solid (styled API, Solid 2) foldStatic: false1.00 ms
vanilla Solid (hand-written ceiling)1.01 ms
Plumeria on Solid1.07 ms
@yak/solid (styled API, Solid 2)1.08 ms
@yak/react (css prop)1.10 ms
@yak/react (css prop) foldStatic: false1.13 ms
Plumeria (classStyle prop)1.38 ms
vanilla (hand-written ceiling)1.41 ms
StyleX without CSS layers (:not() specificity hack)1.44 ms
StyleX1.56 ms
@yak/react (styled API) foldStatic: false1.76 ms
StyleX on Solid1.81 ms
@yak/react (styled API)2.28 ms
Emotion4.11 ms
styled-components7.32 ms
Goober8.92 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)52 ms ±4%
Plumeria (classStyle prop)52 ms ±2%
StyleX without CSS layers (:not() specificity hack)61 ms ±3%
@yak/react (css prop)61 ms ±4%
StyleX62 ms ±3%
@yak/react (css prop) foldStatic: false62 ms ±5%
vanilla Solid (hand-written ceiling)62 ms ±3%
Plumeria on Solid63 ms ±2%
@yak/react (styled API) foldStatic: false63 ms ±3%
@yak/react (styled API)64 ms ±2%
styled-components78 ms ±3%
Emotion80 ms ±6%
StyleX on Solid82 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false103 ms ±1%
@yak/solid (styled API, Solid 2)104 ms ±4%
Goober117 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid7.51 ms active(21.79 ms span)
vanilla Solid (hand-written ceiling)8.11 ms active(34.94 ms span)
StyleX on Solid9.22 ms active(23.28 ms span)
@yak/solid (styled API, Solid 2)10.82 ms active(24.47 ms span)
vanilla (hand-written ceiling)10.88 ms active(24.37 ms span)
Plumeria (classStyle prop)10.93 ms active(35.65 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false11.27 ms active(20.12 ms span)
@yak/react (css prop) foldStatic: false11.41 ms active(35.99 ms span)
@yak/react (styled API)11.57 ms active(35.49 ms span)
StyleX11.71 ms active(39.97 ms span)
@yak/react (css prop)11.72 ms active(36.67 ms span)
styled-components12.13 ms active(30.52 ms span)
@yak/react (styled API) foldStatic: false12.40 ms active(26.49 ms span)
Emotion12.85 ms active(32.19 ms span)
Goober13.15 ms active(35.05 ms span)
StyleX without CSS layers (:not() specificity hack)13.67 ms active(30.86 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)55 ms ±3%
vanilla (hand-written ceiling)57 ms ±9%
Plumeria on Solid69 ms ±12%
@yak/react (css prop)70 ms ±9%
Plumeria (classStyle prop)70 ms ±9%
@yak/react (css prop) foldStatic: false71 ms ±12%
@yak/solid (styled API, Solid 2) foldStatic: false73 ms ±10%
@yak/react (styled API) foldStatic: false74 ms ±10%
@yak/solid (styled API, Solid 2)74 ms ±13%
styled-components75 ms ±6%
@yak/react (styled API)75 ms ±3%
Emotion76 ms ±3%
StyleX79 ms ±3%
StyleX on Solid81 ms ±12%
StyleX without CSS layers (:not() specificity hack)82 ms ±8%
Goober83 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)1.47 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
vanilla (hand-written ceiling)1.70 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
Plumeria on Solid1.93 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false1.98 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.02 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.11 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
StyleX on Solid2.30 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (css prop)2.31 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Goober2.40 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
Plumeria (classStyle prop)2.60 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
styled-components2.67 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/react (styled API)2.67 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false2.68 ms active(33.41 ms span · 33.41 ms profiled median (n=5))
Emotion2.72 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.78 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX3.37 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)45 ms ±4%
Plumeria on Solid46 ms ±3%
vanilla (hand-written ceiling)60 ms ±5%
Plumeria (classStyle prop)62 ms ±4%
@yak/react (css prop)68 ms ±2%
StyleX on Solid70 ms ±5%
@yak/react (css prop) foldStatic: false70 ms ±4%
StyleX without CSS layers (:not() specificity hack)70 ms ±3%
StyleX71 ms ±6%
@yak/react (styled API)72 ms ±2%
@yak/react (styled API) foldStatic: false72 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false84 ms ±2%
@yak/solid (styled API, Solid 2)85 ms ±5%
styled-components141 ms ±2%
Emotion143 ms ±4%
Goober2,273 ms ±1%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)9.24 ms active(18.15 ms span)
Plumeria on Solid9.51 ms active(29.47 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false12.34 ms active(21.37 ms span)
@yak/solid (styled API, Solid 2)12.54 ms active(21.41 ms span)
StyleX on Solid13.19 ms active(21.58 ms span)
StyleX without CSS layers (:not() specificity hack)15.83 ms active(40.52 ms span)
@yak/react (css prop)15.85 ms active(30.00 ms span)
@yak/react (css prop) foldStatic: false15.89 ms active(33.82 ms span)
vanilla (hand-written ceiling)16.02 ms active(38.56 ms span)
StyleX16.04 ms active(40.44 ms span)
@yak/react (styled API) foldStatic: false16.69 ms active(45.00 ms span)
@yak/react (styled API)17.32 ms active(40.91 ms span)
Plumeria (classStyle prop)17.93 ms active(40.25 ms span)
styled-components20.14 ms active(39.95 ms span)
Goober20.24 ms active(34.88 ms span)
Emotion21.80 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)
EmotionChrome2.7 ms1 recalc
Firefox1.0 ms
@yak/react (styled API)Chrome3.3 ms1 recalc
Firefox1.0 ms
@yak/react (css prop)Chrome2.9 ms1 recalc
Firefox0.0 ms
@yak/react (styled API) foldStatic: falseChrome2.9 ms1 recalc
Firefox1.0 ms
styled-componentsChrome2.8 ms1 recalc
Firefox1.0 ms
StyleXChrome3.1 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome3.1 ms1 recalc
Firefox0.0 ms
vanilla Solid (hand-written ceiling)Chrome2.8 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome2.8 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome2.9 ms1 recalc
Firefox1.0 ms
Plumeria (classStyle prop)Chrome3.3 ms1 recalc
Firefox1.0 ms
StyleX on SolidChrome3.3 ms1 recalc
Firefox1.0 ms
Plumeria on SolidChrome3.0 ms1 recalc
Firefox1.0 ms
@yak/react (css prop) foldStatic: falseChrome2.8 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome2.8 ms1 recalc
Firefox2.2 ms
GooberChrome3.1 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
vanilla (hand-written ceiling)5,502 B(0.0 / 0.3 / 5.0 kB)
@yak/react (css prop) foldStatic: false5,821 B(0.5 / 0.1 / 5.1 kB)
@yak/react (css prop)5,843 B(0.5 / 0.1 / 5.1 kB)
@yak/react (styled API)6,442 B(1.2 / 0.1 / 5.0 kB)
@yak/react (styled API) foldStatic: false6,572 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,836 B(0.4 / 0.1 / 8.1 kB)
StyleX without CSS layers (:not() specificity hack)9,245 B(3.5 / 0.1 / 5.4 kB)
StyleX9,251 B(3.5 / 0.1 / 5.4 kB)
StyleX on Solid14,497 B(5.9 / 0.1 / 8.1 kB)
@yak/solid (styled API, Solid 2) foldStatic: false14,639 B(6.5 / 0.1 / 7.7 kB)
@yak/solid (styled API, Solid 2)15,283 B(7.1 / 0.1 / 7.7 kB)
Goober21,594 B(3.9 / 8.9 / 8.3 kB)
styled-components35,416 B(13.1 / 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.01042093134181005001,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.

vanilla Solid (hand-written ceiling)2,261,246 r/s ±6%(0.44 ms / 1k elems)
@yak/solid (styled API, Solid 2)2,167,768 r/s ±8%(0.46 ms / 1k elems)
Plumeria on Solid2,097,945 r/s ±6%(0.48 ms / 1k elems)
cn (shadcn-ui) on Solid1,783,988 r/s ±9%(0.56 ms / 1k elems)
Bamboo1,333,108 r/s ±3%(0.75 ms / 1k elems)
vanilla (hand-written ceiling)1,304,000 r/s ±2%(0.77 ms / 1k elems)
@yak/react (css prop)1,301,993 r/s ±3%(0.77 ms / 1k elems)
@yak/react (styled API)1,301,109 r/s ±5%(0.77 ms / 1k elems)
cn (shadcn-ui)1,290,882 r/s ±3%(0.77 ms / 1k elems)
Plumeria (classStyle prop)1,285,380 r/s ±7%(0.78 ms / 1k elems)
tailwind-merge1,270,500 r/s ±7%(0.79 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false1,121,075 r/s ±3%(0.89 ms / 1k elems)
@yak/react (css prop) foldStatic: false1,064,451 r/s ±4%(0.94 ms / 1k elems)
StyleX810,448 r/s ±4%(1.23 ms / 1k elems)
StyleX on Solid804,332 r/s ±4%(1.24 ms / 1k elems)
Panda (css fn)794,602 r/s ±5%(1.26 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)793,598 r/s ±2%(1.26 ms / 1k elems)
@yak/react (styled API) foldStatic: false792,999 r/s ±2%(1.26 ms / 1k elems)
Goober617,046 r/s ±3%(1.62 ms / 1k elems)
styled-components545,871 r/s ±1%(1.83 ms / 1k elems)
Panda (style props)430,574 r/s ±1%(2.32 ms / 1k elems)
Emotion296,283 r/s ±1%(3.38 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)1,651 req/s ±2%
@yak/solid (styled API, Solid 2)1,623 req/s ±1%
Plumeria on Solid1,618 req/s ±1%
cn (shadcn-ui) on Solid1,311 req/s ±0%
@yak/react (css prop)1,175 req/s ±1%
vanilla (hand-written ceiling)1,171 req/s ±0%
@yak/react (styled API)1,162 req/s ±1%
Bamboo1,139 req/s ±2%
Plumeria (classStyle prop)1,138 req/s ±0%
cn (shadcn-ui)1,104 req/s ±1%
tailwind-merge1,076 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false1,070 req/s ±1%
@yak/react (css prop) foldStatic: false965 req/s ±2%
@yak/react (styled API) foldStatic: false780 req/s ±1%
StyleX without CSS layers (:not() specificity hack)767 req/s ±3%
StyleX761 req/s ±1%
StyleX on Solid685 req/s ±0%
Goober668 req/s ±0%
Panda (css fn)652 req/s ±2%
styled-components537 req/s ±1%
Panda (style props)482 req/s ±1%
Emotion347 req/s ±0%

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

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

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
Plumeria on Solid0.73 ms
cn (shadcn-ui) on Solid0.79 ms
tailwind-merge0.98 ms
@yak/react (styled API)0.98 ms
vanilla Solid (hand-written ceiling)1.01 ms
vanilla (hand-written ceiling)1.01 ms
@yak/react (css prop) foldStatic: false1.03 ms
@yak/solid (styled API, Solid 2)1.10 ms
Plumeria (classStyle prop)1.13 ms
cn (shadcn-ui)1.25 ms
StyleX without CSS layers (:not() specificity hack)1.36 ms
@yak/solid (styled API, Solid 2) foldStatic: false1.37 ms
StyleX1.42 ms
Bamboo1.52 ms
Goober1.52 ms
@yak/react (css prop)1.52 ms
@yak/react (styled API) foldStatic: false1.53 ms
Panda (css fn)1.92 ms
StyleX on Solid1.99 ms
styled-components2.16 ms
Panda (style props)2.21 ms
Emotion3.28 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)52 ms ±2%
vanilla (hand-written ceiling)52 ms ±3%
Plumeria (classStyle prop)52 ms ±4%
@yak/react (css prop)53 ms ±2%
Bamboo54 ms ±4%
cn (shadcn-ui)56 ms ±4%
@yak/react (styled API) foldStatic: false57 ms ±4%
@yak/react (css prop) foldStatic: false58 ms ±3%
Panda (css fn)58 ms ±4%
StyleX60 ms ±5%
vanilla Solid (hand-written ceiling)61 ms ±5%
@yak/solid (styled API, Solid 2)62 ms ±1%
StyleX without CSS layers (:not() specificity hack)63 ms ±6%
Plumeria on Solid64 ms ±5%
styled-components67 ms ±3%
cn (shadcn-ui) on Solid68 ms ±3%
Goober69 ms ±4%
Panda (style props)75 ms ±6%
tailwind-merge77 ms ±3%
Emotion80 ms ±5%
StyleX on Solid80 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false82 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid7.48 ms active(25.41 ms span)
@yak/solid (styled API, Solid 2)7.89 ms active(20.06 ms span)
vanilla Solid (hand-written ceiling)8.56 ms active(32.96 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false8.99 ms active(22.02 ms span)
cn (shadcn-ui) on Solid9.13 ms active(28.51 ms span)
StyleX on Solid9.52 ms active(32.60 ms span)
Panda (css fn)11.05 ms active(32.00 ms span)
Plumeria (classStyle prop)11.13 ms active(34.05 ms span)
@yak/react (styled API)11.17 ms active(40.95 ms span)
@yak/react (styled API) foldStatic: false11.21 ms active(28.41 ms span)
@yak/react (css prop)11.30 ms active(34.44 ms span)
Bamboo11.60 ms active(34.27 ms span)
StyleX11.67 ms active(27.29 ms span)
StyleX without CSS layers (:not() specificity hack)11.71 ms active(23.82 ms span)
Goober12.02 ms active(28.84 ms span)
styled-components12.08 ms active(32.72 ms span)
cn (shadcn-ui)12.27 ms active(22.74 ms span)
vanilla (hand-written ceiling)13.54 ms active(41.63 ms span)
@yak/react (css prop) foldStatic: false13.86 ms active(44.74 ms span)
Panda (style props)14.27 ms active(34.79 ms span)
Emotion16.18 ms active(33.91 ms span)
tailwind-merge18.38 ms active(36.30 ms span)

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

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)55 ms ±2%
@yak/react (css prop)55 ms ±4%
@yak/solid (styled API, Solid 2)56 ms ±4%
Bamboo57 ms ±5%
cn (shadcn-ui) on Solid57 ms ±3%
vanilla (hand-written ceiling)57 ms ±3%
tailwind-merge59 ms ±2%
cn (shadcn-ui)59 ms ±2%
@yak/react (styled API) foldStatic: false60 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false60 ms ±2%
@yak/react (styled API)61 ms ±5%
@yak/react (css prop) foldStatic: false61 ms ±7%
Panda (css fn)62 ms ±3%
Panda (style props)64 ms ±4%
styled-components65 ms ±4%
Goober67 ms ±5%
Emotion72 ms ±3%
Plumeria on Solid72 ms ±14%
Plumeria (classStyle prop)73 ms ±6%
StyleX77 ms ±10%
StyleX on Solid79 ms ±10%
StyleX without CSS layers (:not() specificity hack)80 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/react (styled API)1.62 ms active(33.24 ms span · 33.24 ms profiled median (n=5))
cn (shadcn-ui) on Solid1.62 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)1.71 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/react (css prop)1.75 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
tailwind-merge1.87 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Panda (style props)1.88 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
vanilla (hand-written ceiling)1.91 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Plumeria on Solid1.95 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false1.99 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Bamboo2.09 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
cn (shadcn-ui)2.17 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
styled-components2.24 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false2.24 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Panda (css fn)2.35 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Goober2.38 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.41 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Plumeria (classStyle prop)2.46 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.55 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)2.64 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX2.66 ms active(33.41 ms span · 33.41 ms profiled median (n=5))
Emotion2.76 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
StyleX on Solid2.78 ms active(33.36 ms span · 33.36 ms profiled median (n=5))

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

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

@yak/solid (styled API, Solid 2)45 ms ±3%
Plumeria on Solid45 ms ±3%
vanilla Solid (hand-written ceiling)47 ms ±4%
cn (shadcn-ui) on Solid55 ms ±5%
@yak/react (styled API)59 ms ±4%
@yak/react (css prop)59 ms ±4%
vanilla (hand-written ceiling)59 ms ±4%
Bamboo60 ms ±4%
Plumeria (classStyle prop)60 ms ±5%
@yak/react (css prop) foldStatic: false64 ms ±3%
cn (shadcn-ui)65 ms ±5%
@yak/react (styled API) foldStatic: false66 ms ±3%
StyleX on Solid68 ms ±4%
Panda (css fn)69 ms ±4%
StyleX without CSS layers (:not() specificity hack)70 ms ±4%
StyleX72 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false73 ms ±4%
styled-components77 ms ±4%
Goober80 ms ±5%
tailwind-merge81 ms ±3%
Panda (style props)86 ms ±4%
Emotion96 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid9.12 ms active(33.07 ms span)
vanilla Solid (hand-written ceiling)9.14 ms active(29.85 ms span)
@yak/solid (styled API, Solid 2)9.45 ms active(21.44 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false10.74 ms active(24.78 ms span)
StyleX on Solid11.60 ms active(37.63 ms span)
@yak/react (styled API) foldStatic: false15.09 ms active(28.14 ms span)
@yak/react (css prop) foldStatic: false15.27 ms active(31.95 ms span)
Plumeria (classStyle prop)15.36 ms active(40.91 ms span)
Bamboo15.43 ms active(41.03 ms span)
@yak/react (styled API)15.51 ms active(33.19 ms span)
StyleX15.60 ms active(32.61 ms span)
@yak/react (css prop)15.60 ms active(30.33 ms span)
StyleX without CSS layers (:not() specificity hack)15.80 ms active(33.50 ms span)
cn (shadcn-ui) on Solid15.88 ms active(15.90 ms span)
vanilla (hand-written ceiling)16.37 ms active(34.56 ms span)
Panda (css fn)16.46 ms active(34.51 ms span)
cn (shadcn-ui)16.78 ms active(30.10 ms span)
Goober17.70 ms active(34.79 ms span)
styled-components18.24 ms active(37.76 ms span)
Panda (style props)18.82 ms active(35.65 ms span)
Emotion19.30 ms active(34.88 ms span)
tailwind-merge27.34 ms active(40.49 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)
BambooChrome2.7 ms1 recalc
Firefox0.0 ms
cn (shadcn-ui) on SolidChrome3.1 ms1 recalc
Firefox1.0 ms
@yak/react (css prop) foldStatic: falseChrome2.9 ms1 recalc
Firefox0.0 ms
Panda (css fn)Chrome3.2 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome3.2 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome3.0 ms1 recalc
Firefox0.0 ms
StyleXChrome3.0 ms1 recalc
Firefox1.0 ms
StyleX on SolidChrome3.2 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome3.5 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome3.3 ms1 recalc
Firefox1.0 ms
vanilla Solid (hand-written ceiling)Chrome2.8 ms1 recalc
Firefox0.0 ms
Plumeria (classStyle prop)Chrome2.8 ms1 recalc
Firefox0.0 ms
Plumeria on SolidChrome2.9 ms1 recalc
Firefox1.0 ms
EmotionChrome3.0 ms1 recalc
Firefox2.0 ms
@yak/react (styled API) foldStatic: falseChrome2.8 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome2.9 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome3.1 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui)Chrome3.2 ms1 recalc
Firefox2.0 ms
GooberChrome3.4 ms1 recalc
Firefox1.0 ms
@yak/react (css prop)Chrome3.0 ms1 recalc
Firefox3.0 ms
@yak/react (styled API)Chrome2.9 ms1 recalc
Firefox1.0 ms
styled-componentsChrome3.3 ms1 recalc
Firefox1.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,913 B(0.5 / 0.1 / 5.2 kB)
@yak/react (css prop)5,933 B(0.5 / 0.1 / 5.2 kB)
@yak/react (styled API)6,632 B(1.2 / 0.1 / 5.2 kB)
@yak/react (styled API) foldStatic: false6,761 B(1.3 / 0.1 / 5.2 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)
Bamboo8,555 B(2.8 / 0.1 / 5.4 kB)
Plumeria on Solid8,836 B(0.4 / 0.1 / 8.1 kB)
StyleX without CSS layers (:not() specificity hack)9,245 B(3.5 / 0.1 / 5.4 kB)
StyleX9,251 B(3.5 / 0.1 / 5.4 kB)
Goober9,361 B(3.9 / 0.1 / 5.2 kB)
Panda (css fn)10,537 B(4.8 / 0.1 / 5.4 kB)
StyleX on Solid14,497 B(5.9 / 0.1 / 8.1 kB)
@yak/solid (styled API, Solid 2) foldStatic: false14,783 B(6.5 / 0.1 / 7.9 kB)
Panda (style props)15,224 B(9.2 / 0.1 / 5.5 kB)
@yak/solid (styled API, Solid 2)15,450 B(7.1 / 0.1 / 7.9 kB)
tailwind-merge16,443 B(10.4 / 0.5 / 5.2 kB)
Emotion18,596 B(12.9 / 0.1 / 5.2 kB)
styled-components18,887 B(13.1 / 0.1 / 5.2 kB)
cn (shadcn-ui)18,981 B(12.9 / 0.5 / 5.2 kB)
cn (shadcn-ui) on Solid21,417 B(12.4 / 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.712151005001,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.

@yak/solid (styled API, Solid 2)2,041,083 r/s ±50%(0.49 ms / 1k elems)
@yak/react (styled API)1,630,515 r/s ±12%(0.61 ms / 1k elems)
StyleX1,468,399 r/s ±23%(0.68 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,450,124 r/s ±16%(0.69 ms / 1k elems)
cn (shadcn-ui)1,385,667 r/s ±23%(0.72 ms / 1k elems)
Panda (recipe)1,330,017 r/s ±28%(0.75 ms / 1k elems)
Bamboo1,254,331 r/s ±31%(0.80 ms / 1k elems)
StyleX on Solid1,249,638 r/s ±13%(0.80 ms / 1k elems)
tailwind-merge994,489 r/s ±22%(1.01 ms / 1k elems)
Panda (css fn)759,492 r/s ±14%(1.32 ms / 1k elems)
@yak/react (css prop) foldStatic: false737,290 r/s ±33%(1.36 ms / 1k elems)
vanilla (hand-written ceiling)730,574 r/s ±43%(1.37 ms / 1k elems)
vanilla Solid (hand-written ceiling)720,713 r/s ±16%(1.39 ms / 1k elems)
@yak/react (css prop)712,372 r/s ±41%(1.40 ms / 1k elems)
Plumeria (classStyle prop)705,253 r/s ±5%(1.42 ms / 1k elems)
cn (shadcn-ui) on Solid692,050 r/s ±2%(1.44 ms / 1k elems)
Plumeria on Solid596,913 r/s ±17%(1.68 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false481,234 r/s ±11%(2.08 ms / 1k elems)
Panda (style props)408,235 r/s ±14%(2.45 ms / 1k elems)
@yak/react (styled API) foldStatic: false407,186 r/s ±25%(2.46 ms / 1k elems)
styled-components406,826 r/s ±5%(2.46 ms / 1k elems)
Goober374,024 r/s ±17%(2.67 ms / 1k elems)
Emotion231,061 r/s ±6%(4.33 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,295 req/s ±1%
@yak/react (styled API)1,530 req/s ±1%
vanilla (hand-written ceiling)1,329 req/s ±1%
StyleX without CSS layers (:not() specificity hack)1,319 req/s ±0%
StyleX1,316 req/s ±0%
@yak/react (css prop)1,313 req/s ±1%
Plumeria (classStyle prop)1,128 req/s ±0%
vanilla Solid (hand-written ceiling)1,109 req/s ±1%
StyleX on Solid1,044 req/s ±1%
Bamboo1,023 req/s ±1%
cn (shadcn-ui)994 req/s ±2%
Plumeria on Solid901 req/s ±0%
@yak/react (css prop) foldStatic: false868 req/s ±2%
Panda (recipe)861 req/s ±1%
cn (shadcn-ui) on Solid808 req/s ±2%
tailwind-merge808 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false757 req/s ±1%
Panda (css fn)665 req/s ±0%
@yak/react (styled API) foldStatic: false614 req/s ±0%
styled-components454 req/s ±0%
Panda (style props)416 req/s ±0%
Goober393 req/s ±0%
Emotion317 req/s ±2%

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

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

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
@yak/solid (styled API, Solid 2)0.62 ms
@yak/react (styled API)0.86 ms
StyleX0.94 ms
cn (shadcn-ui) on Solid0.95 ms
StyleX on Solid1.04 ms
Plumeria on Solid1.05 ms
StyleX without CSS layers (:not() specificity hack)1.21 ms
Panda (recipe)1.24 ms
@yak/react (css prop) foldStatic: false1.25 ms
Bamboo1.29 ms
vanilla (hand-written ceiling)1.48 ms
cn (shadcn-ui)1.51 ms
@yak/react (css prop)1.52 ms
vanilla Solid (hand-written ceiling)1.63 ms
@yak/solid (styled API, Solid 2) foldStatic: false1.69 ms
Plumeria (classStyle prop)1.82 ms
Panda (css fn)1.97 ms
tailwind-merge2.15 ms
Panda (style props)2.37 ms
@yak/react (styled API) foldStatic: false2.37 ms
styled-components2.59 ms
Goober2.90 ms
Emotion3.61 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)49 ms ±2%
Bamboo53 ms ±2%
vanilla (hand-written ceiling)53 ms ±3%
@yak/react (css prop)53 ms ±2%
StyleX55 ms ±3%
StyleX without CSS layers (:not() specificity hack)55 ms ±3%
Plumeria (classStyle prop)56 ms ±5%
@yak/solid (styled API, Solid 2)61 ms ±5%
@yak/react (css prop) foldStatic: false62 ms ±3%
Panda (recipe)63 ms ±3%
cn (shadcn-ui)63 ms ±3%
Panda (css fn)66 ms ±2%
vanilla Solid (hand-written ceiling)66 ms ±4%
styled-components67 ms ±2%
@yak/react (styled API) foldStatic: false69 ms ±4%
Plumeria on Solid71 ms ±3%
Goober75 ms ±4%
StyleX on Solid75 ms ±4%
cn (shadcn-ui) on Solid76 ms ±5%
tailwind-merge78 ms ±3%
Panda (style props)83 ms ±2%
Emotion88 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false111 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)7.38 ms active(19.85 ms span)
vanilla Solid (hand-written ceiling)7.61 ms active(25.56 ms span)
Plumeria on Solid7.76 ms active(28.94 ms span)
StyleX on Solid9.03 ms active(21.11 ms span)
vanilla (hand-written ceiling)10.75 ms active(31.04 ms span)
StyleX10.76 ms active(32.83 ms span)
Plumeria (classStyle prop)11.01 ms active(32.83 ms span)
Bamboo11.30 ms active(33.88 ms span)
@yak/react (styled API)11.32 ms active(36.04 ms span)
@yak/react (css prop)11.49 ms active(27.17 ms span)
@yak/react (styled API) foldStatic: false11.96 ms active(35.82 ms span)
cn (shadcn-ui) on Solid12.13 ms active(38.77 ms span)
Panda (css fn)12.32 ms active(34.12 ms span)
@yak/react (css prop) foldStatic: false12.97 ms active(37.19 ms span)
Panda (recipe)13.16 ms active(35.31 ms span)
StyleX without CSS layers (:not() specificity hack)13.29 ms active(31.40 ms span)
Emotion13.57 ms active(34.03 ms span)
Goober13.59 ms active(24.93 ms span)
cn (shadcn-ui)14.23 ms active(41.66 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false14.98 ms active(19.72 ms span)
styled-components15.01 ms active(30.13 ms span)
Panda (style props)18.26 ms active(33.08 ms span)
tailwind-merge18.70 ms active(40.33 ms span)

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

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

@yak/react (css prop)65 ms ±3%
@yak/react (styled API)65 ms ±3%
@yak/solid (styled API, Solid 2)65 ms ±4%
vanilla (hand-written ceiling)66 ms ±3%
Bamboo68 ms ±2%
StyleX69 ms ±3%
Panda (recipe)69 ms ±2%
vanilla Solid (hand-written ceiling)69 ms ±6%
StyleX on Solid70 ms ±2%
tailwind-merge70 ms ±2%
cn (shadcn-ui) on Solid70 ms ±4%
Panda (css fn)70 ms ±2%
Plumeria on Solid71 ms ±3%
@yak/react (css prop) foldStatic: false71 ms ±2%
cn (shadcn-ui)72 ms ±3%
Plumeria (classStyle prop)72 ms ±5%
styled-components73 ms ±5%
StyleX without CSS layers (:not() specificity hack)73 ms ±6%
@yak/react (styled API) foldStatic: false73 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false74 ms ±3%
Goober75 ms ±3%
Panda (style props)78 ms ±5%
Emotion85 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)2.06 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.25 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
styled-components2.31 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX on Solid2.36 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Panda (recipe)2.61 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
cn (shadcn-ui)2.62 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
tailwind-merge2.70 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
Bamboo2.71 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (styled API)2.81 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false2.82 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
Panda (style props)2.86 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
@yak/react (css prop)2.94 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
StyleX2.95 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false3.07 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)3.09 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
Goober3.09 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
vanilla (hand-written ceiling)3.15 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
Emotion3.18 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
Plumeria (classStyle prop)3.22 ms active(33.19 ms span · 33.19 ms profiled median (n=5))
cn (shadcn-ui) on Solid3.32 ms active(33.23 ms span · 33.23 ms profiled median (n=5))
Plumeria on Solid3.56 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false3.57 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
Panda (css fn)4.20 ms active(33.33 ms span · 33.33 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)39 ms ±6%
vanilla Solid (hand-written ceiling)47 ms ±6%
Plumeria on Solid50 ms ±3%
@yak/react (styled API)51 ms ±3%
StyleX on Solid54 ms ±2%
cn (shadcn-ui) on Solid55 ms ±2%
vanilla (hand-written ceiling)56 ms ±4%
Bamboo57 ms ±5%
@yak/react (css prop)57 ms ±3%
StyleX58 ms ±5%
Plumeria (classStyle prop)59 ms ±4%
StyleX without CSS layers (:not() specificity hack)60 ms ±4%
cn (shadcn-ui)65 ms ±3%
@yak/react (css prop) foldStatic: false66 ms ±4%
Panda (recipe)67 ms ±4%
Panda (css fn)68 ms ±4%
@yak/react (styled API) foldStatic: false71 ms ±7%
Goober80 ms ±4%
styled-components81 ms ±2%
tailwind-merge81 ms ±5%
Panda (style props)85 ms ±7%
@yak/solid (styled API, Solid 2) foldStatic: false89 ms ±5%
Emotion104 ms ±6%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
vanilla Solid (hand-written ceiling)11.19 ms active(30.64 ms span)
Plumeria on Solid11.55 ms active(22.63 ms span)
@yak/solid (styled API, Solid 2)11.78 ms active(20.29 ms span)
cn (shadcn-ui) on Solid14.20 ms active(20.64 ms span)
StyleX on Solid14.39 ms active(27.25 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false14.57 ms active(23.27 ms span)
@yak/react (css prop)16.71 ms active(40.52 ms span)
Bamboo16.83 ms active(33.33 ms span)
StyleX without CSS layers (:not() specificity hack)17.26 ms active(40.32 ms span)
@yak/react (styled API)17.48 ms active(34.06 ms span)
@yak/react (styled API) foldStatic: false17.75 ms active(31.30 ms span)
Panda (css fn)18.27 ms active(32.61 ms span)
StyleX18.85 ms active(40.27 ms span)
cn (shadcn-ui)19.28 ms active(42.79 ms span)
Panda (recipe)19.54 ms active(38.02 ms span)
vanilla (hand-written ceiling)20.16 ms active(46.32 ms span)
Plumeria (classStyle prop)20.59 ms active(33.22 ms span)
Panda (style props)20.79 ms active(35.97 ms span)
@yak/react (css prop) foldStatic: false21.30 ms active(45.66 ms span)
styled-components21.35 ms active(46.26 ms span)
Goober23.34 ms active(49.53 ms span)
Emotion24.65 ms active(38.14 ms span)
tailwind-merge25.64 ms active(36.78 ms span)

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

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

style recalclayoutpaint (Chrome)
BambooChrome4.9 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui)Chrome4.7 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui) on SolidChrome4.7 ms1 recalc
Firefox1.0 ms
EmotionChrome5.0 ms1 recalc
Firefox2.0 ms
GooberChrome5.4 ms1 recalc
Firefox1.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: falseChrome5.7 ms1 recalc
Firefox1.0 ms
@yak/react (styled API) foldStatic: falseChrome4.8 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome5.2 ms1 recalc
Firefox4.0 ms
Panda (style props)Chrome5.1 ms1 recalc
Firefox3.0 ms
Panda (recipe)Chrome5.3 ms1 recalc
Firefox2.0 ms
styled-componentsChrome5.1 ms1 recalc
Firefox3.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome4.8 ms1 recalc
Firefox2.0 ms
StyleXChrome5.0 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome5.2 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.6 ms1 recalc
Firefox2.0 ms
Plumeria on SolidChrome5.1 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome5.2 ms1 recalc
Firefox2.0 ms
tailwind-mergeChrome5.6 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome4.8 ms1 recalc
Firefox2.0 ms

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

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)3,789 B(0.0 / 0.5 / 3.2 kB)
@yak/react (css prop) foldStatic: false3,876 B(0.5 / 0.2 / 3.1 kB)
@yak/react (css prop)3,895 B(0.5 / 0.2 / 3.1 kB)
@yak/react (styled API)4,598 B(1.2 / 0.2 / 3.1 kB)
@yak/react (styled API) foldStatic: false4,722 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,385 B(1.3 / 0.3 / 4.6 kB)
Plumeria on Solid6,527 B(0.4 / 0.3 / 5.7 kB)
Goober7,395 B(3.9 / 0.3 / 3.1 kB)
StyleX7,988 B(3.5 / 0.3 / 4.0 kB)
StyleX without CSS layers (:not() specificity hack)7,996 B(3.5 / 0.3 / 4.0 kB)
Bamboo8,007 B(2.8 / 0.4 / 4.5 kB)
Panda (css fn)9,958 B(4.8 / 0.3 / 4.6 kB)
@yak/solid (styled API, Solid 2) foldStatic: false12,089 B(6.5 / 0.2 / 5.1 kB)
StyleX on Solid12,389 B(5.9 / 0.3 / 5.9 kB)
@yak/solid (styled API, Solid 2)12,911 B(7.1 / 0.2 / 5.2 kB)
Panda (style props)14,485 B(9.2 / 0.3 / 4.6 kB)
tailwind-merge15,528 B(10.4 / 0.7 / 4.0 kB)
Emotion16,693 B(12.9 / 0.3 / 3.1 kB)
styled-components17,022 B(13.1 / 0.3 / 3.2 kB)
cn (shadcn-ui)18,066 B(12.9 / 0.7 / 4.0 kB)
cn (shadcn-ui) on Solid19,274 B(12.4 / 0.7 / 5.7 kB)

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

0.04.59.013181005001,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)5,459,558 r/s ±9%(0.18 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false3,673,513 r/s ±6%(0.27 ms / 1k elems)
StyleX on Solid3,125,186 r/s ±12%(0.32 ms / 1k elems)
vanilla Solid (hand-written ceiling)3,048,753 r/s ±9%(0.33 ms / 1k elems)
Plumeria on Solid3,029,055 r/s ±6%(0.33 ms / 1k elems)
cn (shadcn-ui) on Solid2,106,980 r/s ±8%(0.47 ms / 1k elems)
@yak/react (styled API)2,041,708 r/s ±3%(0.49 ms / 1k elems)
Bamboo1,677,807 r/s ±3%(0.60 ms / 1k elems)
StyleX1,648,500 r/s ±2%(0.61 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)1,647,604 r/s ±2%(0.61 ms / 1k elems)
vanilla (hand-written ceiling)1,638,198 r/s ±3%(0.61 ms / 1k elems)
@yak/react (css prop)1,620,307 r/s ±4%(0.62 ms / 1k elems)
tailwind-merge1,567,993 r/s ±4%(0.64 ms / 1k elems)
cn (shadcn-ui)1,537,262 r/s ±2%(0.65 ms / 1k elems)
@yak/react (css prop) foldStatic: false1,276,002 r/s ±3%(0.78 ms / 1k elems)
@yak/react (styled API) foldStatic: false1,072,743 r/s ±1%(0.93 ms / 1k elems)
Plumeria (classStyle prop)983,789 r/s ±4%(1.02 ms / 1k elems)
Panda (css fn)932,296 r/s ±3%(1.07 ms / 1k elems)
Goober754,015 r/s ±1%(1.33 ms / 1k elems)
styled-components631,349 r/s ±1%(1.58 ms / 1k elems)
Panda (style props)509,373 r/s ±1%(1.96 ms / 1k elems)
Emotion334,958 r/s ±1%(2.99 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,596 req/s ±0%
@yak/solid (styled API, Solid 2) foldStatic: false2,681 req/s ±1%
vanilla Solid (hand-written ceiling)2,459 req/s ±0%
Plumeria on Solid2,311 req/s ±1%
StyleX on Solid2,289 req/s ±0%
@yak/react (styled API)1,755 req/s ±0%
cn (shadcn-ui) on Solid1,677 req/s ±0%
@yak/react (css prop)1,517 req/s ±0%
vanilla (hand-written ceiling)1,508 req/s ±1%
StyleX without CSS layers (:not() specificity hack)1,474 req/s ±0%
StyleX1,470 req/s ±0%
Plumeria (classStyle prop)1,453 req/s ±0%
Bamboo1,430 req/s ±1%
cn (shadcn-ui)1,388 req/s ±1%
tailwind-merge1,365 req/s ±1%
@yak/react (css prop) foldStatic: false1,289 req/s ±1%
@yak/react (styled API) foldStatic: false1,156 req/s ±2%
Goober878 req/s ±1%
styled-components660 req/s ±1%
Panda (css fn)613 req/s ±1%
Panda (style props)529 req/s ±0%
Emotion399 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.54 ms
@yak/react (styled API)0.70 ms
cn (shadcn-ui) on Solid0.72 ms
@yak/solid (styled API, Solid 2)0.76 ms
Plumeria on Solid0.77 ms
vanilla Solid (hand-written ceiling)0.79 ms
StyleX on Solid0.83 ms
StyleX without CSS layers (:not() specificity hack)1.06 ms
vanilla (hand-written ceiling)1.07 ms
StyleX1.18 ms
Panda (css fn)1.25 ms
@yak/react (css prop)1.28 ms
@yak/react (css prop) foldStatic: false1.28 ms
Bamboo1.31 ms
cn (shadcn-ui)1.42 ms
tailwind-merge1.44 ms
Plumeria (classStyle prop)1.45 ms
@yak/react (styled API) foldStatic: false1.91 ms
Goober1.99 ms
Panda (style props)2.52 ms
styled-components2.58 ms
Emotion2.98 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)48 ms ±2%
StyleX50 ms ±2%
vanilla (hand-written ceiling)50 ms ±5%
Bamboo51 ms ±3%
@yak/react (css prop)51 ms ±4%
StyleX without CSS layers (:not() specificity hack)52 ms ±4%
Plumeria (classStyle prop)52 ms ±3%
@yak/solid (styled API, Solid 2)54 ms ±3%
@yak/react (styled API) foldStatic: false56 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false57 ms ±5%
cn (shadcn-ui)58 ms ±3%
Panda (css fn)58 ms ±5%
@yak/react (css prop) foldStatic: false58 ms ±3%
vanilla Solid (hand-written ceiling)59 ms ±3%
StyleX on Solid59 ms ±3%
Plumeria on Solid60 ms ±2%
styled-components65 ms ±2%
Goober65 ms ±4%
cn (shadcn-ui) on Solid68 ms ±4%
tailwind-merge74 ms ±3%
Panda (style props)76 ms ±4%
Emotion79 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
Plumeria on Solid6.62 ms active(25.28 ms span)
@yak/solid (styled API, Solid 2)6.76 ms active(13.23 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false6.80 ms active(25.60 ms span)
vanilla Solid (hand-written ceiling)6.90 ms active(28.66 ms span)
StyleX on Solid8.57 ms active(14.91 ms span)
cn (shadcn-ui) on Solid9.26 ms active(33.71 ms span)
vanilla (hand-written ceiling)10.76 ms active(34.45 ms span)
@yak/react (css prop)10.77 ms active(34.02 ms span)
StyleX10.86 ms active(36.07 ms span)
@yak/react (styled API) foldStatic: false10.87 ms active(24.05 ms span)
Panda (css fn)11.01 ms active(20.43 ms span)
Plumeria (classStyle prop)11.12 ms active(32.95 ms span)
StyleX without CSS layers (:not() specificity hack)11.21 ms active(35.81 ms span)
@yak/react (styled API)11.31 ms active(40.91 ms span)
styled-components11.69 ms active(40.22 ms span)
Goober12.25 ms active(26.99 ms span)
@yak/react (css prop) foldStatic: false12.67 ms active(34.35 ms span)
Emotion13.07 ms active(37.95 ms span)
Bamboo13.12 ms active(26.44 ms span)
cn (shadcn-ui)13.38 ms active(36.95 ms span)
Panda (style props)15.38 ms active(22.95 ms span)
tailwind-merge19.00 ms active(36.20 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)53 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false54 ms ±4%
StyleX on Solid55 ms ±3%
vanilla Solid (hand-written ceiling)56 ms ±3%
cn (shadcn-ui) on Solid56 ms ±4%
StyleX without CSS layers (:not() specificity hack)57 ms ±6%
Bamboo57 ms ±4%
@yak/react (css prop) foldStatic: false57 ms ±2%
Plumeria (classStyle prop)57 ms ±4%
StyleX57 ms ±4%
Plumeria on Solid58 ms ±10%
@yak/react (css prop)58 ms ±5%
tailwind-merge59 ms ±4%
@yak/react (styled API)59 ms ±4%
cn (shadcn-ui)59 ms ±7%
vanilla (hand-written ceiling)59 ms ±5%
Panda (css fn)60 ms ±5%
@yak/react (styled API) foldStatic: false60 ms ±5%
Goober61 ms ±4%
styled-components63 ms ±3%
Panda (style props)67 ms ±4%
Emotion70 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
cn (shadcn-ui) on Solid1.61 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)1.75 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
vanilla (hand-written ceiling)1.77 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)1.80 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/react (styled API)1.84 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Panda (css fn)1.85 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false1.88 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false1.88 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)1.94 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
styled-components1.96 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
StyleX on Solid1.97 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Plumeria (classStyle prop)1.97 ms active(33.35 ms span · 33.35 ms profiled median (n=5))
Goober1.98 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Plumeria on Solid2.05 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
tailwind-merge2.06 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
Panda (style props)2.07 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false2.40 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
Bamboo2.40 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
@yak/react (css prop)2.45 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
StyleX2.49 ms active(33.26 ms span · 33.26 ms profiled median (n=5))
cn (shadcn-ui)2.77 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
Emotion3.16 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)33 ms ±5%
vanilla Solid (hand-written ceiling)37 ms ±4%
Plumeria on Solid37 ms ±4%
StyleX on Solid38 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false39 ms ±5%
cn (shadcn-ui) on Solid48 ms ±3%
@yak/react (styled API)49 ms ±3%
vanilla (hand-written ceiling)53 ms ±5%
Plumeria (classStyle prop)53 ms ±4%
@yak/react (css prop)55 ms ±4%
Bamboo55 ms ±4%
StyleX55 ms ±3%
StyleX without CSS layers (:not() specificity hack)57 ms ±3%
@yak/react (styled API) foldStatic: false59 ms ±4%
@yak/react (css prop) foldStatic: false60 ms ±4%
cn (shadcn-ui)62 ms ±4%
Panda (css fn)62 ms ±5%
Goober69 ms ±4%
styled-components73 ms ±3%
tailwind-merge78 ms ±5%
Panda (style props)80 ms ±4%
Emotion88 ms ±5%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)10.10 ms active(15.31 ms span)
StyleX on Solid10.33 ms active(29.98 ms span)
vanilla Solid (hand-written ceiling)10.50 ms active(17.49 ms span)
Plumeria on Solid11.14 ms active(26.33 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false11.64 ms active(19.05 ms span)
cn (shadcn-ui) on Solid12.73 ms active(14.32 ms span)
Bamboo16.25 ms active(32.17 ms span)
@yak/react (styled API)16.31 ms active(29.38 ms span)
StyleX without CSS layers (:not() specificity hack)16.39 ms active(33.21 ms span)
@yak/react (styled API) foldStatic: false16.45 ms active(42.63 ms span)
vanilla (hand-written ceiling)16.68 ms active(30.97 ms span)
Plumeria (classStyle prop)17.15 ms active(41.38 ms span)
StyleX17.34 ms active(39.74 ms span)
Panda (css fn)17.38 ms active(36.37 ms span)
Goober17.65 ms active(40.82 ms span)
@yak/react (css prop)17.82 ms active(36.05 ms span)
@yak/react (css prop) foldStatic: false17.94 ms active(38.41 ms span)
cn (shadcn-ui)18.89 ms active(43.28 ms span)
styled-components19.89 ms active(35.90 ms span)
Panda (style props)20.01 ms active(46.39 ms span)
Emotion21.00 ms active(40.15 ms span)
tailwind-merge28.21 ms active(44.76 ms span)

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

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

style recalclayoutpaint (Chrome)
BambooChrome4.3 ms1 recalc
Firefox2.0 ms
EmotionChrome4.5 ms1 recalc
Firefox1.0 ms
GooberChrome4.3 ms1 recalc
Firefox1.0 ms
@yak/react (styled API)Chrome4.8 ms1 recalc
Firefox1.0 ms
@yak/react (css prop)Chrome4.8 ms1 recalc
Firefox2.0 ms
@yak/react (css prop) foldStatic: falseChrome5.1 ms1 recalc
Firefox1.0 ms
@yak/react (styled API) foldStatic: falseChrome4.2 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome4.7 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome4.7 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome4.4 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome4.5 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome4.7 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome4.3 ms1 recalc
Firefox2.0 ms
@yak/solid (styled API, Solid 2)Chrome4.4 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.6 ms1 recalc
Firefox1.0 ms
Plumeria (classStyle prop)Chrome4.9 ms1 recalc
Firefox1.0 ms
Plumeria on SolidChrome5.1 ms1 recalc
Firefox1.0 ms
StyleXChrome4.9 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui)Chrome4.7 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome4.8 ms1 recalc
Firefox2.0 ms
cn (shadcn-ui) on SolidChrome4.6 ms1 recalc
Firefox2.0 ms
styled-componentsChrome5.1 ms1 recalc
Firefox2.0 ms

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

Page bytes shipped — JS + CSS + HTML, gzipped · lower is betteriGzipped bytes the browser downloads: the JavaScript this lane adds on top of a bare framework page, its CSS, and the server HTML. Lower is better. Solid marks every element with a hydration key and React needs none, so read the HTML column across frameworks with that in mind.

JSCSSHTML
vanilla (hand-written ceiling)2,700 B(0.0 / 0.3 / 2.4 kB)
@yak/react (css prop) foldStatic: false3,167 B(0.5 / 0.1 / 2.5 kB)
@yak/react (css prop)3,187 B(0.5 / 0.1 / 2.5 kB)
@yak/react (styled API)3,888 B(1.2 / 0.1 / 2.5 kB)
@yak/react (styled API) foldStatic: false4,020 B(1.3 / 0.1 / 2.5 kB)
Plumeria (classStyle prop)4,201 B(1.1 / 0.2 / 2.8 kB)
vanilla Solid (hand-written ceiling)5,340 B(0.0 / 0.3 / 5.0 kB)
Plumeria on Solid5,916 B(0.4 / 0.2 / 5.2 kB)
Bamboo6,093 B(2.8 / 0.2 / 2.9 kB)
StyleX without CSS layers (:not() specificity hack)6,566 B(3.5 / 0.2 / 2.7 kB)
StyleX6,570 B(3.5 / 0.2 / 2.7 kB)
Goober6,730 B(3.9 / 0.1 / 2.6 kB)
Panda (css fn)8,251 B(4.8 / 0.2 / 3.1 kB)
StyleX on Solid11,584 B(5.9 / 0.2 / 5.2 kB)
@yak/solid (styled API, Solid 2) foldStatic: false11,860 B(6.5 / 0.1 / 4.9 kB)
@yak/solid (styled API, Solid 2)12,633 B(7.1 / 0.1 / 5.1 kB)
Panda (style props)12,778 B(9.2 / 0.2 / 3.1 kB)
tailwind-merge14,201 B(10.4 / 0.6 / 2.9 kB)
Emotion15,877 B(12.9 / 0.2 / 2.4 kB)
styled-components16,248 B(13.1 / 0.2 / 2.6 kB)
cn (shadcn-ui)16,739 B(12.9 / 0.6 / 2.9 kB)
cn (shadcn-ui) on Solid18,831 B(12.4 / 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,578,489 r/s ±15%(0.18 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false3,658,969 r/s ±6%(0.27 ms / 1k elems)
@yak/react (styled API)2,020,471 r/s ±3%(0.49 ms / 1k elems)
Bamboo1,173,998 r/s ±3%(0.85 ms / 1k elems)
vanilla Solid (hand-written ceiling)1,132,711 r/s ±8%(0.88 ms / 1k elems)
vanilla (hand-written ceiling)1,117,776 r/s ±2%(0.89 ms / 1k elems)
cn (shadcn-ui) on Solid1,110,700 r/s ±7%(0.90 ms / 1k elems)
cn (shadcn-ui)1,103,873 r/s ±2%(0.91 ms / 1k elems)
@yak/react (styled API) foldStatic: false907,396 r/s ±3%(1.10 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)838,249 r/s ±3%(1.19 ms / 1k elems)
StyleX835,946 r/s ±1%(1.20 ms / 1k elems)
StyleX on Solid784,563 r/s ±3%(1.27 ms / 1k elems)
@yak/react (css prop)716,395 r/s ±3%(1.40 ms / 1k elems)
@yak/react (css prop) foldStatic: false711,226 r/s ±3%(1.41 ms / 1k elems)
tailwind-merge678,654 r/s ±3%(1.47 ms / 1k elems)
styled-components576,170 r/s ±1%(1.74 ms / 1k elems)
Panda (style props)544,790 r/s ±1%(1.84 ms / 1k elems)
Panda (css fn)528,644 r/s ±2%(1.89 ms / 1k elems)
Goober362,952 r/s ±1%(2.76 ms / 1k elems)
Emotion291,494 r/s ±1%(3.43 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,458 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false2,661 req/s ±1%
@yak/react (styled API)1,730 req/s ±0%
vanilla (hand-written ceiling)1,079 req/s ±0%
vanilla Solid (hand-written ceiling)999 req/s ±0%
@yak/react (styled API) foldStatic: false944 req/s ±0%
Bamboo893 req/s ±0%
cn (shadcn-ui)847 req/s ±1%
StyleX without CSS layers (:not() specificity hack)822 req/s ±2%
StyleX816 req/s ±0%
cn (shadcn-ui) on Solid759 req/s ±1%
@yak/react (css prop) foldStatic: false740 req/s ±0%
@yak/react (css prop)732 req/s ±1%
StyleX on Solid695 req/s ±0%
tailwind-merge581 req/s ±1%
styled-components579 req/s ±2%
Panda (style props)523 req/s ±0%
Panda (css fn)463 req/s ±1%
Goober432 req/s ±2%
Emotion357 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.29 ms
@yak/solid (styled API, Solid 2) foldStatic: false0.31 ms
vanilla Solid (hand-written ceiling)0.99 ms
vanilla (hand-written ceiling)1.02 ms
@yak/react (styled API)1.13 ms
cn (shadcn-ui) on Solid1.18 ms
StyleX1.36 ms
@yak/react (css prop) foldStatic: false1.47 ms
cn (shadcn-ui)1.50 ms
Bamboo1.51 ms
StyleX without CSS layers (:not() specificity hack)1.53 ms
@yak/react (css prop)1.53 ms
tailwind-merge1.57 ms
StyleX on Solid1.77 ms
@yak/react (styled API) foldStatic: false1.97 ms
Panda (css fn)2.11 ms
styled-components2.35 ms
Panda (style props)2.44 ms
Goober2.59 ms
Emotion3.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.

@yak/react (styled API)48 ms ±5%
@yak/solid (styled API, Solid 2)54 ms ±3%
@yak/solid (styled API, Solid 2) foldStatic: false55 ms ±2%
@yak/react (styled API) foldStatic: false58 ms ±4%
vanilla (hand-written ceiling)59 ms ±3%
Bamboo60 ms ±3%
StyleX62 ms ±2%
StyleX without CSS layers (:not() specificity hack)63 ms ±5%
@yak/react (css prop) foldStatic: false63 ms ±3%
@yak/react (css prop)64 ms ±5%
styled-components66 ms ±2%
cn (shadcn-ui)68 ms ±3%
vanilla Solid (hand-written ceiling)69 ms ±4%
Panda (css fn)73 ms ±3%
Goober78 ms ±3%
cn (shadcn-ui) on Solid78 ms ±5%
Panda (style props)79 ms ±4%
tailwind-merge79 ms ±3%
Emotion81 ms ±3%
StyleX on Solid84 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2) foldStatic: false7.26 ms active(34.75 ms span)
@yak/solid (styled API, Solid 2)8.30 ms active(19.43 ms span)
vanilla Solid (hand-written ceiling)8.42 ms active(25.31 ms span)
cn (shadcn-ui) on Solid10.16 ms active(32.22 ms span)
StyleX on Solid10.31 ms active(15.70 ms span)
@yak/react (styled API)10.87 ms active(28.12 ms span)
@yak/react (styled API) foldStatic: false11.08 ms active(31.84 ms span)
@yak/react (css prop) foldStatic: false11.11 ms active(20.53 ms span)
styled-components11.26 ms active(26.99 ms span)
vanilla (hand-written ceiling)11.69 ms active(34.72 ms span)
StyleX11.76 ms active(37.68 ms span)
@yak/react (css prop)11.82 ms active(35.16 ms span)
StyleX without CSS layers (:not() specificity hack)12.27 ms active(33.80 ms span)
Panda (css fn)12.73 ms active(22.27 ms span)
Bamboo12.90 ms active(21.63 ms span)
cn (shadcn-ui)13.95 ms active(24.56 ms span)
Panda (style props)14.66 ms active(27.07 ms span)
Goober15.55 ms active(37.41 ms span)
Emotion16.18 ms active(31.65 ms span)
tailwind-merge18.62 ms active(30.48 ms span)

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

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

vanilla Solid (hand-written ceiling)58 ms ±3%
StyleX on Solid58 ms ±5%
@yak/react (styled API)58 ms ±4%
@yak/solid (styled API, Solid 2) foldStatic: false59 ms ±5%
cn (shadcn-ui) on Solid59 ms ±4%
@yak/solid (styled API, Solid 2)59 ms ±6%
vanilla (hand-written ceiling)60 ms ±4%
Bamboo61 ms ±6%
StyleX without CSS layers (:not() specificity hack)61 ms ±3%
tailwind-merge62 ms ±4%
@yak/react (styled API) foldStatic: false62 ms ±5%
@yak/react (css prop)63 ms ±4%
cn (shadcn-ui)64 ms ±3%
@yak/react (css prop) foldStatic: false65 ms ±4%
Panda (css fn)65 ms ±6%
StyleX66 ms ±6%
styled-components66 ms ±4%
Panda (style props)67 ms ±3%
Emotion68 ms ±5%
Goober73 ms ±3%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
tailwind-merge1.66 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)1.80 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
styled-components1.92 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
cn (shadcn-ui) on Solid1.96 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Panda (style props)2.09 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
vanilla (hand-written ceiling)2.10 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
Panda (css fn)2.14 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false2.29 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
StyleX on Solid2.34 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.36 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
@yak/react (styled API)2.45 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
StyleX2.45 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false2.46 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
Emotion2.50 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/react (css prop)2.59 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
cn (shadcn-ui)2.66 ms active(33.38 ms span · 33.38 ms profiled median (n=5))
Bamboo2.73 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)2.81 ms active(33.29 ms span · 33.29 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.89 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Goober3.24 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)32 ms ±2%
@yak/solid (styled API, Solid 2) foldStatic: false38 ms ±4%
vanilla Solid (hand-written ceiling)49 ms ±2%
@yak/react (styled API)49 ms ±3%
cn (shadcn-ui) on Solid55 ms ±5%
vanilla (hand-written ceiling)61 ms ±5%
Bamboo61 ms ±3%
StyleX on Solid61 ms ±3%
@yak/react (styled API) foldStatic: false62 ms ±5%
StyleX66 ms ±3%
@yak/react (css prop) foldStatic: false67 ms ±6%
StyleX without CSS layers (:not() specificity hack)67 ms ±4%
cn (shadcn-ui)68 ms ±5%
@yak/react (css prop)71 ms ±5%
styled-components75 ms ±2%
Panda (css fn)78 ms ±2%
tailwind-merge83 ms ±6%
Panda (style props)84 ms ±4%
Goober84 ms ±3%
Emotion93 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)9.68 ms active(21.36 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false10.45 ms active(24.94 ms span)
vanilla Solid (hand-written ceiling)11.61 ms active(20.93 ms span)
StyleX on Solid12.07 ms active(22.19 ms span)
cn (shadcn-ui) on Solid13.87 ms active(13.89 ms span)
@yak/react (styled API)15.89 ms active(31.84 ms span)
@yak/react (styled API) foldStatic: false16.83 ms active(31.02 ms span)
Bamboo17.08 ms active(28.96 ms span)
@yak/react (css prop) foldStatic: false17.11 ms active(37.36 ms span)
@yak/react (css prop)17.20 ms active(29.10 ms span)
vanilla (hand-written ceiling)17.39 ms active(27.81 ms span)
StyleX17.54 ms active(43.22 ms span)
Goober17.88 ms active(28.99 ms span)
StyleX without CSS layers (:not() specificity hack)18.32 ms active(36.41 ms span)
Panda (css fn)18.34 ms active(44.92 ms span)
Panda (style props)20.13 ms active(45.91 ms span)
Emotion21.07 ms active(36.94 ms span)
styled-components21.16 ms active(34.39 ms span)
cn (shadcn-ui)23.86 ms active(49.17 ms span)
tailwind-merge25.79 ms active(49.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)
BambooChrome4.8 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui)Chrome5.5 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome4.9 ms1 recalc
Firefox2.0 ms
EmotionChrome4.7 ms1 recalc
Firefox1.0 ms
@yak/react (css prop)Chrome4.8 ms1 recalc
Firefox1.0 ms
@yak/react (css prop) foldStatic: falseChrome4.8 ms1 recalc
Firefox1.0 ms
@yak/react (styled API) foldStatic: falseChrome4.5 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome4.6 ms1 recalc
Firefox1.0 ms
styled-componentsChrome5.3 ms1 recalc
Firefox1.0 ms
StyleXChrome4.7 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome4.5 ms1 recalc
Firefox1.0 ms
vanilla (hand-written ceiling)Chrome5.2 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome4.2 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.7 ms1 recalc
Firefox1.0 ms
@yak/react (styled API)Chrome4.4 ms1 recalc
Firefox2.0 ms
Panda (style props)Chrome4.6 ms1 recalc
Firefox2.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.1 ms1 recalc
Firefox2.0 ms
GooberChrome4.4 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome4.5 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome4.7 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,287 B(0.5 / 0.2 / 2.6 kB)
@yak/react (css prop)3,305 B(0.5 / 0.2 / 2.6 kB)
@yak/react (styled API)4,006 B(1.2 / 0.2 / 2.6 kB)
@yak/react (styled API) foldStatic: false4,137 B(1.3 / 0.2 / 2.6 kB)
vanilla Solid (hand-written ceiling)5,193 B(0.0 / 0.4 / 4.7 kB)
Bamboo6,310 B(2.8 / 0.3 / 3.1 kB)
StyleX6,792 B(3.5 / 0.3 / 2.9 kB)
StyleX without CSS layers (:not() specificity hack)6,794 B(3.5 / 0.3 / 2.9 kB)
Goober6,810 B(3.9 / 0.2 / 2.6 kB)
Panda (css fn)8,417 B(4.8 / 0.2 / 3.2 kB)
StyleX on Solid11,911 B(5.9 / 0.3 / 5.5 kB)
@yak/solid (styled API, Solid 2) foldStatic: false12,117 B(6.5 / 0.2 / 5.1 kB)
@yak/solid (styled API, Solid 2)12,726 B(7.1 / 0.2 / 5.1 kB)
Panda (style props)12,944 B(9.2 / 0.2 / 3.2 kB)
tailwind-merge14,486 B(10.4 / 0.7 / 3.1 kB)
Emotion15,929 B(12.9 / 0.2 / 2.4 kB)
styled-components16,507 B(13.1 / 0.3 / 2.7 kB)
cn (shadcn-ui)17,024 B(12.9 / 0.7 / 3.1 kB)
cn (shadcn-ui) on Solid19,310 B(12.4 / 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,949,477 r/s ±6%(0.17 ms / 1k elems)
@yak/solid (styled API, Solid 2) foldStatic: false3,866,557 r/s ±6%(0.26 ms / 1k elems)
@yak/react (styled API)2,080,460 r/s ±2%(0.48 ms / 1k elems)
@yak/react (styled API) foldStatic: false866,902 r/s ±1%(1.15 ms / 1k elems)
Bamboo815,539 r/s ±2%(1.23 ms / 1k elems)
vanilla (hand-written ceiling)758,033 r/s ±2%(1.32 ms / 1k elems)
cn (shadcn-ui)710,664 r/s ±2%(1.41 ms / 1k elems)
vanilla Solid (hand-written ceiling)670,096 r/s ±6%(1.49 ms / 1k elems)
cn (shadcn-ui) on Solid628,120 r/s ±5%(1.59 ms / 1k elems)
StyleX without CSS layers (:not() specificity hack)590,181 r/s ±3%(1.69 ms / 1k elems)
StyleX587,144 r/s ±1%(1.70 ms / 1k elems)
styled-components527,234 r/s ±1%(1.90 ms / 1k elems)
StyleX on Solid505,420 r/s ±4%(1.98 ms / 1k elems)
@yak/react (css prop)463,256 r/s ±2%(2.16 ms / 1k elems)
@yak/react (css prop) foldStatic: false444,719 r/s ±1%(2.25 ms / 1k elems)
Panda (style props)407,197 r/s ±1%(2.46 ms / 1k elems)
tailwind-merge376,879 r/s ±1%(2.65 ms / 1k elems)
Panda (css fn)292,771 r/s ±3%(3.42 ms / 1k elems)
Emotion242,691 r/s ±1%(4.12 ms / 1k elems)
Goober203,785 r/s ±0%(4.91 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,337 req/s ±1%
@yak/solid (styled API, Solid 2) foldStatic: false2,514 req/s ±0%
@yak/react (styled API)1,672 req/s ±1%
@yak/react (styled API) foldStatic: false880 req/s ±0%
vanilla (hand-written ceiling)770 req/s ±1%
Bamboo680 req/s ±1%
cn (shadcn-ui)637 req/s ±2%
vanilla Solid (hand-written ceiling)607 req/s ±0%
StyleX without CSS layers (:not() specificity hack)588 req/s ±2%
StyleX564 req/s ±3%
styled-components525 req/s ±2%
cn (shadcn-ui) on Solid495 req/s ±0%
@yak/react (css prop)487 req/s ±1%
@yak/react (css prop) foldStatic: false486 req/s ±2%
StyleX on Solid452 req/s ±2%
Panda (style props)410 req/s ±1%
tailwind-merge349 req/s ±1%
Emotion305 req/s ±0%
Panda (css fn)273 req/s ±1%
Goober236 req/s ±2%

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

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

react-dom / solid (the framework floor)styling library runtimeyour componentnode / gc / unattributed
@yak/solid (styled API, Solid 2) foldStatic: false0.54 ms
@yak/solid (styled API, Solid 2)0.64 ms
@yak/react (styled API)1.07 ms
Bamboo1.41 ms
vanilla (hand-written ceiling)1.44 ms
@yak/react (styled API) foldStatic: false1.50 ms
vanilla Solid (hand-written ceiling)1.59 ms
cn (shadcn-ui)1.67 ms
cn (shadcn-ui) on Solid1.71 ms
StyleX1.83 ms
StyleX without CSS layers (:not() specificity hack)1.84 ms
@yak/react (css prop)2.44 ms
Panda (style props)2.68 ms
@yak/react (css prop) foldStatic: false2.74 ms
tailwind-merge2.76 ms
StyleX on Solid2.98 ms
styled-components3.10 ms
Panda (css fn)3.75 ms
Emotion4.13 ms
Goober4.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)47 ms ±3%
@yak/solid (styled API, Solid 2)54 ms ±6%
@yak/solid (styled API, Solid 2) foldStatic: false55 ms ±4%
@yak/react (styled API) foldStatic: false59 ms ±4%
vanilla (hand-written ceiling)63 ms ±4%
styled-components66 ms ±3%
Bamboo66 ms ±6%
StyleX66 ms ±3%
StyleX without CSS layers (:not() specificity hack)67 ms ±2%
cn (shadcn-ui)70 ms ±4%
@yak/react (css prop)71 ms ±1%
@yak/react (css prop) foldStatic: false74 ms ±3%
vanilla Solid (hand-written ceiling)79 ms ±4%
Panda (style props)83 ms ±2%
Emotion86 ms ±5%
cn (shadcn-ui) on Solid90 ms ±4%
tailwind-merge90 ms ±4%
StyleX on Solid92 ms ±3%
Panda (css fn)94 ms ±4%
Goober99 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)6.62 ms active(22.39 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false7.03 ms active(30.30 ms span)
vanilla Solid (hand-written ceiling)10.15 ms active(23.35 ms span)
@yak/react (styled API)10.21 ms active(30.89 ms span)
StyleX on Solid11.10 ms active(17.14 ms span)
@yak/react (styled API) foldStatic: false12.01 ms active(40.84 ms span)
styled-components12.07 ms active(38.18 ms span)
@yak/react (css prop) foldStatic: false12.13 ms active(29.20 ms span)
Bamboo12.41 ms active(39.39 ms span)
@yak/react (css prop)12.48 ms active(24.59 ms span)
StyleX12.94 ms active(30.76 ms span)
cn (shadcn-ui) on Solid13.53 ms active(32.86 ms span)
Goober13.54 ms active(24.40 ms span)
Emotion13.57 ms active(31.17 ms span)
vanilla (hand-written ceiling)13.90 ms active(34.97 ms span)
StyleX without CSS layers (:not() specificity hack)14.47 ms active(26.11 ms span)
Panda (style props)14.97 ms active(29.63 ms span)
Panda (css fn)15.50 ms active(29.67 ms span)
tailwind-merge21.40 ms active(27.90 ms span)
cn (shadcn-ui)21.86 ms active(33.08 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)53 ms ±6%
@yak/solid (styled API, Solid 2) foldStatic: false56 ms ±5%
vanilla Solid (hand-written ceiling)58 ms ±2%
@yak/react (styled API)59 ms ±6%
StyleX on Solid59 ms ±3%
cn (shadcn-ui) on Solid60 ms ±5%
@yak/react (styled API) foldStatic: false62 ms ±7%
cn (shadcn-ui)62 ms ±2%
Bamboo63 ms ±3%
vanilla (hand-written ceiling)64 ms ±4%
styled-components65 ms ±6%
StyleX65 ms ±5%
StyleX without CSS layers (:not() specificity hack)65 ms ±2%
tailwind-merge65 ms ±4%
@yak/react (css prop)68 ms ±7%
@yak/react (css prop) foldStatic: false68 ms ±3%
Panda (style props)72 ms ±4%
Emotion73 ms ±2%
Panda (css fn)73 ms ±5%
Goober89 ms ±4%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
cn (shadcn-ui) on Solid1.42 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
Bamboo1.48 ms active(33.39 ms span · 33.39 ms profiled median (n=5))
Panda (style props)1.80 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
StyleX without CSS layers (:not() specificity hack)1.85 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
StyleX on Solid1.93 ms active(33.28 ms span · 33.28 ms profiled median (n=5))
tailwind-merge1.95 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
@yak/react (styled API) foldStatic: false1.98 ms active(33.37 ms span · 33.37 ms profiled median (n=5))
styled-components2.03 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
@yak/react (styled API)2.05 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
Emotion2.18 ms active(33.34 ms span · 33.34 ms profiled median (n=5))
@yak/solid (styled API, Solid 2)2.25 ms active(33.36 ms span · 33.36 ms profiled median (n=5))
cn (shadcn-ui)2.25 ms active(33.27 ms span · 33.27 ms profiled median (n=5))
@yak/react (css prop) foldStatic: false2.25 ms active(33.32 ms span · 33.32 ms profiled median (n=5))
StyleX2.29 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
@yak/react (css prop)2.46 ms active(33.30 ms span · 33.30 ms profiled median (n=5))
@yak/solid (styled API, Solid 2) foldStatic: false2.64 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Goober2.85 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
vanilla Solid (hand-written ceiling)2.92 ms active(33.33 ms span · 33.33 ms profiled median (n=5))
Panda (css fn)3.07 ms active(33.31 ms span · 33.31 ms profiled median (n=5))
vanilla (hand-written ceiling)3.15 ms active(33.38 ms span · 33.38 ms profiled median (n=5))

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

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

@yak/solid (styled API, Solid 2)33 ms ±6%
@yak/solid (styled API, Solid 2) foldStatic: false38 ms ±4%
@yak/react (styled API)50 ms ±3%
vanilla Solid (hand-written ceiling)59 ms ±3%
@yak/react (styled API) foldStatic: false62 ms ±5%
vanilla (hand-written ceiling)65 ms ±5%
Bamboo68 ms ±3%
cn (shadcn-ui) on Solid69 ms ±4%
StyleX on Solid71 ms ±4%
StyleX72 ms ±4%
StyleX without CSS layers (:not() specificity hack)73 ms ±4%
cn (shadcn-ui)75 ms ±2%
styled-components78 ms ±5%
@yak/react (css prop)78 ms ±6%
@yak/react (css prop) foldStatic: false78 ms ±4%
Panda (style props)89 ms ±2%
Emotion93 ms ±4%
tailwind-merge94 ms ±5%
Panda (css fn)98 ms ±3%
Goober105 ms ±2%
JavaScriptstylelayoutpaintGCbrowser / otheridle / frame wait
@yak/solid (styled API, Solid 2)10.47 ms active(21.02 ms span)
@yak/solid (styled API, Solid 2) foldStatic: false10.59 ms active(27.23 ms span)
vanilla Solid (hand-written ceiling)11.62 ms active(23.83 ms span)
StyleX on Solid12.81 ms active(30.70 ms span)
cn (shadcn-ui) on Solid14.11 ms active(15.61 ms span)
@yak/react (styled API)16.23 ms active(32.02 ms span)
@yak/react (styled API) foldStatic: false16.50 ms active(28.10 ms span)
Bamboo17.46 ms active(28.75 ms span)
@yak/react (css prop) foldStatic: false18.02 ms active(39.69 ms span)
StyleX18.30 ms active(40.75 ms span)
@yak/react (css prop)18.54 ms active(36.09 ms span)
StyleX without CSS layers (:not() specificity hack)19.12 ms active(42.51 ms span)
vanilla (hand-written ceiling)19.48 ms active(33.00 ms span)
Goober20.04 ms active(43.76 ms span)
Panda (css fn)20.23 ms active(33.22 ms span)
cn (shadcn-ui)20.24 ms active(39.53 ms span)
Panda (style props)20.66 ms active(34.47 ms span)
Emotion22.09 ms active(46.39 ms span)
styled-components22.94 ms active(47.79 ms span)
tailwind-merge24.81 ms active(48.19 ms span)

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

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

style recalclayoutpaint (Chrome)
cn (shadcn-ui)Chrome4.7 ms1 recalc
Firefox1.0 ms
cn (shadcn-ui) on SolidChrome4.4 ms1 recalc
Firefox1.0 ms
EmotionChrome4.6 ms1 recalc
Firefox2.0 ms
GooberChrome4.7 ms1 recalc
Firefox1.0 ms
@yak/react (styled API)Chrome4.5 ms1 recalc
Firefox1.0 ms
@yak/react (css prop)Chrome4.8 ms1 recalc
Firefox2.0 ms
@yak/react (css prop) foldStatic: falseChrome4.7 ms1 recalc
Firefox1.0 ms
@yak/react (styled API) foldStatic: falseChrome4.4 ms1 recalc
Firefox1.0 ms
Panda (css fn)Chrome4.7 ms1 recalc
Firefox1.0 ms
Panda (style props)Chrome4.9 ms1 recalc
Firefox1.0 ms
styled-componentsChrome4.5 ms1 recalc
Firefox1.0 ms
StyleX without CSS layers (:not() specificity hack)Chrome5.1 ms1 recalc
Firefox1.0 ms
StyleXChrome4.5 ms1 recalc
Firefox2.0 ms
StyleX on SolidChrome4.5 ms1 recalc
Firefox1.0 ms
tailwind-mergeChrome4.5 ms1 recalc
Firefox2.0 ms
vanilla (hand-written ceiling)Chrome4.7 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2) foldStatic: falseChrome4.7 ms1 recalc
Firefox1.0 ms
@yak/solid (styled API, Solid 2)Chrome4.7 ms1 recalc
Firefox2.0 ms
BambooChrome4.8 ms1 recalc
Firefox2.0 ms
vanilla Solid (hand-written ceiling)Chrome4.8 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,440 B(0.5 / 0.2 / 2.7 kB)
@yak/react (css prop)3,464 B(0.5 / 0.2 / 2.7 kB)
@yak/react (styled API)4,164 B(1.2 / 0.2 / 2.7 kB)
@yak/react (styled API) foldStatic: false4,294 B(1.3 / 0.2 / 2.7 kB)
vanilla Solid (hand-written ceiling)5,673 B(0.0 / 0.4 / 5.1 kB)
Bamboo6,311 B(2.8 / 0.3 / 3.1 kB)
StyleX6,816 B(3.5 / 0.3 / 2.9 kB)
StyleX without CSS layers (:not() specificity hack)6,817 B(3.5 / 0.3 / 2.9 kB)
Goober7,123 B(3.9 / 0.2 / 2.8 kB)
Panda (css fn)8,417 B(4.8 / 0.2 / 3.2 kB)
StyleX on Solid11,954 B(5.9 / 0.3 / 5.5 kB)
@yak/solid (styled API, Solid 2) foldStatic: false12,246 B(6.5 / 0.2 / 5.2 kB)
@yak/solid (styled API, Solid 2)12,852 B(7.1 / 0.2 / 5.2 kB)
Panda (style props)12,944 B(9.2 / 0.2 / 3.2 kB)
tailwind-merge14,486 B(10.4 / 0.7 / 3.1 kB)
Emotion15,949 B(12.9 / 0.2 / 2.4 kB)
styled-components16,784 B(13.1 / 0.3 / 2.9 kB)
cn (shadcn-ui)17,024 B(12.9 / 0.7 / 3.1 kB)
cn (shadcn-ui) on Solid19,310 B(12.4 / 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.05.01015201005001,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)86 msnone · warm 85 ms
Plumeria on Solid119 msatomic · warm 116 ms
cn (shadcn-ui) on Solid123 msutility · warm 121 ms
vanilla (hand-written ceiling)123 msnone · warm 121 ms
Goober128 msruntime · warm 122 ms
styled-components135 msruntime · warm 132 ms
Emotion137 msruntime · warm 126 ms
@yak/solid (styled API, Solid 2)140 msextracted · warm 152 ms
Plumeria (classStyle prop)145 msatomic · warm 152 ms
tailwind-merge152 msutility · warm 157 ms
cn (shadcn-ui)157 msutility · warm 152 ms
@yak/react (styled API) foldStatic: false164 msextracted · warm 163 ms
@yak/react (styled API)165 msextracted · warm 180 ms
@yak/solid (styled API, Solid 2) foldStatic: false166 msextracted · warm 154 ms
@yak/react (css prop) foldStatic: false181 msextracted · warm 177 ms
@yak/react (css prop)183 msextracted · warm 176 ms
Panda (recipe)376 msatomic · warm 379 ms
Panda (css fn)402 msatomic · warm 401 ms
Panda (style props)414 msatomic · warm 407 ms
StyleX on Solid565 msatomic · warm 582 ms
StyleX without CSS layers (:not() specificity hack)573 msatomic · warm 585 ms
StyleX579 msatomic · warm 565 ms
Bamboo1,029 msatomic · warm 1,023 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.