Skip to content

refactor(es/react): Inline dev jsx metadata into jsx hook#11533

Open
kdy1 wants to merge 1 commit intomainfrom
kdy1/merge-react
Open

refactor(es/react): Inline dev jsx metadata into jsx hook#11533
kdy1 wants to merge 1 commit intomainfrom
kdy1/merge-react

Conversation

@kdy1
Copy link
Copy Markdown
Member

@kdy1 kdy1 commented Feb 6, 2026

Summary

  • inline development JSX metadata generation (__source, __self) into the main jsx hook
  • simplify react() hook composition by removing separate jsx_src and jsx_self passes from the react pipeline
  • preserve automatic-runtime createElement fallback behavior for source/self metadata and constructor-safe this handling

Testing

  • cargo fmt --all
  • cargo test -p swc_ecma_transforms_typescript -p swc_ecma_transforms_react -p swc_ecma_transforms
  • cargo test -p swc --test projects --test tsc

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 6, 2026

⚠️ No Changeset found

Latest commit: cb13be7

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.

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

@kdy1 kdy1 changed the title refactor(react): inline dev jsx metadata into jsx hook refactor(es/react): inline dev jsx metadata into jsx hook Feb 6, 2026
@kdy1 kdy1 changed the title refactor(es/react): inline dev jsx metadata into jsx hook refactor(es/react): Unline dev jsx metadata into jsx hook Feb 6, 2026
@kdy1 kdy1 changed the title refactor(es/react): Unline dev jsx metadata into jsx hook refactor(es/react): Inline dev jsx metadata into jsx hook Feb 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 6, 2026

Binary Sizes

File Size
swc.linux-x64-gnu.node 28M (28599368 bytes)

Commit: 917da36

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Feb 6, 2026

Merging this PR will not alter performance

✅ 184 untouched benchmarks


Comparing kdy1/merge-react (cb13be7) with main (b069558)

Open in CodSpeed

@kdy1
Copy link
Copy Markdown
Member Author

kdy1 commented Feb 6, 2026

I expected more perf gain, but it seems like this is not very helpful

@kdy1 kdy1 closed this Feb 6, 2026
@kdy1 kdy1 deleted the kdy1/merge-react branch February 6, 2026 08:54
@kdy1 kdy1 restored the kdy1/merge-react branch February 6, 2026 10:40
@kdy1
Copy link
Copy Markdown
Member Author

kdy1 commented Feb 6, 2026

Reopening as it has improvement of 1%

@kdy1 kdy1 reopened this Feb 6, 2026
@kdy1 kdy1 marked this pull request as ready for review February 6, 2026 10:40
@kdy1 kdy1 requested a review from a team as a code owner February 6, 2026 10:40
Copilot AI review requested due to automatic review settings February 6, 2026 10:40
@kdy1 kdy1 added this to the Planned milestone Feb 6, 2026
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Feb 6, 2026

PR Review: refactor(es/react): Inline dev jsx metadata into jsx hook

Summary

This PR inlines the jsx_src (__source) and jsx_self (__self) development metadata generation directly into the main Jsx hook, removing two separate hook passes from the react() composite pipeline. This is a well-motivated refactoring that reduces overhead from the composite hook dispatch.


Code Quality & Correctness

The inlining is faithful to the originals. I compared the new code against jsx_src/mod.rs and jsx_self/mod.rs line-by-line:

  • JsxSelfCtx is a direct rename of jsx_self::Context with identical fields
  • current_ctx(), push_ctx(), pop_ctx() are identical
  • should_inject_self() correctly replicates the !(in_constructor && in_derived_class) guard
  • source_object_expr() produces the same {fileName, lineNumber, columnNumber} object as jsx_src
  • All enter_*/exit_* VisitMutHook implementations for class/constructor/method/etc. context tracking are identical to the original jsx_self implementations

Stale comment at line 1420-1423:

/// By doing this in exit_expr (after children are visited), we ensure that
/// jsx_src and jsx_self have already added their __source and __self
/// attributes.

This comment references the old pipeline where jsx_src and jsx_self were separate hooks that ran before jsx. Now that the functionality is inlined, this comment is misleading — the __source and __self injection now happens within the same hook during jsx_elem_to_expr, not from separate hooks. Consider updating this to reflect the new reality.


Behavioral Analysis

Hook ordering change in react() (lib.rs):

Old order: jsx_src → jsx_self → refresh → jsx → display_name → pure_annotations

New order: refresh → jsx (with inlined src/self) → display_name → pure_annotations → NoopHook

This is correct because:

  • Previously, jsx_src and jsx_self used enter_jsx_opening_element to inject __source/__self as JSX attributes before jsx converted them to JS in exit_expr. The inlined version now handles this directly during the element-to-expression conversion, so the ordering dependency is eliminated.
  • refresh needs to run before jsx transformation to instrument components, which is preserved.

Automatic runtime (jsxDEV) path — Lines 994-1001: When not using createElement, source_props and self_props are populated as fallback arguments to the jsxDEV() call. This correctly mirrors the previous behavior where __source/__self attributes (added by the old hooks) were extracted from the opening element attrs and passed as dedicated jsxDEV arguments.

Classic runtime (createElement) path — Lines 1107-1109: append_dev_attrs() appends __source/__self as JSX attributes before fold_attrs_for_classic converts them to object properties. This is correct and matches the previous behavior.

createElement fallback in automatic runtime — Lines 976-993: When use_create_element is true (spread attrs, etc.), __source/__self are added as object properties directly to props_obj. This correctly handles the case where the automatic runtime falls back to createElement (which doesn't have dedicated source/self parameters like jsxDEV).


Performance

This is a net positive for performance:

  • Eliminates two hook instantiations and their composite dispatch overhead
  • The enter_*/exit_* context tracking methods are now only invoked when development mode is on (since non-development Jsx hooks have empty ctx_stack operations that are trivially cheap but still had to be dispatched through CompositeHook before)
  • #[inline] annotations on the hot-path helpers (current_ctx, push_ctx, pop_ctx, should_inject_self) are appropriate

Minor note: ctx_stack: Vec<JsxSelfCtx> is allocated even in production mode where it's never used beyond the initial element. Since JsxSelfCtx is Copy and 2 bytes, and the Vec is initialized with a single element, this is negligible. If you wanted to squeeze out a bit more, you could wrap it in an Option or skip the enter/exit hooks with an early return when !self.development, but this is not worth the added complexity.


Potential Issues

  1. Dead modules: jsx_self and jsx_src modules are still declared in lib.rs (lines 16-17) and their public re-exports (jsx_self(), jsx_src() functions on lines 29-41) remain. These are presumably kept for backward compatibility, but if no external consumers use them, they could be removed in a follow-up to reduce dead code.

  2. Fragment dev metadata: The jsx_frag_to_expr method (line 685) does not inject __source/__self into fragments. This matches babel's behavior (fragments don't get these props), so this is correct. Just noting it for completeness.

  3. The NoopHook tail: The composite chain now ends with swc_ecma_hooks::NoopHook (line 114). This is fine — the compiler should optimize away all the no-op calls. This could be simplified if CompositeHook is ever updated to not require exactly two children, but it's not a problem.


Test Coverage

The PR description indicates the following test suites were run:

  • cargo test -p swc_ecma_transforms_typescript -p swc_ecma_transforms_react -p swc_ecma_transforms
  • cargo test -p swc --test projects --test tsc

This is adequate coverage. The existing fixture tests for __source and __self injection should catch any regressions from the inlining. No new tests are needed since this is a pure refactor with no behavioral change.


Verdict

Clean refactoring that correctly consolidates three hooks into one. The only actionable item is the stale comment at line 1420-1423. The rest is solid.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the React transform pipeline to inline development JSX metadata generation (__source, __self) into the main JSX hook, removing the need for separate jsx_src / jsx_self passes while keeping React Refresh and other hooks in the preset.

Changes:

  • Reordered the react() composite hook pipeline to remove standalone jsx_src / jsx_self hooks and rely on jsx::hook for dev metadata.
  • Added dev-mode __source / __self generation directly inside jsx/mod.rs, including constructor/derived-class suppression logic for __self.
  • Updated classic-runtime JSX element lowering to append dev attrs just before folding attributes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
crates/swc_ecma_transforms_react/src/lib.rs Removes jsx_src / jsx_self from the react() preset hook composition and simplifies the composite hook chain.
crates/swc_ecma_transforms_react/src/jsx/mod.rs Implements dev __source / __self injection inside the JSX transform and adds context tracking to mirror prior jsx_self behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +585 to +589
fn append_dev_attrs(&self, attrs: &mut Vec<JSXAttrOrSpread>, opening_span: Span) {
if !self.development {
return;
}

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dev-only __source/__self injection is now implemented inside the JSX transform (source_object_expr/append_dev_attrs/should_inject_self) instead of via separate passes. There don’t appear to be JSX transform tests asserting the generated __source/__self output (or the constructor/derived-class suppression behavior) for the integrated react() pipeline. Adding a focused test case would help prevent regressions in this newly inlined logic (including the createElement fallback path).

Copilot uses AI. Check for mistakes.
Comment on lines +977 to +986
if let Some(source) = self.source_object_expr(opening_span) {
props_obj
.props
.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("__source")),
value: source,
}))));
}

if self.should_inject_self() {
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the use_create_element development branch, __source/__self are appended unconditionally to props_obj. If the original JSX already included __source/__self attributes, the earlier loop will already have emitted those keys into props_obj, and this later injection will create duplicate keys (and override the user-provided value because it is appended last). Consider checking whether props_obj already contains these keys (or tracking booleans while folding attrs) before pushing the dev metadata entries.

Suggested change
if let Some(source) = self.source_object_expr(opening_span) {
props_obj
.props
.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("__source")),
value: source,
}))));
}
if self.should_inject_self() {
let has_source_prop = props_obj.props.iter().any(|prop_or_spread| {
if let PropOrSpread::Prop(prop) = prop_or_spread {
if let Prop::KeyValue(kv) = &**prop {
if let PropName::Ident(ident) = &kv.key {
return ident.sym == *"__source";
}
}
}
false
});
let has_self_prop = props_obj.props.iter().any(|prop_or_spread| {
if let PropOrSpread::Prop(prop) = prop_or_spread {
if let Prop::KeyValue(kv) = &**prop {
if let PropName::Ident(ident) = &kv.key {
return ident.sym == *"__self";
}
}
}
false
});
if let Some(source) = self.source_object_expr(opening_span) {
if !has_source_prop {
props_obj
.props
.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(quote_ident!("__source")),
value: source,
}))));
}
}
if self.should_inject_self() && !has_self_prop {

Copilot uses AI. Check for mistakes.
Comment on lines +1414 to 1418
fn exit_static_block(&mut self, _n: &mut StaticBlock, _ctx: &mut ()) {
self.pop_ctx();
}

/// Called after visiting children of an expression.
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment for exit_expr just below this point still says jsx_src/jsx_self have already added __source/__self. After inlining dev metadata generation into the JSX hook, that comment is now misleading and should be updated/removed to match the new flow.

Copilot uses AI. Check for mistakes.
@kdy1
Copy link
Copy Markdown
Member Author

kdy1 commented Feb 6, 2026

I'll review this later

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants