Tracking: known optimizer bugs
Maintainers usually reply within 1 day
@ybai08 is already working on this.
Since Oct 30, 2026.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 15/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- javascript, typescript
- Domain
- frontend
Research direction
This is a tracking issue for many TypeScript optimizer bugs, with reproductions and affected builds listed under each item. Start by choosing one specific bug and reproducing it with the described segment or SSR build; the payload does not name implementation files or tests. Done means fixing that reproduction and verifying the reported behavior, but the broad scope and lack of a settled decision make this unsuitable as a first issue.
Written by the indexing model from the issue text.
Description
This issue tracks known optimizer bugs: those whose fix PRs I closed because they need more work or a decision first, and those that never had a PR. Every bug below happens on main at d1ca2412e (2026-10-22).
- TS is the TypeScript optimizer (
tsOptimizer: true); Rust is the default one. - Repros use the segment strategy (the client build) with
transpileTsandtranspileJsx, unless noted. "SSR build" means the hoist strategy. Repros omit imports from@qwik.dev/core;./xexports any function. - Open PRs that fix other optimizer bugs: #9107, #9127, #9133, #9159, #9160.
From closed PRs
#9131: component consts (TS)
-
Alias chains crash SSR and library builds. Under hoist, inline and lib, the output reads an undeclared
a:ReferenceError: a is not defined. The same happens withconst a = bump; const b = a; b();inside a handler.Repro
export const Cmp = component$(() => { const sig = useSignal(0); const a = sig; const b = a; return <div>{b.value}</div>; }); -
A handler reads a sibling handler's const. The second button logs
innerinstead ofouter(segment and SSR builds). Same when the inner const sits in a block or a helper in the component.Repro
export const Cmp = component$(() => { const label = 'outer'; return ( <div> <button onClick$={() => { const label = 'inner'; console.log(label); }} /> <button onClick$={() => console.log(label)} /> </div> ); }); -
An alias read through a loop param of the same name. SSR renders
undefined-1instead of9-1(hoist and inline): the_fnSignaldeps get the loop param, not the signal.Repro
export const Cmp = component$(() => { const sig = useSignal(9); const a = sig; return <div>{[1].map((sig) => <span>{a.value + '-' + sig}</span>)}</div>; }); -
A captured const used as an object shorthand breaks the build. TS emits
console.log({ 'Hello' }), then fails withExpected `:` but found `}`(segment, hoist and inline). Same forconst x = 23;in the component withrender({ x })in a handler.Repro
export const Cmp = component$(() => { const g = 'Hello'; return <button onClick$={() => console.log({ g })} />; }); -
Dev columns drift after a destructure. In dev mode,
<b />getscolumnNumber: 27instead of 21, so click-to-source (data-qwik-inspector) points 6 columns too far right.Repro
import { useForm2 } from './x'; export const Cmp = component$(() => { const { store5 } = useForm2(); return <p>{store5}<b /></p>; });
#9132 and #9137: destructured props in event handlers (TS)
-
A handler reading a body destructure crashes SSR. TS emits
"q:p": awithaundeclared:ReferenceError: a is not defined(segment and SSR builds). Rust passesprops.Repro
export const Cmp = component$((props: { a?: string }) => { const { a } = props; return <button onClick$={() => console.log(a)}>{a}</button>; }); -
A handler in a render prop or nested
$()crashes SSR. Same"q:p": awithaundeclared, for a destructured param read insiderenderItem$orconst render = $(() => <button onClick$={…} />).Repro
import { List } from './list'; export const Cmp = component$(({ a }: { a: string }) => ( <List renderItem$={() => <button onClick$={() => console.log(a)} />} /> )); -
The second handler on an element reads
undefined. The dblclick handler gets(_, _1, _2, _3, count)for"q:ps": [_rawProps, count], socount.valuethrows.Repro
export const Cmp = component$(({ a, b }: { a: string; b: string }) => { const count = useSignal(5); return ( <button onClick$={() => console.log(a, b)} onDblClick$={() => console.log(count.value)}> x </button> ); }); -
A dynamic prop default read in a handler is lost. Clicking
<Cmp />throwsReferenceError: _defaultValue is not defined(SSR and client).Repro
import { getFallback } from './x'; export const Cmp = component$(({ x = getFallback() }: { x?: string }) => ( <button onClick$={() => console.log(x)} /> )); -
Nested dynamic defaults break the build.
Identifier '_defaultValue' has already been declared(Rust names the inner one_defaultValue1). Same with a render prop that has its own default.Repro
import { getA, getB } from './x'; export const Outer = component$(({ a = getA() }: { a?: string }) => { const Inner = component$(({ b = getB() }: { b?: string }) => <p>{a}{b}</p>); return <Inner />; }); -
A handler that destructures its event breaks once it captures anything. TS emits
(type, _1, count) => … _rawProps.typewith_rawPropsundeclared, so the click does nothing. No props needed.Repro
export const Cmp = component$(() => { const count = useSignal(5); return <button onClick$={({ type }: Event) => console.log(count.value + ':' + type)} />; });
#9134: event handler captures (TS)
-
Library builds keep first-render values in handlers. In lib mode (and the inline strategy), TS puts a capturing handler in const props, so after the parent changes
labelthe click still logsONE; Rust's lib output logsTWO. #9160 moves these handlers to var props, which should fix it.Repro
export const Child = component$((props: { label: string }) => { const upper = props.label.toUpperCase(); return <button onClick$={() => console.log(upper)} />; }); -
A
$()handler in a plain function component readsundefinedafter SSR. TS lifts the capture intoq:pwithout.m(); Rust keeps.w([text]). Client-only rendering works.Repro
export function ButtonQrl({ text }: { text: string }) { return <button onClick$={$(() => console.log(text))}>{text}</button>; } -
onClick$={server$(…)}breaks both builds. The client build emits"q-e:click": server$ = { … }(an assignment to the import), dropsconst sig, and ships theserver$body; the SSR build reads an undeclaredsig.Repro
export const Works = component$(() => { const sig = useSignal('hola'); return <div onClick$={server$(() => console.log('in server', sig.value))}></div>; }); -
$()handlers onserver-*androute*custom tags do nothing. The client build strips them as server code (export const … = null,_noopQrl). Same for<router-link onClick$={$(…)}>.Repro
export const Cmp = component$(() => { const sig = useSignal(0); return <server-status onClick$={$(() => sig.value++)}>{sig.value}</server-status>; }); -
Module values read in a plain function component's handler go into
q:ps. SSR throwsQ34 Cannot serialize function; with a modulelet, the handler bumps a copy.Repro
const fmt = { prefix: 'p:', format: (s: string) => s }; export function FmtButton({ text }: { text: string }) { return <button onClick$={() => console.log(fmt.prefix + text)}>{text}</button>; } -
In dev, a component
*$prop in a.map()is hoisted above the loop. The dev client build emits….w([item])beforeitems.map(…):ReferenceError: item is not defined. Prod is fine.Repro
import { Child } from './child'; export const Cmp = component$(() => { const items = ['a', 'b']; return <ul>{items.map((item) => <Child onClick$={() => console.log(item)} />)}</ul>; }); -
A block const captured next to an outer binding is dropped. The handler gets
(_, _1, s)and reads a freex:ReferenceError: x is not defined.Repro
export const Cmp = component$(() => { const s = useSignal(1); if (s.value) { const x = s.value + 1; return <button onClick$={() => console.log(x, s.value)} />; } return <p />; });
#9135: unused declarations (TS)
-
Without semicolons, an unused initializer joins the line above. The client build emits
export const a = foo()[1, 2].map(log);. The SSR build is correct.Repro
import { component$ } from '@qwik.dev/core' import { foo, log } from './x' export const a = foo() const x = [1, 2].map(log) export const Cmp = component$(() => <p>hi</p>) -
Unused object, class and IIFE initializers that have side effects. At module level the client build throws (
could not strip TypeScript from rewritten module). In a component or handler body, TS emits invalid JS such as{ a: f(), b: g() }orfunction () { log(); }();with no error, and Vite fails later. Same forconst noop = function () { log(); }();andconst K = class { static { log(); } };.Repro
import { f, g } from './x'; const o = { a: f(), b: g() }; export const Cmp = component$(() => <p>hi</p>); -
Output only: dead pure calls. TS keeps unused
Math.random(),Date()and'abc'.toUpperCase()that Rust drops, and re-exports a single-useconst x = Math.random()that Rust moves into its segment. Values do not change.
#9136: SSR and client builds out of step (TS)
-
SSR and client number JSX keys differently. When a handler stripped from SSR renders JSX, the SSR build gives the root
u6_0and the clientu6_1(Rust:u6_1in both). The first client re-render after resume rebuilds the element: typed input and child state are lost. JSX in auseVisibleTask$, or a$()with JSX in a later component, does the same.Repro
import { toast } from './toast'; export const Cmp = component$(() => { const n = useSignal(0); if (n.value < 0) return null; return ( <div> <input /> <button onClick$={() => { n.value++; toast(<p>saved</p>); }}>+</button> </div> ); }); -
A render prop's signal attribute disappears after resume. SSR puts
classin var props, the client in const props, so re-rendering the list removesclassfor good. KeepListin its own file: in one file thelikey also drifts and the element is rebuilt, which hides this bug.Repro
// list.tsx export const List = component$((props: { n: number; renderItem$: QRL<(i: number) => JSXOutput> }) => ( <ul>{props.renderItem$(props.n)}</ul> )); // test.tsx import { List } from './list'; export const Cmp = component$(() => { const sel = useSignal(0); const n = useSignal(0); return ( <> <List n={n.value} renderItem$={(i: number) => <li class={'c' + sel.value}>{i}</li>} /> <button onClick$={() => n.value++}>re-render list</button> </> ); }); -
A render prop's render-time capture goes stale. The client build puts
title={label}in const props (Rust: var), sotitlekeepsL0in a client render and disappears after resume.Repro
// same list.tsx as above export const Cmp = component$(() => { const n = useSignal(0); const label = 'L' + String(n.value); return ( <> <List n={n.value} renderItem$={(i: number) => <li title={label}>{i}</li>} /> <button onClick$={() => n.value++}>+</button> </> ); });
#9138: module-level bindings in JSX (TS)
-
A module object read next to a signal crashes SSR. TS puts
fmtin the_fnSignaldeps, and SSR throwsQ34 Cannot serialize function(Q20for a class instance). Rust emits a plain read.Repro
const fmt = { prefix: 'n=', fn: () => 1 }; export const Cmp = component$(() => { const count = useSignal(0); return <button onClick$={() => count.value++}>{fmt.prefix + count.value}</button>; }); -
A dynamic
q:slotand a forwardedrefon HTML elements. TS wraps both in signals: the<p>lands in no slot in a client render and never moves after SSR, andref={props.inputRef}is never set. Rust's plain values work.Repro
const Panel = component$(() => ( <div> <section id="a"><Slot name="a" /></section> <section id="b"><Slot name="b" /></section> </div> )); export const SignalSlot = component$(() => { const flag = useSignal(true); return ( <> <button onClick$={() => (flag.value = !flag.value)}>t</button> <Panel><p q:slot={flag.value ? 'a' : 'b'}>X</p></Panel> </> ); }); const RefChild = component$((props: { inputRef: Signal<Element | undefined> }) => ( <p ref={props.inputRef}>R</p> )); export const ForwardRef = component$(() => { const a = useSignal<Element>(); const b = useSignal<Element>(); const flag = useSignal(true); return ( <> <button onClick$={() => (flag.value = !flag.value)}>t</button> <RefChild inputRef={flag.value ? a : b} /> </> ); }); -
A local that shadows an import goes stale in an attribute. After a click, the text shows
onbuttitlestaysoff: TS putstitle: labelin const props.Repro
import { label } from './x'; export const read = () => label; export const Toggle = component$(() => { const on = useSignal(false); const label = on.value ? 'on' : 'off'; return <button title={label} onClick$={() => (on.value = !on.value)}>{label}</button>; });
No PR yet
TS
-
jsx()calls next to JSX syntax break the build. Hoist and inline fail withIdentifier `_hf0` has already been declared. Also,jsx(Child, {})gets keyu6_0in the SSR build andu6_1in the client (same effect as the key drift above).Repro
import { jsx } from '@qwik.dev/core/jsx-runtime'; export const Cmp = component$(() => { const s = useSignal(0); return <div title={s.value + 1}>{jsx('p', { title: s.value + 2 })}</div>; }); -
The props rewrite reaches
$()params. TS turns({ item }) => { item = item + 1 }into_rawProps.item = _rawProps.item + 1, so the call changes the caller's object: logs2:2, not2:1.Repro
export const Cmp = component$(() => { const inc = $(({ item }: { item: number }) => { item = item + 1; return item; }); return ( <button onClick$={async () => { const arg = { item: 1 }; const result = await inc(arg); console.log(result + ':' + arg.item); }} /> ); }); -
A
worker$handler gets the props object instead of a loop param of the same name. TS emits"q:p": _rawProps. #9133 fixes this for plain handlers only.Repro
import { worker$ } from '@qwik.dev/core/worker'; export const Cmp = component$(({ item, items }: { item: string; items: string[] }) => ( <ul>{items.map((item) => <li onClick$={worker$(() => console.log(item))}>{item}</li>)}</ul> )); -
Lib mode crashes on any
bind:value:Identifier `inlinedQrl` has already been declared. Plain inline is fine.Repro
export const Cmp = component$(() => { const v = useSignal(''); return <input bind:value={v} />; }); -
The client build crashes when a stripped export holds JSX (
onGetis in the defaultstripExports):Cannot split a chunk that has already been edited.Repro
export const onGet = () => <div />; export default component$(() => <p>hi</p>); -
Hoist with
transpileJsx: falseemits invalid code:const A_component_… = var _reactJsxRuntime = require("react/jsx-runtime");. Only directtransformModulescallers hit this; the Vite plugin always transpiles JSX. -
Invalid output ships without an optimizer error.
lazyParseintransform/module-cleanup.tswaits for a throw, butparseWithRawTransferreturns its errors instead, sorunDcePipelinepasses invalid code through (as in the unused-initializer bug above) and Vite fails later at generated positions.
Rust
-
A local named like a module export goes stale in an attribute. Rust matches exports by name, puts
title: labelin const props and emits flags 7; after a click the text showsonbuttitlestaysoff. Renaming the export gives the right output. TS is fine.Repro
export const label = 'Count'; export const Toggle = component$(() => { const on = useSignal(false); const label = on.value ? 'on' : 'off'; return <button title={label} onClick$={() => (on.value = !on.value)}>{label}</button>; }); -
bind:valuestops updating whenonInput$is a var handler. Rust splits"q-e:input"across both bags, so the handler runs butvnever changes. Same with a direct handler that reads a prop.Repro
export const Cmp = component$(() => { const v = useSignal(''); return ( <> <input bind:value={v} onInput$={$(() => console.log('input'))} /> <b>{v.value}</b> </> ); }); -
A spread's
refbeats arefwritten after it. With<Wrapper ref={outer} />,outergets the element andinnerstays unset: Rust putsref: innerin var props next to_getConstProps(props).q:slotafter a spread gets the same placement.Repro
export const Wrapper = component$((props: any) => { const inner = useSignal<Element>(); return <input {...props} ref={inner} />; }); -
A render prop with its own and outer props declares
_rawPropstwice under hoist (the SSR build) and inline:(_rawProps)=>{ let _rawProps = _capturesObj._[0]; …, which V8 rejects.Repro
import { List } from './list'; export const Cmp = component$(({ a }: any) => ( <List renderItem$={({ b }: any) => ( <div> <button onClick$={() => console.log(a)} /> <button onClick$={() => console.log(b)} /> </div> )} /> )); -
const { a } = storebecomes a livestore.a. The first click logs2, not the render-time1. For a plain local object, the handler captures the whole object, and SSR throwsQ34if it holds a function.Repro
export const Cmp = component$(() => { const store = useStore({ a: 1 }); const { a } = store; return <button onClick$={() => { store.a++; console.log(a); }}>{a}</button>; }); -
$()in a JSX prop outsidecomponent$stays a runtime call (#9338). With a prop name that doesn't end in$and an identifier or a function with no parameters as the argument, Rust keepsaction: $(pick). Dev shows "Optimizer should replace all usages of $()", and production logsQ14and serves the 500 page. TS is fine. Checked ond7693f511.Repro
import { Child } from './child'; const pick = () => console.log('picked'); const renderChild = () => <Child action={$(pick)} />; export const Cmp = component$(() => <>{renderChild()}</>);
Both optimizers
-
const handler = $(() => …)keeps first-render values. Both put the$()result in const props, so after the parent changeslabelthe click still logsONE.Repro
export const Child = component$((props: { label: string }) => { const upper = props.label.toUpperCase(); const handler = $(() => console.log(upper)); return <button onClick$={handler} />; }); -
A nested
$()returningitems.map(…)uses an undeclared handler. The render segment references the click handler's QRL without declaring it:ReferenceErroron the client (segment builds; Rust also in prod).Repro
export const Cmp = component$(() => { const count = useSignal(0); const items = ['a', 'b']; const render = $(() => items.map((item) => <li onClick$={() => console.log(count.value)}>{item}</li>)); return <ul>{render()}</ul>; }); -
A dynamic
q:sloton a component child. Both wrap it in a_fnSignalin const props: after SSR the child never moves, and a client render shows it in no slot.Repro
const Slotted = component$(() => ( <div> <section class="a"><Slot name="a" /></section> <section class="b"><Slot name="b" /></section> </div> )); const Item = component$(() => <span>X</span>); export const Cmp = component$(() => { const flag = useSignal(true); return ( <div> <button id="toggle" onClick$={() => (flag.value = !flag.value)}>t</button> <Slotted><Item q:slot={flag.value ? 'a' : 'b'} /></Slotted> </div> ); }); -
clicks++on a moduleletin a handler writes to an import (import { _auto_clicks as clicks }) and throwsAssignment to constant variable, with no diagnostic (segment builds).Repro
let clicks = 0; export const Cmp = component$(() => <button onClick$={() => clicks++}>{clicks}</button>); -
A destructured prop used as a JSX tag is undefined when JSX isn't transpiled. With
transpileJsx: false, both optimizers turn the param into_rawPropsbut leave<Model />, for a prop with a default or a quoted key, inside handlers too. Rust also does it for a plain({ Model }), which #9127 fixes in TS. The Vite plugin always transpiles JSX, so only directtransformModulescallers hit this.Repro
import { Fallback } from './x'; export const Cmp = component$(({ Model = Fallback }: { Model?: any }) => <Model />);With
transpileJsx: false, both emit(_rawProps) => <Model />.
Core runtime
These show up with both optimizers, which emit the var-props shape that triggers them.
-
useOndrops the element's own var handler for the same event. One click logs onlyuseOn, notown:ONE:addUseOnEventwrites over the var entry.Repro
export const UseOnChild = component$((props: { label: string }) => { const upper = props.label.toUpperCase(); useOn('click', $(() => console.log('useOn'))); return <button onClick$={() => console.log('own:' + upper)} />; }); -
A var
document:onQinit$ordocument:onQidle$runs again after a re-render. Each re-render putsq-d:qinitback and queues the event again.Repro
export const QinitChild = component$((props: { label: string }) => { const upper = props.label.toUpperCase(); return <div document:onQinit$={() => console.log('qinit:' + upper)} />; });
- Dominant language
- TypeScript
- Stars
- 22.1k
- Forks
- 1.4k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 103
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from QwikDev/qwik
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
docs router V2
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
DX enhancement V2
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
QwikDev/qwik#8480 · 2 comments ·
Maintainers usually reply within 1 day
-
[🐞] v2: `on*$` handler on a dynamic tag (`<Tag onClick$>` with `Tag = "button"`) is rendered as an attribute instead of a listenerPossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 4/5 3-5 days Newbie friendliness 25/100
Maintainers usually reply within 1 day
-
[🐞] v2: bare boolean attribute (`<button disabled>`) on a client-created element becomes `disabled="true"`Possibly taken @intellix claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 25/100
Maintainers usually reply within 1 day
Similar issues
-
Add: Class TV ModaOpencheck:passed streams:add
Difficulty 1/5 Under an hour Newbie friendliness 65/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
GiganticMinecraft/seichi-playguide#183 ·
Maintainers usually reply within 1 day
-
area: core complexity: easy impact: medium
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
ueberdosis/tiptap#8668 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
longbridge/nsh#804 ·
Maintainers usually reply within 1 day