CSS-in-JS Arena Bamboo, StyleX and Panda on Pixel-Identical Apps

Lobsters Hottest Tools

Summary

This article presents a benchmark harness for compile-time CSS-in-JS engines, comparing Bamboo, StyleX, and Panda CSS on pixel-identical React apps, with Bamboo leading in performance metrics like shipped bytes and build times.

<p>A little context, I've been working on Contra for the last 6 years. It's a marketplace-network – an application that's comparable to the likes of LinkedIn, UpWork, and similar in terms of its surface area. That's hundreds of routes, thousands of components, and tens of thousands of styles. Over the last year, we have been obsessed with performance. We have optimized every layer of our infrastructure to the point where profiling the application increasingly started to surface bottlenecks in client-side (bundle size, and metrics like TBT, LCP, and INP). That's where CSS-in-JS comes in.</p> <p>6 years ago, we started with styled-components. Then zero-runtime alternatives emerged and we started to experiment with them, eventually landing on Panda CSS. Panda took us a long way, but... they aren't <em>actually</em> zero-runtime. Panda extracts CSS at build time, but it uses ~15KB runtime to map those style objects at runtime. This overhead surfaced repeatedly when profiling pages with lots of components (server-side and client-side).</p> <p>That's where I ended up writing Bamboo to solve this.</p> <p>Bamboo folds styles at build-time achieving <em>near</em> zero-runtime (0.5KB vs 15 KB). If you write:</p> <pre><code>&lt;div className={css({ fontSize: 'lg', fontWeight: 'bold' })}&gt;Title&lt;/div&gt; </code></pre> <p>it becomes:</p> <pre><code>&lt;div className="fs_lg fw_bold"&gt;Title&lt;/div&gt; </code></pre> <p>at build time.</p> <p>If the variant is dynamic, then bundle is inlined with pre-computed map of classes.</p> <pre><code>&lt;span className={pick(status, { ok: "d_inline-flex px_8px bg_successSoft c_success", warn: "d_inline-flex px_8px bg_warningSoft c_warning", err: "d_inline-flex px_8px bg_dangerSoft c_danger", })} /&gt; </code></pre> <p>That's the main idea behind Bamboo.</p> <p>Thanks to folding, we were able to improve our server-side and client-side performance.</p> <p>I built CSS-in-JS Arena as a sanity benchmark to track how we compare to Panda, but also to any other emerging frameworks.</p> <p><a href="https://lobste.rs/s/wj7pei/css_js_arena_bamboo_stylex_panda_on_pixel">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 08/18/26, 04:21 PM

gajus/css-in-js-arena

Source: https://github.com/gajus/css-in-js-arena

CSS-in-JS Arena

A benchmark harness for compile-time CSS engines. Each engine gets its own React Router 8 app under apps/, and every app renders the same six-page admin console — identical markup, identical design, identical data. The apps are verified pixel-identical before anything is measured, so the numbers isolate the engine and nothing else.

EngineIntegrationVersion
Bamboo CSS@bamboocss/vite1.45.3
StyleX@stylexjs/unplugin0.19.0
Panda CSS@pandacss/postcss1.12.0

Measured 2026-08-18 at the versions above · macOS, Node 24.10, Vite 8.2.1

EngineShipped bytesBuild & devAuthoringCorrectness & maintenanceRows won 🏆
Bamboo 🏆8 / 10 🏆3 / 6 🏆7 / 8 🏆4 / 4 🏆22 / 28 🏆
StyleX3 / 101 / 62 / 81 / 47 / 28
Panda2 / 102 / 66 / 81 / 411 / 28

Rows won per category, out of the scored rows in each. They are not equally weighted and two are unscored, so the tally is a scanning aid rather than the judgement — and the byte margins do not survive scale, as the next section shows.


Full results

AxisBamboo 🏆StyleXPandaMargin
Shipped bytes
Full first load104,911 B 🏆106,117 B112,840 B−1.1% / −7.0%
CSS, brotli6,802 B 🏆7,008 B9,518 B−2.9% / −29%
CSS, gzip7,876 B 🏆8,176 B11,524 B−3.7% / −32%
CSS, raw37,227 B 🏆40,430 B54,007 B−7.9% / −31%
CSS rules emitted525467532not a quality axis
Client JS, brotli92,569 B 🏆93,583 B97,778 B−1.1% / −5.3%
SSR HTML, gzip (mean of 6)5,540 B5,526 B5,544 Btie — spread 0.3%
Class attribute bytes, raw93,036 B70,843 B 🏆92,738 B−24%
— on the selector-heavy route11,728 B 🏆11,754 B 🏆11,685 B 🏆tie — spread 0.6%
Unreachable CSS shipped0 B 🏆344 Bn/a — runtimeonly engine at zero
Orphan file in include, imported by nothing+7,200 B+0 B 🏆+7,200 Bonly StyleX scopes to the bundle graph
Stylesheets emitted1 🏆21 🏆StyleX emits an unreferenced duplicate
Build & dev
Production build, cold1,564 ms 🏆2,336 ms1,611 ms−2.9% / −33%
Production build, warm1,591 ms 🏆2,293 ms1,704 ms−6.6% / −31%
Dev server cold start1,725 ms1,434 ms1,300 ms 🏆−9.3% / −25%
HMR — edit a shared style module213 ms103 ms 🏆183 ms−44% / −52%
HMR — edit a component file189 ms229 ms118 ms 🏆−38% / −48%
HMR payload, one shared edit336 KB · 9 🏆356 KB · 10402 KB · 9−5.5% / −16%
Authoring
Total lines written3,921 🏆4,0903,930 🏆Bamboo ≡ Panda (0.2%); StyleX +4.3%
Structural & relational selectorsone rule on the container 🏆class per cell, last in JSone rule on the container 🏆StyleX styles only its own element
Next-sibling selector (+)yes 🏆~ only, via when + a markeryes 🏆StyleX has no adjacency form
Variant recipescva, typed matrix 🏆compose per call sitecva, typed matrix 🏆StyleX has no equivalent
Light/dark theming2 values per token 🏆3 values per token4 values per tokenhalf of Panda’s
Dynamic valuesinline stylecustom property 🏆inline styleothers break the cascade
Register an @propertyglobal.vars 🏆stylex.types.* 🏆globalVars 🏆all three; not via globalCss
Animate a registered propertyyes 🏆declaration droppedyes 🏆StyleX emits neither the keyframe nor the rule
Correctness & maintenance
Mistyped token namebuild fails 🏆TS error, build succeedsnot caught at allonly engine that fails the build
Mistyped property namecaught (TS2561) 🏆ships pading-blockcaught (TS2561) 🏆StyleX ships it
Delete a page → CSS shrinks−20.1% 🏆−8.4%−13.5%reclaims the most
Class names folded to literals522 / 522 🏆453 / 458 🏆25 / 529Panda computes the rest in the browser, from a 14.7 KB runtime chunk
Rows won 🏆22711of 28 scored

Where the main table doesn’t generalise

Everything above is one app in one configuration. Two things move the answer: how many styles the app has, and whether it ships more than light and dark.

Style volume

The main table describes an app of 549 rule blocks. Real applications run an order of magnitude past that, where fixed overhead stops mattering and marginal cost per rule becomes everything.

tools/scale.mjs generates N style definitions with all-distinct values and measures the emitted stylesheet, isolating each engine’s true cost per rule.

Downloaded stylesheet, brotli, relative to Bamboo:

Style definitionsBambooStyleXPanda
0 — the app as it shipsref+3.0%+39.9%
50ref+18.0%+35.4%
200ref+50.2%+28.0%
800ref+102.9%+16.3%

Both challengers move, in opposite directions. The ranking at the arena’s size is not the ranking at production size.

Marginal cost per declarationGap to Bamboo at n=0at n=800
Bamboo40.3 B raw · 2.0 B brotlirefref
Panda40.3 B raw · 2.0 B brotli+16,903 B+16,903 B
StyleX65.8 B raw · 5.5 B brotli+4,222 B+126,621 B

Panda’s marginal cost is identical to Bamboo’s, to the byte. Its whole penalty is a fixed 16,903 B of scaffolding — the same constant at 0 styles and at 800 — so “Panda ships 40% more CSS” is a statement about a small app, not about Panda. StyleX is the reverse: almost pure slope, growing 30× across the same range, because every rule carries :not(#\#) specificity padding that repeats per declaration and compresses poorly.

Theming

Brand themes are a different question from light/dark, and the arena app ships none — so this is measured separately. tools/theming.mjs injects N themes through each engine’s own multi-theme API (Bamboo theme.variants, Panda themes, StyleX createTheme), each overriding the same 18 colours with a light and a dark value.

Stylesheet the browser downloads, brotli:

Brand themesBambooStyleXPanda
06,802 B7,008 B9,518 B
26,802 B7,427 B9,518 B
86,802 B8,350 B9,518 B
added per theme0 B 🏆+168 B0 B 🏆

Theme payload, fetched only when a theme is selected:

AxisBambooStyleXPandaMargin
Bytes per theme1,374 B 🏆n/a — in the stylesheet2,805 B−51%
Themes in the critical pathnone 🏆all of themnone 🏆StyleX has no lazy option

Two mechanisms, not three. Bamboo and Panda emit each theme as its own artifact, imported on demand, so first load is flat however many exist. StyleX’s createTheme compiles into the linked stylesheet, so every visitor pays for every theme — at eight, its CSS is 19% larger than at zero. Between the two lazy engines the gap is the same encoding difference as the light/dark row: Bamboo writes base and _osDark and lets light-dark() resolve the rest, Panda writes four values. That is 2.04× the bytes per theme.

This reverses for a site that ships one fixed brand theme and never switches: a lazy artifact is then a second request for bytes the stylesheet would have carried anyway.


What’s measured

Ground rules

  • One reference app. apps/bamboo is the reference; every other is diffed against it, so all match each other transitively — element for element, then pixel for pixel.
  • Shared source is byte-identical. data.ts, icons.tsx and chart-utils.ts are the same bytes in every app.
  • Same baseline reset. Engines that ship one use theirs; engines that do not vendor Bamboo’s preflight verbatim, so nobody gets a typography head start.
  • Default configuration only. Each engine is measured as it ships. Opt-in settings are reported separately, never folded into the main table.

Pages

Every app renders these six routes identically:

RouteWhat it exercises
/KPI grid, SVG bar chart + sparklines, activity feed, responsive 2-col dashboard
/projectsData table, status badges, progress bars, toolbar, pagination
/settingsSticky section nav, 2-col form grid, validation states, toggle switches, radio cards, danger zone, sticky save bar
/pricingFeatured pricing cards, billing toggle, comparison table, <details> FAQ
/docs3-column docs layout, prose typography, code block, callouts, table, TOC
/labStructural + relational selectors, keyframe motion, container queries

All six are responsive across three breakpoints and support system dark mode plus an explicit light/dark toggle.


Reproducing this

Every number here comes from one contiguous measurement session on one machine. The harness, the parity gate and the exact commands are in RUNNING.md.


FAQ

Why is CSS minification disabled?

build.cssMinify: false in all three apps. Vite’s default runs Lightning CSS over the emitted stylesheet and rewrites it — most visibly downlevelling light-dark() into a 54-variable polyfill under the baseline-widely-available target. That measures the downleveller rather than the engine, and penalises only engines emitting modern CSS. With it off, every stylesheet measured here is exactly what its engine wrote.

StyleX still shows some Lightning CSS output because @stylexjs/unplugin depends on it directly — that is part of its product, not the harness.

Similar Articles

ReactBench v1 (14 minute read)

TLDR AI

ReactBench is a new evaluation benchmark for coding agents on realistic React work, going beyond passing tests to enforce React performance, accessibility, and quality via the open-source React Doctor verifier. Early results show top models solve fewer than half the tasks, with bugs being the most common newly introduced issue.

The CSS Zen Garden dream, finally shipped

Hacker News Top

The article details the rebuild of Firefox.com using modern, native CSS without preprocessors, showcasing the evolution from the CSS Zen Garden concept to practical, production-ready web development.

@GoSailGlobal: https://x.com/GoSailGlobal/status/2052573500800700560

X AI KOLs Timeline

SWE-WebDev Bench is a paper on arXiv that evaluated 6 mainstream vibe coding platforms (Lovable, Replit Agent3, Vercel v0-Max, Base44, Emergent E1-OPUS, QwikBuild). It found that all platforms scored below 60% on engineering composite metrics — their front-end UIs look great but back-end, security, and production readiness all collectively fail, requiring 12-60 hours of manual fixes before going live.

Show HN: A CSS 3D Engine (no WebGL)

Hacker News Top

PolyCSS is a CSS polygon mesh library that renders 3D models as real HTML elements using CSS matrix3d, supporting OBJ/MTL, GLB, and VOX formats with React, Vue, or vanilla JS.