Tracking: known optimizer bugs

Open
#9,278 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@ybai08 is already working on this.

Since Oct 30, 2026.

  • #9324 by @ybai08 — open
  • #9325 by @ybai08 — open

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
15/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
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 transpileTs and transpileJsx, unless noted. "SSR build" means the hoist strategy. Repros omit imports from @qwik.dev/core; ./x exports 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 with const 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 inner instead of outer (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-1 instead of 9-1 (hoist and inline): the _fnSignal deps 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 with Expected `:` but found `}` (segment, hoist and inline). Same for const x = 23; in the component with render({ 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 /> gets columnNumber: 27 instead 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": a with a undeclared: ReferenceError: a is not defined (segment and SSR builds). Rust passes props.

    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": a with a undeclared, for a destructured param read inside renderItem$ or const 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], so count.value throws.

    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 /> throws ReferenceError: _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.type with _rawProps undeclared, 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 label the click still logs ONE; Rust's lib output logs TWO. #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 reads undefined after SSR. TS lifts the capture into q:p without .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), drops const sig, and ships the server$ body; the SSR build reads an undeclared sig.

    Repro
    export const Works = component$(() => {
      const sig = useSignal('hola');
      return <div onClick$={server$(() => console.log('in server', sig.value))}></div>;
    });
    
  • $() handlers on server-* and route* 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 throws Q34 Cannot serialize function; with a module let, 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]) before items.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 free x: 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() } or function () { log(); }(); with no error, and Vite fails later. Same for const noop = function () { log(); }(); and const 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-use const 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_0 and the client u6_1 (Rust: u6_1 in both). The first client re-render after resume rebuilds the element: typed input and child state are lost. JSX in a useVisibleTask$, 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 class in var props, the client in const props, so re-rendering the list removes class for good. Keep List in its own file: in one file the li key 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), so title keeps L0 in 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 fmt in the _fnSignal deps, and SSR throws Q34 Cannot serialize function (Q20 for 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:slot and a forwarded ref on HTML elements. TS wraps both in signals: the <p> lands in no slot in a client render and never moves after SSR, and ref={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 on but title stays off: TS puts title: label in 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 with Identifier `_hf0` has already been declared. Also, jsx(Child, {}) gets key u6_0 in the SSR build and u6_1 in 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: logs 2:2, not 2: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 (onGet is in the default stripExports): Cannot split a chunk that has already been edited.

    Repro
    export const onGet = () => <div />;
    export default component$(() => <p>hi</p>);
    
  • Hoist with transpileJsx: false emits invalid code: const A_component_… = var _reactJsxRuntime = require("react/jsx-runtime");. Only direct transformModules callers hit this; the Vite plugin always transpiles JSX.

  • Invalid output ships without an optimizer error. lazyParse in transform/module-cleanup.ts waits for a throw, but parseWithRawTransfer returns its errors instead, so runDcePipeline passes 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: label in const props and emits flags 7; after a click the text shows on but title stays off. 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:value stops updating when onInput$ is a var handler. Rust splits "q-e:input" across both bags, so the handler runs but v never 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 ref beats a ref written after it. With <Wrapper ref={outer} />, outer gets the element and inner stays unset: Rust puts ref: inner in var props next to _getConstProps(props). q:slot after 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 _rawProps twice 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 } = store becomes a live store.a. The first click logs 2, not the render-time 1. For a plain local object, the handler captures the whole object, and SSR throws Q34 if 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 outside component$ 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 keeps action: $(pick). Dev shows "Optimizer should replace all usages of $()", and production logs Q14 and serves the 500 page. TS is fine. Checked on d7693f511.

    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 changes label the click still logs ONE.

    Repro
    export const Child = component$((props: { label: string }) => {
      const upper = props.label.toUpperCase();
      const handler = $(() => console.log(upper));
      return <button onClick$={handler} />;
    });
    
  • A nested $() returning items.map(…) uses an undeclared handler. The render segment references the click handler's QRL without declaring it: ReferenceError on 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:slot on a component child. Both wrap it in a _fnSignal in 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 module let in a handler writes to an import (import { _auto_clicks as clicks }) and throws Assignment 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 _rawProps but 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 direct transformModules callers 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.

  • useOn drops the element's own var handler for the same event. One click logs only useOn, not own:ONE: addUseOnEvent writes 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$ or document:onQidle$ runs again after a re-render. Each re-render puts q-d:qinit back 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

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from QwikDev/qwik

All issues in QwikDev/qwik

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.