Bug: performance: `createPortal()` attaches listeners even in contained roots
I'm working on a new virtualization engine for the MUI X Data Grid and other components. I was hoping to be able to do some work outside of react for performance reasons, and to tie React in with portals, but the base cost of adding portals is too high for that use-case due to the event listeners that are added on new roots.
React version: 19
Steps To Reproduce
- Create a portal inside the app's main root
Link to code example: https://stackblitz.com/edit/mui-virtual-exploration
The current behavior
React attaches event listeners to new portal roots even when the root node is contained by the main app root node. It creates a lot of overhead for cases where multiple portals are attached at once.
The expected behavior
No added listeners if the root is contained in the main root.
Since you’re already experimenting outside of React for performance reasons, have you considered leveraging native DOM manipulation for virtualization? Sometimes bypassing React's reconciliation in critical areas can dramatically improve efficiency.
If this issue turns out to be an actual bug, reporting it upstream and seeing if the React team acknowledges it would be a good next step. Have you noticed similar behavior in earlier React versions, or is this strictly something introduced in React 19?
I'm precisely trying to use native DOM for virtualization, the example above is a completed exploration of a virtualization engine that uses the DOM directly. The problem is that we need to support React-rendered cells' content because that's what our users use. So we need to call React to render content into our manually created cells. That's where createPortal() comes in, but it's not usable for that use-case due to the event listeners. So anyway, this means native DOM manipulation for virtualization is blocked by React.
The overhead of adding the event listeners on portal roots by React negates any gain made by native DOM manipulation.
I looked into this and it seems the performance issue is caused by how React adds event listeners to portal containers.
Right now, preparePortalMount() always calls listenToAllSupportedEvents() on the portal container — even when that container is already nested inside an existing React root. That means each portal adds a full set of listeners, even though the events would bubble up to the root anyway.
I tested a patch that skips adding listeners if the portal is nested inside a container that's already marked as a React root:
export function preparePortalMount(portalInstance: Instance): void {
// Skip adding another set of global listeners if this portal's container
// is already nested inside an existing React root. Events will bubble
// to the root container's listeners in that case.
let parentNode = portalInstance.parentNode;
while (parentNode !== null) {
if (isContainerMarkedAsRoot(((parentNode: any): Container))) {
return;
}
parentNode = parentNode.parentNode;
}
listenToAllSupportedEvents(portalInstance);
}
It seems to prevent redundant listeners in my test case and still works fine for portals outside the main root.
Would love to know if this would be considered a safe change — or if there are edge cases I might be overlooking (e.g. hydration or multi-root setups). I’m happy to send a PR if this sounds like a reasonable direction.
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!