dx-styles 1.5.0: half the build time, and a benchmark you can rerun

2026-11-06 · Anton Evzhakov

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 warm build time by wyw-in-js release Line chart, two series. With per-component modules the build takes 1.43 s on wyw-in-js 2.3.0, 1.35 s on 2.4.4, 0.91 s on 2.5.1, 0.68 s on 2.6.0 and 0.68 s on 2.7.1. With one styles module it takes 1.94, 1.78, 1.34, 0.78 and 0.78 s. Both series drop most at 2.5.1 and 2.6.0 and stay flat from 2.6.0 to 2.7.1. One styles module Per-component modules (41) 0 s 0.5 1.0 1.5 2.0 s 2.3.0 2.4.4 2.5.1 2.6.0 2.7.1 wyw-in-js release · dx-styles 1.3.0 · median of 7 warm builds One styles module, wyw-in-js 2.3.0: 1.94 s One styles module, wyw-in-js 2.4.4: 1.78 s One styles module, wyw-in-js 2.5.1: 1.34 s One styles module, wyw-in-js 2.6.0: 0.78 s One styles module, wyw-in-js 2.7.1: 0.78 s Per-component modules, wyw-in-js 2.3.0: 1.43 s Per-component modules, wyw-in-js 2.4.4: 1.35 s Per-component modules, wyw-in-js 2.5.1: 0.91 s Per-component modules, wyw-in-js 2.6.0: 0.68 s Per-component modules, wyw-in-js 2.7.1: 0.68 s 1.94 s 1.43 s 0.78 s 0.68 s
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

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.

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.