dx-styles 1.5.0 is out. It requires wyw-in-js 2.7.1, and that is where the speed comes from. On a generated design-system scene with 1,308 recipe variants, going from dx-styles 1.4.1 on wyw-in-js 2.4.4 to 1.5.0 on 2.7.1 takes a warm production build from 1.34 s to 0.68 s (−49%). With all styles in one module, it goes from 1.79 s to 0.78 s (−56%).

On wyw-in-js 2.7.1, dx-styles 1.4.1 and 1.5.0 build byte-identical bundles in the same time. If you already updated your bundler integration, you have had this speed for a while; 1.5.0 makes 2.7.1 the minimum.

In July I promised a build-time benchmark with numbers you can rerun rather than take on faith. It ships with this release: dx-styles/benchmarks. It builds the same scene with dx-styles, eight other libraries and plain CSS Modules, and the results include the cases where dx-styles does not win.

Where the speed comes from

dx-styles is a set of processors on top of wyw-in-js, the engine that also runs Linaria. A build evaluates your style declarations, writes the CSS and leaves class names in the JavaScript, and most of that time is spent in the engine.

Two releases did most of the work, and the two layouts of the scene respond to them differently.

wyw-in-js 2.5 made parsing cheaper on large projects: the parser hands its AST over without a JSON round trip, and parse results are shared between files. When the styles are spread over 41 modules, this is the bigger step, from 1.35 s to 0.91 s.

wyw-in-js 2.6 stopped several passes from doing their work once per styled declaration. CSS extraction used to copy the rules collected so far for every processor in a file, so its cost grew with the square of the number of styles; now the rules are merged in place, and the Stylis plugin chain is built once per file. The check of whether processor tags are referenced walked the whole program once per tag and now walks it once. With all styles in one module, this step is the larger one, from 1.34 s to 0.78 s, as you would expect from a cost that grew with the number of styles per file.

wyw-in-js 2.4 takes off 6 to 8%. 2.7 fixes an exponential loop check on deep module graphs, which does not show on this scene because its graph is shallow. The release also picks up the correctness fixes of wyw-in-js 2.5 to 2.7, and the changelog lists the ones that reach dx-styles builds.

Build time by wyw-in-js release

dx-styles wyw-in-js Per-component modules, s One styles module, s
1.3.0 2.3.0 1.43 1.94
1.3.0 2.4.4 1.35 1.78
1.3.0 2.5.1 0.91 1.34
1.3.0 2.6.0 0.68 0.78
1.3.0 2.7.1 0.68 0.78
1.4.1 2.4.4 1.34 1.79
1.4.1 2.7.1 0.68 0.78
1.5.0 2.7.1 0.68 0.78

The series uses dx-styles 1.3.0, the one release whose range accepts every wyw-in-js from 2.3.0 to 2.7.1; the last three rows are the actual upgrade path. Each number is the median of 7 warm builds. The entries alternate forward and backward in every round, so a slow minute on the machine hits all of them alike. Every step except the one from 2.6.0 to 2.7.1 is outside the noise, since the ranges of the runs do not overlap, and each dx-styles release builds a byte-identical bundle on every wyw-in-js release it was built with.

The same scene in other libraries

Library Warm build, s Total gzip, KB First paint, ms
styled-components 6.6.0 0.16 97.2 208
Emotion 11.14 0.17 93.5 212
Panda 2.4.0 0.21 89.2 188
next-yak 9.10.3 0.30 96.3 168
vanilla-extract 1.21.3 0.56 94.9 160
CSS Modules (no library) 0.66 102.9 156
dx-styles 1.5.0 0.68 99.4 162
Linaria 8.3.0 1.86 105.3 160
StyleX 0.19.1 2.06 89.5 168
Compiled 2.0.0 5.93 88.7 168

styled-components and Emotion build fastest because they do no CSS work at build time. Their styles travel in the JavaScript and are injected at runtime, which is also why they paint about 45 to 50 ms later than dx-styles. Of the libraries that extract CSS at build time, dx-styles builds in about the time of plain CSS Modules, which have no CSS-in-JS layer at all; the 0.02 s between them is noise.

Linaria runs on the same engine, wyw-in-js 2.7.1, and here it builds in 1.86 s because of how this port models tokens. Every style module interpolates tokens from a shared JS object, so the engine evaluates that object for each of the 41 modules. dx-styles declares its tokens once, as a contract that its processors resolve without evaluating anything. Written with static CSS variables instead, the same Linaria scene builds in 0.38 s, faster than dx-styles, so the dx-styles layer (typed token contracts and recipes) costs about 0.3 s on top of the engine on this scene.

The output of every library is also checked in a browser by comparing 22 computed properties of all 3,642 elements with the dx-styles render. All of them match except Compiled 2.0.0. With its default class hash, two different declarations, color: var(--tk-color-danger600) and color: var(--tk-color-info800), get the same atomic class, and 5 elements are painted the wrong color. Hash collisions are a known issue of the default hash. The opt-in collisionResistantHash option fixes this one with the same build time and 1.4 KB more gzip.

Where dx-styles does not win

  • Panda (0.21 s), next-yak (0.30 s), vanilla-extract (0.56 s) and Linaria with static CSS variables (0.38 s) build faster than dx-styles (0.68 s), and plain CSS Modules (0.66 s) build in the same time.
  • dx-styles ships 99.4 KB gzip in total, more than every other lane here except Linaria and plain CSS Modules. Its CSS is 382 KB raw against 314 KB for vanilla-extract, which hashes its token variable names. dx-styles keeps them readable (--tk-color-accent600) because they are the theming contract.
  • Its first paint (162 ms) is level with CSS Modules, vanilla-extract and Linaria at 156 to 160 ms.

How it was measured

The scene is generated from one description: 104 design tokens, 45 structural slots and 34 recipe families (buttons, tabs, badges, alerts, inputs, menu items and more) with 1,308 variants. It renders as an issue-tracker screen plus a gallery of every variant, 3,642 elements in all, with the styles in 41 modules. Each library has its own generator, which writes the scene with that library's token layer and API and builds it through its documented Vite integration.

  • Machine: Apple M3 Max (16 cores, 64 GB), macOS 26.6.2, Node 24.4.1, Vite 8.3.5, React 19.3.1; Chromium 156 for first paint.
  • Every library is its own project with exact pins and a lockfile. The whole wyw-in-js stack is pinned through npm overrides, and npm ls checks that each pinned package is installed once.
  • All projects are installed in one session. The harness then waits 90 s for the file-system indexing that a fresh install triggers, does a warm-up build of each, and runs 7 rounds. A round builds every project cold and then warm, in alternating order, and starts only when the machine is quiet. The tables give the median of the warm builds. Cold builds are at most about 0.1 s slower (Panda's includes its codegen); RESULTS.md has them, with the minimums.
  • First paint is measured in headless Chromium on a fast machine with no throttling, so a difference of a few milliseconds means nothing; only the gap of the runtime libraries holds.
  • It is one machine and one scene, and the scene is large and regular on purpose, which is not how every app looks. Build times are comparable only within one run.

To rerun it:

git clone https://github.com/dx-styles/benchmarks cd benchmarks npm ci npx playwright install chromium npm run bench 

A full run takes about an hour. If a library is set up unfairly, or a scenario you care about is missing, open a pull request.

Upgrading

npm install dx-styles@^1.5.0 npm install -D @wyw-in-js/vite@2.7.1 

With Next.js, update @wyw-in-js/nextjs to 2.7.1 instead; with another bundler, update its wyw-in-js integration. Update the integration together with dx-styles: the build runs the transform that the integration installs, and dx-styles 1.5.0 needs wyw-in-js 2.7.1 or newer.

Anton Evzhakov maintains Linaria and is the author of wyw-in-js, the zero-runtime engine behind Linaria and MUI's Pigment CSS. He builds dx-styles.