From 40f9df1300a3e54045b14e69a89b670d4d6a86d5 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 16 Oct 2026 15:21:41 +0300 Subject: [PATCH 1/2] perf(compiler): return nothing from visitors that only read 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. --- .../compiler/visitor-returns.test.ts | 77 +++++++++++++++++++ src/compiler/compiler.ts | 4 +- 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/compiler/visitor-returns.test.ts diff --git a/src/__tests__/compiler/visitor-returns.test.ts b/src/__tests__/compiler/visitor-returns.test.ts new file mode 100644 index 00000000..e254652e --- /dev/null +++ b/src/__tests__/compiler/visitor-returns.test.ts @@ -0,0 +1,77 @@ +import { compile } from "react-native-css/compiler"; + +interface VisitorCall { + pass: number; + visitor: string; + returned: unknown; +} + +const calls: VisitorCall[] = []; + +jest.mock("../../compiler/lightningcss-loader", () => { + const actual = jest.requireActual< + typeof import("../../compiler/lightningcss-loader") + >("../../compiler/lightningcss-loader"); + + return { + lightningcssLoader: () => { + const loaded = actual.lightningcssLoader(); + let pass = 0; + + return { + ...loaded, + lightningcss: (options: Parameters[0]) => { + pass++; + const currentPass = pass; + const visitor = Object.fromEntries( + Object.entries(options.visitor ?? {}).map(([name, visit]) => [ + name, + typeof visit === "function" + ? (...args: unknown[]) => { + const returned: unknown = ( + visit as (...visitArgs: unknown[]) => unknown + )(...args); + calls.push({ pass: currentPass, visitor: name, returned }); + return returned; + } + : visit, + ]), + ); + + return loaded.lightningcss({ ...options, visitor }); + }, + }; + }, + }; +}); + +beforeEach(() => { + calls.length = 0; +}); + +test("the second pass hands lightningcss nothing back", () => { + compile(` +.a { color: var(--color); width: 10px; } +@media (min-width: 640px) { .b { color: red; } } +`); + + const secondPass = calls.filter((call) => call.pass === 2); + + expect(secondPass.map((call) => call.visitor)).toEqual( + expect.arrayContaining(["Rule", "StyleSheetExit"]), + ); + expect(secondPass.filter((call) => call.returned !== undefined)).toEqual([]); +}); + +test("the first pass hands back only the lengths it converts", () => { + compile(`.a { width: 10px; height: 2rem; }`); + + const lengths = calls.filter( + (call) => call.pass === 1 && call.visitor === "Length", + ); + + expect(lengths.map((call) => call.returned)).toEqual([ + undefined, + { unit: "px", value: 28 }, + ]); +}); diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 363ded58..d4a0424e 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -135,7 +135,7 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) { const remMultiplier = effectiveRem; firstPassVisitor.Length = (length) => { if (length.unit !== "rem") { - return length; + return undefined; } return { @@ -203,7 +203,6 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) { const visitor: Visitor = { Rule(rule) { maybeMutateReactNativeOptions(rule, builder); - return rule; }, StyleSheetExit(sheet) { if (isLoggerEnabled) { @@ -220,7 +219,6 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) { } logger(`Exiting lightningcss`); - return sheet; }, }; From f1156bae6618298f20b52f05486223221c72a086 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 16 Oct 2026 15:32:51 +0300 Subject: [PATCH 2/2] test(compiler): key the second-pass check on the visitor every pass declares --- src/__tests__/compiler/visitor-returns.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/__tests__/compiler/visitor-returns.test.ts b/src/__tests__/compiler/visitor-returns.test.ts index e254652e..7282c1f1 100644 --- a/src/__tests__/compiler/visitor-returns.test.ts +++ b/src/__tests__/compiler/visitor-returns.test.ts @@ -57,9 +57,7 @@ test("the second pass hands lightningcss nothing back", () => { const secondPass = calls.filter((call) => call.pass === 2); - expect(secondPass.map((call) => call.visitor)).toEqual( - expect.arrayContaining(["Rule", "StyleSheetExit"]), - ); + expect(secondPass.map((call) => call.visitor)).toContain("StyleSheetExit"); expect(secondPass.filter((call) => call.returned !== undefined)).toEqual([]); });