Repository navigation
perf(compiler): return nothing from visitors that only read - #490
YevheniiKotyrlo wants to merge 2 commits into
Conversation
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.
Compile cost —
|
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}");
Problem
The compiler's second pass only reads - its output is discarded - but its
RuleandStyleSheetExitvisitors return the node they were given, and the first pass'sLengthvisitor 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()carriesfrom: nullandfallback: null, which those releases no longer deserialize (#470). So withinlineVariables: false, any stylesheet holding avar()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.tsrecords 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 onmain.Verification
main's with variables inlined, withinlineVariables: falseand withinlineRem: false.main, and a 76 KB Tailwind app stylesheet 62.9 ms against 117.2 ms, on Ubuntu with Node 22.18.0, wheremainagainst itself differs by under 2% (the measurement).inlineVariables: falsecompiles that stylesheet wheremainthrows. 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 twocalc(infinity * 1px)cases, which are Failed to deserialize infinity or big number when using visitors parcel-bundler/lightningcss#1188.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.