Skip to content

feat(compiler): add an optimize option for constant folding and DCE - #3231

Open
lxsmnsyc wants to merge 4 commits into
solidjs:nextfrom
lxsmnsyc:compiler-optimize-option
Open

feat(compiler): add an optimize option for constant folding and DCE#3231
lxsmnsyc wants to merge 4 commits into
solidjs:nextfrom
lxsmnsyc:compiler-optimize-option

Conversation

@lxsmnsyc

@lxsmnsyc lxsmnsyc commented Sep 3, 2026

Copy link
Copy Markdown
Member

Add an optimize option to @solidjs/compiler

optimize: true (default false) runs a constant-folding and dead-code-elimination pass over the parsed program before JSX is lowered. Anything it resolves never reaches a generate at all: the element pays for no component call, memo, or insert hole, and its markup joins the surrounding template.

What folds

  • Constant expressions: literals, template literals, and the unary, binary, logical, and conditional operators over them.
  • Constant bindings at any scope: const, and let that nothing ever writes to. References resolve through oxc_semantic, so a const DEBUG = false inside a component folds at its use sites while a same-named binding in another scope is a different symbol and is left alone. var is excluded, since a read before its declaration sees undefined rather than throwing.
  • Unreachable statements: if/else branches, while (false) loops, and anything after a return, throw, break, or continue.
  • A discarded condition still runs. Every fold that reads a condition also discards it. When the condition is effectful but statically decided ([effect()], { k: effect() }, a class with a static block), the branch still folds and the condition is resequenced in front of the result: [b()] ? c() : d() becomes ([b()], c()). Component props are the exception and stay unfolded, since Solid reads them inside a memo.

Control-flow components

Tag Resolves to
<Show when> its children, or its fallback
<For each> its fallback when each is [] or statically falsy
<Repeat count> its fallback when count is below one
<Switch> / <Match when> drops statically false matches, collapses to a statically true one, falls back when every match is false
<Dynamic component> the element itself, when component is a static intrinsic tag name

<Portal>, <Loading>, <Errored>, and <Reveal> deliberately never fold. Each gates on runtime state (a mount target, a pending read, a thrown error, a reveal order) that no static analysis can decide.

Rules that keep a fold from changing behavior

  • Identity before name. Being in builtIns only makes a name a candidate. A tag folds only when it resolves to Solid's own component: either nothing declares the name and the compiler auto-imports it, or it is a top-level value import from moduleName or solid-js. A local Show, or one imported from an unrelated module, is a different component and is left alone.
  • Aliases resolve by exported name. <Cond> from import { Show as Cond } from "solid-js" folds as Show. import { Show as Cond } from "./my-show" does not fold.
  • No spread attributes. A spread can supply or override the very prop the fold reads.
  • No function children. The runtime decides from their arity whether to call them.
  • Conservative constant lattice. The evaluator declines any fold whose spelling could diverge from a real engine: string and number coercions outside the safe-integer range, non-ASCII relational comparison, and string-to-number coercion. A missed fold costs bytes, a wrong fold costs correctness.
  • One accepted deviation. A reference that runs before its declaration is a temporal dead zone throw at runtime, and folding turns it into a value. This matches what production minifiers do and is called out in the README.
  • Hoisting is respected. A branch carrying a var or function declaration is never removed.

Consistency requirement

Folding changes the shape of the rendered tree, and with it hydration ids. A server build and its client build must be compiled with the same optimize value. This is called out in the README, the TypeScript types, and the changeset.

Files

Path
src/optimize/mod.rs pass driver, JSX control-flow folding, statement DCE
src/optimize/value.rs the constant lattice and its ECMAScript operator semantics
src/shared/ast_builder.rs builder for an intrinsic element, used by the <Dynamic> fold
src/compiler.rs, src/config.rs, src/node_adapter.rs, src/lib.rs option plumbing
index.js, types.d.ts, README.md JavaScript surface, boolean validation, docs

Testing

  • 17 Rust unit tests covering the folding rules, the constant lattice edge cases (NaN, -0, ToInt32 wrapping, 1 ** Infinity), scope resolution, and every built-in identity case.
  • 9 JavaScript tests covering the option surface, generated output, and SSR parity.
  • Full suite green: 5357 JavaScript tests across 37 files. Rust tests pass under the default features, --no-default-features, and --features tsrx.
  • rustfmt clean, and clippy produces no new warnings.

Scope

Compiler only. @solidjs/babel-plugin does not gain the option, so the option-matrix parity suite is untouched. The pass is off by default, so existing output is unchanged.

@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0ce6be7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@solidjs/compiler Minor
@solidjs/babel-plugin Minor
@solidjs/diagnostics Minor
@solidjs/element Minor
@solidjs/h Minor
@solidjs/html Minor
@solidjs/signals Minor
solid-js Minor
@solidjs/universal Minor
@solidjs/web Minor
test-integration Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codspeed-hq

codspeed-hq Bot commented Sep 3, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 136 untouched benchmarks


Comparing lxsmnsyc:compiler-optimize-option (0ce6be7) with next (3424f9a)

Open in CodSpeed

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