Skip to content

Jsx#138

Open
kshutkin wants to merge 97 commits into
mainfrom
jsx
Open

Jsx#138
kshutkin wants to merge 97 commits into
mainfrom
jsx

Conversation

@kshutkin
Copy link
Copy Markdown
Owner

No description provided.

kshutkin added 30 commits May 13, 2026 23:08
The reconciler effect inside forEach (and the function-child effect in
appendChild) runs deferred via the flush queue. When it ran, activeScope
was typically undefined, so scope(...) calls inside them created scopes
with no parent — unreachable from the surrounding render scope.

Consequence: render() dispose did not tear down per-item reactive
children. They kept firing on shared-signal changes and held references
that prevented GC (~660 KB per 200x50 mount/dispose cycle leaked).

Fix: capture activeScope at the synchronous setup site (forEach() call,
appendChild function-child branch) and pass it explicitly as the parent
of the inner scope().

Added regression test 14 in for-each.spec.mjs that uses a manual flush
queue to reproduce the deferred-execution conditions (the rest of the
suite uses a synchronous scheduler, under which the bug doesn't show).
The 'if (k === "children") continue' branch was unreachable under the
automatic JSX transform (jsx-runtime strips children from props before
delegating to createElement) and under normal classic JSX (children are
passed as varargs).

The only reachable callers were:
- Hand-written createElement({children: ...}) calls (tests only).
- The corner-case <div children="..." /> JSX attribute spelling, which
  nobody uses in practice.

Removed both the guard and the one test that exercised it via a direct
createElement call. The other 'children' test still passes because it
uses jsx(), which strips children before calling createElement.
for...in on null/undefined is a spec'd no-op (skipped without iteration),
so the guard added no correctness — only a branch. Micro-bench shows the
guard-less version is marginally faster (~10% on the null-props path)
and the V8 deopt profile is unchanged (same two training deopts on the
for-in / inner call sites in both versions).
Spread of null/undefined is a no-op per spec ({...null} === {}), so the
?? {} fallback inside { ...(props ?? {}), children } is redundant.

Kept the other occurrence (compProps = props ?? {}) which is observable:
it provides a safe object argument to user components when called as
classic JSX <MyComp /> compiles to createElement(MyComp, null).
…entArray

Both jsx() and jsxs() (and jsxDEV) now share a single function body that
calls a new internal createElementArray(type, props, children) entry
point. This skips the array -> spread(...) -> rest-array roundtrip that
previously went through createElement's varargs signature.

createElement remains a thin varargs wrapper that delegates to
createElementArray, so the classic JSX transform and direct user calls
are unchanged.

Tight-loop bench (50K iter * 25 samples * 5-iter warmup, p25/p75 spread
< 3%) shows a consistent 1-5% reduction on small-tree shapes:

  jsx <div/>             410 -> 391 ns  (-5%)
  jsx <div>x</div>       917 -> 890 ns  (-3%)
  jsx <div><a/></div>   1374 -> 1336 ns (-3%)
  jsxs 2 children       2230 -> 2180 ns (-2%)
  jsxs 3 children       4224 -> 4124 ns (-2%)
  jsxs 5 children       6428 -> 6345 ns (-1%)
  jsxs 1000 children    3561 -> 3561 us (DOM-bound, no change)

Unifying jsx/jsxs/jsxDEV into one shared function body avoids inline-
cache fragmentation that an earlier split-by-arity attempt exhibited.
The two comment markers `start` and `end` are always inserted into the
same parent and never moved or removed during the function-child effect,
so the sibling walk from `start.nextSibling` is guaranteed to reach
`end` before running off the end of the sibling chain. The
`nextSibling !== null` check was therefore redundant.

Real-Chromium microbench (31 samples, per-op GC, N=0..100 children
between markers) shows the simpler loop within noise of the guarded
loop at every N — no perf regression, just less code.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 27, 2026

⚠️ No Changeset found

Latest commit: 0b28e5a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

kshutkin added 8 commits May 28, 2026 21:54
createElementArray now passes { is } to document.createElement for host tags so <button is="my-tag"> produced by the runtime upgrades. SVG/createElementNS path untouched. Adds feature-guarded is.spec.mjs.
…Element

createCustomElement builds an unregistered class (custom/scoped registries, manual define). defineElement is a thin autonomous-element facade (no extendElement). defineBuiltinElement handles customized built-ins. Adds tests for unregistered class, scoped registry isolation, and end-to-end built-in upgrade via the jsx is= runtime.
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