Skip to content

perf(compiler): return nothing from visitors that only read - #490

Open
YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:perf/read-only-visitors
Open

YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:perf/read-only-visitors

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The compiler's second pass only reads - its output is discarded - but its Rule and StyleSheetExit visitors return the node they were given, and the first pass's Length visitor returns every length it does not convert. lightningcss deserializes a returned node back into its AST, so every compile reads the whole stylesheet back from JS once more.

On lightningcss 1.30.2 and later that read also fails: a returned var() carries from: null and fallback: null, which those releases no longer deserialize (#470). So with inlineVariables: false, any stylesheet holding a var() throws.

Solution

The three visitors return a node only when they changed it, so lightningcss keeps its own. The first pass's StyleSheetExit, which returns the inlined sheet, is unchanged.

Tests

src/__tests__/compiler/visitor-returns.test.ts records what each visitor hands back during a compile: the second pass returns nothing, and the first pass returns only the lengths it converts. Both fail on main.

Verification

  • The compiled output of a 1.17 MB Tailwind 4 stylesheet is byte-identical to main's with variables inlined, with inlineVariables: false and with inlineRem: false.
  • Compiling it takes 2.52 s against 4.93 s on main, and a 76 KB Tailwind app stylesheet 62.9 ms against 117.2 ms, on Ubuntu with Node 22.18.0, where main against itself differs by under 2% (the measurement).
  • On lightningcss 1.33.0, inlineVariables: false compiles that stylesheet where main throws. With variables inlined it still throws, from the inlined sheet: fix: deserialize null as None in visitor results parcel-bundler/lightningcss#1357 fixes that, and with it built on 1.33.0 every test that passes on 1.30.1 passes but the two calc(infinity * 1px) cases, which are Failed to deserialize infinity or big number when using visitors parcel-bundler/lightningcss#1188.
  • On Ubuntu with the workflow's steps - build, lint, typecheck, yarn test, node --test node/__tests__/*.mjs, both library builds, the web export, the unstaged-changes checks and commitlint - everything passes.

Not run: the iOS and Android example builds.

Refs #470.

The second pass's Rule and StyleSheetExit visitors and the first pass's
Length visitor returned the node they were given. A returned node is
deserialized back into lightningcss's AST, so every compile paid for
reading the whole stylesheet back from JS, and on lightningcss 1.30.2
and later a returned var() fails to deserialize at all.
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 16, 2026 12:29
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Compile cost — main against this PR

main — the second pass hands every rule and the stylesheet back to lightningcss, which reads the whole sheet back from JavaScript once more before the output is discarded.

This PR — a visitor returns a node only when it changed it, and the compile takes about half the time; the stylesheet it emits is byte-identical to main's for both inputs with variables inlined, with inlineVariables: false and with inlineRem: false.

compile() and stylesheet() main this PR
a 76 KB Tailwind 4 app stylesheet 117.2 ms (108.4–134.3) 62.9 ms (58.6–64.6)
a 1.17 MB Tailwind 4 stylesheet 4.93 s (4.65–5.27) 2.52 s (2.41–2.62)

Ubuntu 24.04 under WSL2, Node 22.18.0, lightningcss 1.30.1, each side built with yarn build. Each cell is the median of five interleaved rounds' process medians, with the range across them in brackets; a process ran 40 compiles after 10 warm-ups for the small sheet and 8 after 2 for the large one, and main was measured twice a round for the noise floor, 0.7 ms and 0.07 s.

The large stylesheet

Tailwind 4.1.13 over this input, 1,170,455 bytes out:

@import "tailwindcss";
@source inline("{,hover:,focus:,dark:}{bg,text,border,ring,outline,fill,stroke,shadow}-{red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose,slate,gray,zinc,neutral,stone}-{50,{100..900..100},950}");
@source inline("{,md:,lg:}{p,px,py,pt,pb,m,mx,my,mt,mb,gap,w,h,min-w,max-w,inset,top,left,translate-x,translate-y}-{0,1,2,3,4,5,6,8,10,12,16,20,24,32,40,48,64}");
@source inline("{,hover:}{rounded,rounded-t,rounded-b,shadow,blur,opacity,scale,rotate}-{none,sm,md,lg,xl,2xl,3xl,full,0,50,75,100,45,90}");

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.

1 participant