[compiler][wip] Inline single return JSX for known function components - #30901
[compiler][wip] Inline single return JSX for known function components#30901josephsavona wants to merge 4 commits into
Conversation
See comments, still WIP bc i'm getting a weird error with Babel and line numbers, one of the source locations must be off. [ghstack-poisoned]
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
| /** | ||
| * If a given component has a single return statement and returns JSX where the | ||
| * tag is known (via moduleTypeProvider) to be a function component, then it is | ||
| * safe to convert the JSX expression into a direct call to the component in question: | ||
| * | ||
| * function Parent() { | ||
| * return <Child /> | ||
| * } | ||
| * => | ||
| * function Parent() { | ||
| * return Child({}) | ||
| * } | ||
| */ |
There was a problem hiding this comment.
This is the basic idea. We have to know that the tag corresponds to a function component, though, for this transformation to be safe.
| function Component({a, b}) { | ||
| const c = [a, b]; | ||
| return ( | ||
| <Child value={c}> |
There was a problem hiding this comment.
Btw a deopt case to think about is if Child is passed a reactive key, since this should remount the component when it changes right?
There was a problem hiding this comment.
good call, yeah similar to the reactive tag check.
…n components" See comments, still WIP bc i'm getting a weird error with Babel and line numbers, one of the source locations must be off. [ghstack-poisoned]
| } else { | ||
| t1 = $[1]; | ||
| } | ||
| return Child({ value: a, children: t1 }); |
There was a problem hiding this comment.
If you emit:
const result = Child({ value: a, children: t1 });
if (__DEV__ && typeof result === 'object' && result !== null) {
(result._debugInfo || (result._debugInfo = [])).unshift({
name: 'Child',
});
}
return result;
It should show up in DevTools (built from main) as a separate Component. Same as Server Components.
There was a problem hiding this comment.
I'll probably add props too so might as well do:
.unshift({
name: 'Child',
props: { value: a, children: t1 }
});
There was a problem hiding this comment.
Ofc, if it's cached it probably shouldn't keep unshifting.
There was a problem hiding this comment.
Actually I think we have to wrap the result in a fragment and attach the debug info there. Bc there could be multiple levels of this inlining happening and we’d attach to the innermost result w the outermost component name.
There was a problem hiding this comment.
Note that RSC already does something similar without a fragment to avoid changing behavior.
This means that we also add this data to non-React types. Mostly an array of children also gets this or a Promise of children. Anything that’s a valid React node object. Maybe we should’ve used a symbol instead for this reason but faster this way.
The return value of an inlined function could itself be a return value from a server component which would also already have some debug info.
That’s why I conditionally added or reused the debug info array in my example.
Note that I also used unshift rather than push to add it to the beginning.
So if the inner one rendered Server Component > div. By adding it to the beginning we ensure the tree becomes Child > Server Component > div.
The same thing works for your example where it is multiple inlined because each level would add to the beginning and the outer one is the last to add it to the beginning so it’s correct.
There was a problem hiding this comment.
Note that wrapping in a keyless fragment is not actually the same because a keyless fragment is the same a no fragment when switching between them. But adding a keyless fragment at the root should change the behavior of a return value that conditionally renderers a keyless fragment or plain jsx. Since it would no longer be at the root.
So best to avoid changing the dev behavior since subtle things like this.
There was a problem hiding this comment.
Instead of mutating it could clone the react element or array from the child and clone the debug info.
There was a problem hiding this comment.
Ah, that makes sense in terms of the unshifting maintaining the stack. One other consideration is hooks. With this change, all hooks from the inlined component will presumably show up as part of the outer component. Thoughts on what to do there? For example, the compiler could emit (in dev) a call to some function to say "we're in component X now" so that subsequent hook calls can be associated to that component.
cc @hoxyq
There was a problem hiding this comment.
Disclaimer: I might be missing some context here.
This is the simplified algorithm of how hook tree parsing works in DevTools currently:
- Get the
renderfunction of the inspected component from its Fiber - Patch React Dispatcher so that every built-in hook call will populate a new frame in a custom callstack
- Call
renderfunction of the component - Parse the callstack and re-create the hook tree
With this change (in this PR), inlined single return JSX won't have a corresponding Fiber, right? Thats why @sebmarkbage is suggesting to patch it with _debugInfo, so that it is displayed as a virtual instance in React DevTools (same as Server Component). Since this is a virtual instance, there is no stateful node (like Fiber) that can be used.
For example, the compiler could emit (in dev) a call to some function to say "we're in component X now"
Parsing hook tree can be expensive, this is why we do this lazily on DevTools side, only when user selects some component. We could potentially add some API on the RDT global hook, like this:
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerComponentEntry(...)
Which will be no-op if user didn't select any component in RDT yet. Once user selects some component, RDT will:
- Patch this method with a real implementation
- Find lowest non-virtual parent instance (with a Fiber) for this component
- Call render function on this component
- Track hook calls and match them with the component entries
Thats just an idea of how this might look like, maybe there are better ways to do this, but we definitely should do this lazily on DevTools side
…n components" See comments, still WIP bc i'm getting a weird error with Babel and line numbers, one of the source locations must be off. [ghstack-poisoned]
…n components" See comments, still WIP bc i'm getting a weird error with Babel and line numbers, one of the source locations must be off. [ghstack-poisoned]
Stack from ghstack (oldest at bottom):
See comments, still WIP bc i'm getting a weird error with Babel and line numbers, one of the source locations must be off.