Skip to content

🍕 vite: compile CSS with Lightning CSS across worker threads - #277

Open
jjpaulino wants to merge 6 commits into
masterfrom
jordan/vite-lightningcss
Open

jjpaulino wants to merge 6 commits into
masterfrom
jordan/vite-lightningcss

Conversation

@jjpaulino

Copy link
Copy Markdown
Member

What

The clay vite styles step keeps the PostCSS source plugins (postcss-import, postcss-mixins, postcss-simple-vars, postcss-nested, then any stylesConfig.plugins). It then runs Lightning CSS for vendor prefixes and minification, replacing autoprefixer + cssnano, and compiles files across worker threads.

On nymag/sites (2,872 styleguide files, Node 24, CLAYCLI_COMPILE_MINIFIED=true), clay vite --only styles takes:

wall time
before (autoprefixer + cssnano) 16.7–18.0 s
after, main thread (CLAYCLI_VITE_CSS_WORKERS=0) 5.5 s
after, workers (default, up to 8) 2.6 s

Output is 0.3% smaller (0.4% gzipped).

Behavior changes

  • Targets come from the project's browserslist (.browserslistrc / package.json, or stylesConfig.browserslist). Lightning CSS treats them as a floor: once every target supports newer syntax it prints that syntax and drops fallbacks. A usage share like > 2% would make nymag/sites print 9,828 range media queries (@media (width>=768px)), which Safari < 16.4 and Chrome < 104 ignore. autoprefixerOptions (including overrideBrowserslist) is therefore ignored, with a warning.
  • Invalid CSS fails the build. CSS that Lightning CSS cannot parse is reported at its source file and line (mapped back through @import, mixins and nesting), once per location with a count of affected output files. Every file is still compiled and written; then the styles step fails so clay vite exits non-zero (--watch keeps running). Files PostCSS cannot compile, such as a missing @import, now fail the build too instead of only being logged.
  • stylesConfig keeps working: it re-runs inside each worker, since plugin instances can't cross threads. It gains browserslist and lightningcssOptions.
  • Worker threads: one per core minus one, up to 8. Builds under 200 files and watch rebuilds of a changed component compile on the main thread. CLAYCLI_VITE_CSS_WORKERS=<n> overrides the count.
  • clay compile (Browserify) is unchanged.

Lightning CSS 1.33 pitfalls

CLAY-VITE.md lists the valid CSS patterns Lightning CSS mishandles and how to write them instead:

  • fit-content followed by -moz-fit-content;
  • an !important lost when non-adjacent same-selector rules merge;
  • font printed after properties it resets;
  • font-stretch printed as a percentage inside font;
  • unitless lengths read as px;
  • mixed-prefix selector lists being split.

claycli doesn't patch around them; the sites CSS is fixed instead (companion sites PR).

Verification

On nymag/sites, the companion branch's CSS was built with this pipeline and compared against today's production output (rc.10):

  • Each output file was parsed by Chrome's CSS parser and compared fact by fact.
  • The computed styles of every element on 21 local pages were compared at 360, 768, 1024 and 1280 px.

Result: 0 unexplained differences, and page heights are equal everywhere. The remaining differences are equivalent spellings (translate(-50%) for translateX(-50%), 0 vs 0%, :is() merges with equal specificity) plus the sites CSS changes listed in that PR. npm test passes (602 tests).

The styles step ran autoprefixer and cssnano on the main thread; cssnano was
about two thirds of its time. Keep postcss-import, postcss-mixins,
postcss-simple-vars and postcss-nested, then run Lightning CSS for vendor
prefixes and minification, and compile files across worker threads (one per
core minus one, up to 8; the main thread for small builds and watch
rebuilds). On nymag/sites (2,872 files) `clay vite --only styles` goes from
~17 s to 2.6 s (5.5 s on the main thread), with slightly smaller output.

Lightning CSS targets the project's browserslist (or stylesConfig
`browserslist`). It treats them as the oldest browsers to support: once all
of them support newer syntax it prints that syntax and drops fallbacks. So
autoprefixer's overrideBrowserslist (a usage share like '> 2%', which would
make it print range media queries Safari < 16.4 ignores) is no longer read;
autoprefixerOptions is ignored with a warning.

CSS that Lightning CSS cannot parse is an error at its source file and line.
Every file still compiles and is written, then the styles step fails so
`clay vite` exits non-zero (`--watch` keeps running); files PostCSS cannot
compile fail it too. stylesConfig keeps working (it re-runs in each worker)
and gains `browserslist` and `lightningcssOptions`; `plugins` still run
before Lightning CSS. CLAY-VITE.md documents the options and the CSS
patterns Lightning CSS 1.33 mishandles. clay compile is unchanged.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T22:57:53.414245Z b4649a0 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@jjpaulino jjpaulino self-assigned this Oct 22, 2026
@coveralls

coveralls commented Oct 22, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 85.572% (+0.02%) from 85.553% — jordan/vite-lightningcss into master

… package

.nvmrc and "engines" say 26.12.1 (the first 26.x with the October 2026
security fixes). CI runs one job that reads .nvmrc instead of a
Node 18/20/22 matrix, with the actions pinned by commit.

The coveralls npm package was only the old upload script; CI uploads
coverage with the Coveralls action. Removing it drops 38 packages from
the lockfile (the old `request` chain).
One worker only moves the work to another thread, and with a 2-CPU quota the
pool takes CPU from the js step (full sites build in a container: 44.9 s with 8
workers, 40.7 s on the main thread). Node 26 counts a container's CPU quota in
os.availableParallelism(), so no cgroup reading is needed.
…rary builds

buildDefines() read process.env.NODE_ENV for every pass, but Vite's build()
sets it to 'production' when it is unset, and the view and Kiln passes
configure in parallel with that. Whichever pass configured after Vite started
got 'production' and the other 'development', so which dom-mutator build (and
which other library guards) landed in which chunk depended on timing. The value
is now read when the config is read, before either pass starts.

With Vite 5 both passes happened to read it first; Vite 7 starts sooner, and the
Kiln edit bundle came out with a production dom-mutator while the view chunks
had the development one.
Vite 5 is unsupported. Vite 7 is ESM-only, which Node 26's require(esm) loads
for claycli's CommonJS code, so scripts.js now loads it on first use (Jest's
module VM cannot load ESM, and the config helpers need no Vite), and the
VITE_CJS_IGNORE_WARNING workaround for Vite 5/6's CJS build is gone.

Checked on the nymag/sites build with Node 26 (minified, as shipped): 318
chunks before and after, every chunk with the same set of modules, the same
manifest, byte-identical CSS, fonts and generated entries, total size +0.01%,
20.3 s against 20.9 s. 279 chunks are identical once hashed chunk names are
normalized; the other 39 differ in how esbuild 0.27 names variables, which also
gives most chunks a new hashed URL once. Built unminified (so the minifier is
out of the picture), 916 of 947 chunks are identical the same way and 28 of the
remaining 31 differ only in how parentheses are printed; the other 3 take Vite 7's
own changes: JSON over 10 kB becomes JSON.parse(), and the preload helper gains
a Promise.allSettled fallback. --watch rebuilds on edit.

The commonjs canary test still passes with Vite 7's bundled
@rollup/plugin-commonjs 29.0.x, so claycli's own copy is still needed. Its
test patched vite.build, which is a read-only namespace now; it hands scripts.js
a wrapped module instead.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants