Skip to main content

qwik-optimizer-development

Use when modifying or reviewing the Qwik optimizer under packages/optimizer, Rust transform code, WASM/NAPI bindings, optimizer snapshots, or optimizer-facing runtime behavior.

Source facts

Repository
QwikDev/qwik
Last source activity
October 15, 2026 at 15:38
Detected SKILL.md language
English
Stars
22,063
Forks
1,418

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
qwik-optimizer-development
description
Use when modifying or reviewing the Qwik optimizer under packages/optimizer, Rust transform code, WASM/NAPI bindings, optimizer snapshots, or optimizer-facing runtime behavior.
# Qwik Optimizer Development Use this skill for `packages/optimizer/**` and Rust optimizer work. Keep the repo-wide rules from `.ruler/AGENTS.md` in force. ## Fast Path 1. Identify whether the change is Rust transform logic, snapshots, WASM bindings, NAPI bindings, or TypeScript optimizer-facing integration. 2. Read the closest Rust source, fixture, snapshot, and runtime helper that consumes the emitted shape before editing. 3. Keep transform behavior deterministic: prefer explicit parser/AST cases and stable ordering over source-text heuristics. 4. Use Rust-focused verification first; use `pnpm build.rust` only when a full binding/WASM rebuild is required. It is the only build step that needs the Rust toolchain and `wasm-pack`. 5. If a runtime/core change is also involved, load `qwik-core-development` for that slice. ## Source Map - Rust optimizer core: `packages/optimizer/core/src/` - Rust fixtures: `packages/optimizer/core/src/fixtures/` - Rust snapshots: `packages/optimizer/core/src/snapshots/` - WASM bindings: `packages/optimizer/wasm/` - NAPI bindings: `packages/optimizer/napi/` - Optimizer package entry points: `packages/optimizer/src/` - Vite/Rollup integration: `packages/qwik-vite/src/` ## Verification Use the smallest command that covers the change: ```bash pnpm lint.rust pnpm test.rust pnpm test.rust.update pnpm build.rust pnpm vitest run packages/qwik-vite/src/plugins/plugin.unit.ts ``` `pnpm test.rust` maps to `make test`, which runs Cargo tests for `packages/optimizer/core/Cargo.toml`. Use `pnpm test.rust.update` only when snapshot updates are intentional. The repo's own scripts and tests run on the TypeScript optimizer, and CI runs core specs only there. To run a focused spec on the Rust optimizer, rebuild the native binding (`pnpm build.rust.dev`; `scripts/index.ts --optimizer --platform-binding --dev` skips WASM) and rerun the spec with `QWIK_OPTIMIZER=rust`. ## Baselining a TypeScript optimizer failure The repo's unit and e2e suites run on the TypeScript optimizer, so a parity bug shows up as a failing core test rather than a snapshot diff. To tell a TS-only bug from a pre-existing failure: 1. `pnpm build.platform.copy` downloads the published Rust bindings (no toolchain needed). 2. Rerun the failing file with `QWIK_OPTIMIZER=rust`, which overrides `tsOptimizer` for the run. 3. Transform the failing source with both `createOptimizer`s (`packages/optimizer/dist/index.mjs` and `packages/qwik/dist/ts-optimizer.mjs`) using the plugin's options (server: `hoist`, `minify: 'simplify'`, plus the client `stripCtxName`/`stripExports` and server `stripEventHandlers` lists from `packages/qwik-vite/src/plugins/plugin.ts`) and diff the outputs; without the strip options a diff can look identical while the real build differs. Call sites (`.w([captures])`, `q:p`, stripped `q_qrl_*` sentinels) diverge more often than segment bodies. When an e2e app misbehaves, baseline the app build too, not only the router library: the dev server compiles the fixture apps with whichever optimizer its `qwikVite()` call selects. Compare `segment.entry` per display name as well, in `mode: 'prod'`: a grouping gap shows up only as a different bundle count in the app's `q-manifest.json`, never as a failing test. The router SSG snapshot e2e runs on both optimizers in CI, so any output difference between them fails there; keep it that way instead of skipping one leg. 4. Rebuild the bundle with `pnpm build --optimizer --dev` before rerunning core tests; vitest loads `packages/qwik/dist/ts-optimizer.mjs`, not the optimizer source. ## TypeScript-only syntax Every pass after `stripTypeScript` parses its input as `.js`, and oxc drops syntax that only parses as TypeScript (parameter decorators) from those ASTs, so what it references looks unused and gets DCE'd. Lower such syntax inside `stripTypeScript` itself, threading the option to every call site (see `decorator`); lowering later in the Vite plugin is too late. Segments are served as `.js`, so Vite's own `vite:oxc` pass never touches them. ## JSX tag names Rust transpiles JSX before its Qwik transform, so SWC's rule decides what a tag is: only a tag starting with `[a-z]` is an intrinsic element, while `<Foo>`, `<_Foo>` and the object of `<ui.Home>` read bindings. In the TypeScript optimizer, ask oxc-walker's `isReferenceIdentifier` (or `isNonReferenceIdentifier`) whether a `JSXIdentifier` reads a binding, and `isHtmlElement` whether a tag is intrinsic. ## Const prop bag The runtime trusts a const prop to stay the same for the component's lifetime: reads never subscribe, `_wrapProp` returns const values unwrapped, and SSR skips serializing all-const props. If one changes anyway, the child only re-renders and its `useComputed$`/`useTask$` readers go stale. So either optimizer may put a prop, including a `_fnSignal`/`_wrapProp` wrapper, in the const bag only when every local it reads survives re-renders: imports, `const` bindings with a static initializer (`use*`, `*$`, `*Qrl` calls), or params. Render-computed locals and `_restProps` copies are recreated per render and belong in the var bag. Unit specs pass on the TS optimizer alone, so pin classification with a `test_input!` snapshot and check the runtime with `QWIK_OPTIMIZER=rust`. ## Worker pool changes The transform worker pool (`packages/ts-optimizer/src/worker-pool.ts`) never starts real workers under vitest: the worker entry cannot resolve the `.js`-suffixed imports of the TypeScript sources, so pool tests silently fall back to in-process transforms. Verify worker behaviour against the built bundle instead: `pnpm build --optimizer`, then a Node script that imports `packages/qwik/dist/ts-optimizer.mjs`, runs a few transforms, and reads `VmData` from `/proc/self/status` before spawning a child process. fork() fails with ENOMEM when one mapping exceeds RAM plus swap, and the raw-transfer buffers of several threads in one process coalesce into one mapping (three workers made an 18 GB mapping on a 16 GB host), so keep raw-transfer parsing and other multi-gigabyte reservations out of the workers. `QWIK_TS_OPTIMIZER_RAW_TRANSFER=0` turns raw transfer off in the host as well when a spawn still fails with ENOMEM. ## Generated Binding Names Both optimizers invent bindings (`_rawProps`, `_defaultValue`, the `use*` destructure flattening). Neither gets a scope-correct rename for free, so name uniqueness is the transform's job: - Rust: `private_ident!` plus swc hygiene is not enough once a binding is captured by an extracted segment. Hygiene commits one name per `SyntaxContext` in source order, and segments lifted to module scope are sibling scopes that each keep the base name — the enclosing component scope then carries a duplicate declaration hygiene never revisits. Number generated names at creation. - TypeScript: there is no hygiene pass at all, and the rewrite matches identifiers by name. Number every generated binding, skip names inside destructuring patterns, and resolve references through oxc-walker's `ScopeTracker` so names re-bound in a nested scope keep their declaration. Do not bail out of the rewrite on shadowing — Rust still consolidates, and the snapshot diverges. - Props objects (`_rawProps`) follow Rust hygiene: each takes the lowest `_rawProps{n}` not held by a props object bound beside it, in emission order. A segment file is its own module; inline `.s()` bodies share one, so never hardcode a suffix — go through `rawPropsBindingNames`. Pin cross-optimizer behavior with a Rust `test_input!` snapshot registered in `packages/ts-optimizer/tests/optimizer/snapshot-options.ts`, not with string asserts in a TS test: only the convergence suite catches parity drift. Its mismatches fail in the final `convergence summary` test, so a `-t` filter that skips it reports a false pass. ## Hard Rules - Do not hand-edit generated optimizer build output in `dist/`, `lib/`, or `target/`. - Do not update snapshots without understanding the transform behavior change. - Add or update a transform fixture/snapshot for every optimizer behavior change, including negative cases where the transform must not run. - Keep optimizer output and runtime JSX/QRL/loader semantics in sync. If a transform changes an emitted attribute, import, segment, or capture shape, inspect the runtime consumer too. - After touching `packages/optimizer/core/`, do not claim verification without Rust test/build evidence or a recorded blocker. - If this skill becomes stale after source inspection, update it before finishing or record why guidance edits were out of scope.
View on GitHub