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
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.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: false
Plumeria (classStyle prop)
Plumeria on SolidA 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
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.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: false
Plumeria (classStyle prop)
Plumeria on SolidA 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
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.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: false
Plumeria (classStyle prop)
Plumeria on SolidButton 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
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.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: false
Plumeria (classStyle prop)
Plumeria on SolidButton variantscomposed as JSX componentsn = 1,000low cardinality
The IDENTICAL ladder as button-variants — same declarations, same three modules, same rendered element — but each module exports a component instead of a style value: GhostPrimaryButton wraps GhostButton wraps Button. next-yak collapses the chain at construction, so three levels stay one React element; the runtime and utility lanes render three components and re-merge at every level. The gap to button-variants is what the component API costs, with the file layout held constant. The next-yak css-prop lanes have no cell here: the css prop resolves only top-scope values, so a style fragment cannot reach a component through a prop — that API composes fragments at build time or not at all.
Source · generated HTML · generated CSS · rendered preview
The css prop takes styles written in place. From the
next-yak docs: "Its value has to be styles written in place: a css template or an atoms() call and logical combinations of them" (Features, CSS Prop). The compiler folds those fragments onto the element where they are written and collapses them into one class on one host tag.
This case asks each module to export a component that extends the one below: GhostPrimaryButton wraps GhostButton wraps Button. A fragment cannot travel to another component through a prop, and one component cannot extend another, so the css prop has nothing to compose against. The styled API does that with styled(Button), which is the
next-yak lane's cell. Merging class names at runtime would be a workaround rather than the primitive.
The same three declarations as style values are in button-variants, where this API composes the ladder onto one element.
The css prop takes styles written in place. From the
next-yak docs: "Its value has to be styles written in place: a css template or an atoms() call and logical combinations of them" (Features, CSS Prop). The compiler folds those fragments onto the element where they are written and collapses them into one class on one host tag.
This case asks each module to export a component that extends the one below: GhostPrimaryButton wraps GhostButton wraps Button. A fragment cannot travel to another component through a prop, and one component cannot extend another, so the css prop has nothing to compose against. The styled API does that with styled(Button), which is the
next-yak lane's cell. Merging class names at runtime would be a workaround rather than the primitive.
The same three declarations as style values are in button-variants, where this API composes the ladder onto one element.
This case asks each module to export a component that extends the one below: GhostPrimaryButton wraps GhostButton wraps Button, each adding its own style and passing the accumulated styles down.
Plumeria lets a style value cross one component boundary, but the receiving component has to apply it to an element; relaying a received classStyle to another component is a build error, because composition is resolved where the styles are written.
The same three declarations as style values are in button-variants, where classStyle composes the ladder onto one element. Joining class strings at runtime here would bypass
Plumeria's conflict resolution, so the lane has no cell. Source: the lane author's note on the pull request.
This case asks each module to export a component that extends the one below: GhostPrimaryButton wraps GhostButton wraps Button, each adding its own style and passing the accumulated styles down.
Plumeria lets a style value cross one component boundary, but the receiving component has to apply it to an element; relaying a received classStyle to another component is a build error, because composition is resolved where the styles are written.
The same three declarations as style values are in button-variants, where classStyle composes the ladder onto one element. Joining class strings at runtime here would bypass
Plumeria's conflict resolution, so the lane has no cell. Source: the lane author's note on the pull request.
SSR render throughput — renders / sec · higher is betteriUses a microbenchmark to measure how fast Node turns components into an HTML string after warmup. Measures rendering only, excluding build time, HTTP handling, response transfer and browser work. Results count component instances per second: a workload of 400 product tiles counts as 400 renders. Higher is better.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: falseA real Tabs component150 groupsn = 150low cardinality
A real design-system Tabs: responsive typography, the full active/hover/focus-visible/disabled state matrix, an animated active underline via CSS anchor positioning (with a per-tab ::after fallback gated on @supports), a ::before WCAG tap target, hidden-scrollbar overflow and a composed FullWidthTabs wrapper. Tailwind needs a ~40-token list per tab; next-yak compiles it all at build time.
Source · generated HTML · generated CSS · rendered preview
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.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: false
Plumeria (classStyle prop)
Plumeria on SolidMulti-file compositionimported styled primitivesn = 150low cardinality
The EXACT Tabs workload — identical DOM, identical CSS — but the styled primitives are moved to an imported parts.tsx module, the way a design system ships components. Per-module compile-time optimizations such as next-yak's JSX folding cannot see across the module boundary, while runtime libraries do not care about file layout. Any gap between this case and tabs is the cost of that boundary.
Source · generated HTML · generated CSS · rendered preview
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.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: false
Plumeria (classStyle prop)
Plumeria on SolidDynamic valuetranslateX (written directly)n = 1,000high cardinality
1,000 elements each with a unique translateX, a value no build step can know, such as a position from a database record or from a drag-and-drop interaction, written directly: it goes straight into the style declaration, the way each library's API takes it. styled-components emits a CSS rule per value, while next-yak turns it into a CSS variable, so its per-instance work stays constant. The build-time lanes have no cell here: Tailwind's scanner, Panda's extractor and Bamboo's compiler resolve names ahead of time and emit no rule for a render-time value. Their working path, an inline style, is what dyn-fair measures.
Source · generated HTML · generated CSS · rendered preview
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=ordata-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.
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.
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).
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.
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.
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.
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.
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.
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.
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: false
Plumeria (classStyle prop)
Plumeria on SolidDynamic valuetranslateX (each lane's best practice)n = 1,000high cardinality
The same 1,000-unique-translateX workload as the direct case, but idiomatic. The value is one no build step can know, such as a position from a database record or from a drag-and-drop interaction. Lanes whose styles are fixed at build time pass it as an inline style over a static class, their documented answer to such values; StyleX uses its dynamic style function. The next-yak lanes write the same plain inline style here for comparison only: their own path, the interpolation compiled to a CSS variable, is what dyn-translate measures, so the gap between the two cases for those lanes is what that path costs. Compare with dyn-translate to see what the direct pattern costs each ecosystem.
Source · generated HTML · generated CSS · rendered preview
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.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: false
Plumeria (classStyle prop)
Plumeria on SolidVariant / 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
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.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
Panda (recipe)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: false
Plumeria (classStyle prop)
Plumeria on SolidComposition1 level (control)n = 1,000low cardinality
The compose-3 button family at depth 1: one styled component carrying only the base styles, no wrapper chain. Brackets compose-3 from below (compose-6 brackets it from above) to chart how per-element cost grows with composition depth — the boundary that defeats next-yak's JSX folding.
Source · generated HTML · generated CSS · rendered preview
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.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: false
Plumeria (classStyle prop)
Plumeria on SolidComposed components (3 levels)n = 1,000low cardinality
A Button wrapped by two more components, each adding styles and threading className down. next-yak flattens the chain at build time (depth ≈ free); the Tailwind lanes pay one merge per level and styled-components/Emotion run a wrapper component at each.
Source · generated HTML · generated CSS · rendered preview
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.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: falseComposition6 levelsn = 1,000low cardinality
The compose-3 button family at depth 6: the same base button wrapped five times, each level adding one small border-left/padding-left declaration. Brackets compose-3 from above (compose-1 brackets it from below) to chart how per-element cost grows with composition depth — the boundary that defeats next-yak's JSX folding.
Source · generated HTML · generated CSS · rendered preview
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.
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.
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).
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.
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.
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.
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.
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.
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.
Bamboo
cn (shadcn-ui)
cn (shadcn-ui) on Solid
Emotion
Goober
next-yak (styled API)
next-yak (css prop)
next-yak (css prop) foldStatic: false
next-yak (styled API) foldStatic: false
Panda (css fn)
Panda (style props)
styled-components
StyleX without CSS layers (:not() specificity hack)
StyleX
StyleX on Solid
tailwind-merge
vanilla (hand-written ceiling)
vanilla Solid (hand-written ceiling)
@yak/solid (styled API, Solid 2)
@yak/solid (styled API, Solid 2) foldStatic: falseBuild 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.
How this was measured
- microbench — an in-process Node loop that renders each workload to an HTML string (
renderToString) and counts instance renders per second. - autocannon — 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.
- web-performance-debugger — records CPU and render profiles in Chrome, Firefox and Node and attributes the time to libraries and functions through source maps.
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.