@samitouri / QOS-React / commits / 481dece580

Use recursion to traverse during mutation phase

Most of the commit phase uses iterative loops to traverse the tree. Originally we thought this would be faster than using recursion, but a while back @trueadm did some performance testing and found that the loop was slower because we assign to the `return` pointer before entering a subtree (which we have to do because the `return` pointer is not always consistent; it could point to one of two fibers). The other motivation is so we can take advantage of the JS stack to track contextual information, like the nearest host parent. We already use recursion in a few places; this changes the mutation phase to use it, too.

Andrew Clark committed Apr 8, 2022 at 00:45 UTC 481dece5808d1207acd66a6a0365efee729cf0f7
3 files changed +108 -86
packages/react-reconciler/src/ReactCurrentFiber.js
+10 -2
@@ -51,14 +51,22 @@ export function resetCurrentFiber() {
51 }
52 }
53
54 -export function setCurrentFiber(fiber: Fiber) {
54 +export function setCurrentFiber(fiber: Fiber | null) {
55 if (__DEV__) {
56 - ReactDebugCurrentFrame.getCurrentStack = getCurrentFiberStackInDev;
56 + ReactDebugCurrentFrame.getCurrentStack =
57 + fiber === null ? null : getCurrentFiberStackInDev;
58 current = fiber;
59 isRendering = false;
60 }
61 }
62
63 +export function getCurrentFiber(): Fiber | null {
64 + if (__DEV__) {
65 + return current;
66 + }
67 + return null;
68 +}
69 +
70 export function setIsRendering(rendering: boolean) {
71 if (__DEV__) {
72 isRendering = rendering;
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+49 -42
@@ -84,6 +84,7 @@ import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFrom
84 import {
85 resetCurrentFiber as resetCurrentDebugFiberInDEV,
86 setCurrentFiber as setCurrentDebugFiberInDEV,
87 + getCurrentFiber as getCurrentDebugFiberInDEV,
88 } from './ReactCurrentFiber';
89 import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
90 import {
@@ -1901,62 +1902,50 @@ export function isSuspenseBoundaryBeingHidden(
1902
1903 export function commitMutationEffects(
1904 root: FiberRoot,
1904 - firstChild: Fiber,
1905 + finishedWork: Fiber,
1906 committedLanes: Lanes,
1907 ) {
1908 inProgressLanes = committedLanes;
1909 inProgressRoot = root;
1909 - nextEffect = firstChild;
1910 + nextEffect = finishedWork;
1911
1911 - commitMutationEffects_begin(root, committedLanes);
1912 + setCurrentDebugFiberInDEV(finishedWork);
1913 + commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
1914 + setCurrentDebugFiberInDEV(finishedWork);
1915
1916 inProgressLanes = null;
1917 inProgressRoot = null;
1918 }
1919
1917 -function commitMutationEffects_begin(root: FiberRoot, lanes: Lanes) {
1918 - while (nextEffect !== null) {
1919 - const fiber = nextEffect;
1920 -
1921 - // TODO: Should wrap this in flags check, too, as optimization
1922 - const deletions = fiber.deletions;
1923 - if (deletions !== null) {
1924 - for (let i = 0; i < deletions.length; i++) {
1925 - const childToDelete = deletions[i];
1926 - try {
1927 - commitDeletion(root, childToDelete, fiber);
1928 - } catch (error) {
1929 - captureCommitPhaseError(childToDelete, fiber, error);
1930 - }
1920 +function recursivelyTraverseMutationEffects(
1921 + root: FiberRoot,
1922 + parentFiber: Fiber,
1923 + lanes: Lanes,
1924 +) {
1925 + // Deletions effects can be scheduled on any fiber type. They need to happen
1926 + // before the children effects hae fired.
1927 + const deletions = parentFiber.deletions;
1928 + if (deletions !== null) {
1929 + for (let i = 0; i < deletions.length; i++) {
1930 + const childToDelete = deletions[i];
1931 + try {
1932 + commitDeletion(root, childToDelete, parentFiber);
1933 + } catch (error) {
1934 + captureCommitPhaseError(childToDelete, parentFiber, error);
1935 }
1936 }
1933 -
1934 - const child = fiber.child;
1935 - if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) {
1936 - child.return = fiber;
1937 - nextEffect = child;
1938 - } else {
1939 - commitMutationEffects_complete(root, lanes);
1940 - }
1937 }
1942 -}
1938
1944 -function commitMutationEffects_complete(root: FiberRoot, lanes: Lanes) {
1945 - while (nextEffect !== null) {
1946 - const fiber = nextEffect;
1947 - setCurrentDebugFiberInDEV(fiber);
1948 - commitMutationEffectsOnFiber(fiber, root, lanes);
1949 - resetCurrentDebugFiberInDEV();
1950 -
1951 - const sibling = fiber.sibling;
1952 - if (sibling !== null) {
1953 - sibling.return = fiber.return;
1954 - nextEffect = sibling;
1955 - return;
1939 + const prevDebugFiber = getCurrentDebugFiberInDEV();
1940 + if (parentFiber.subtreeFlags & MutationMask) {
1941 + let child = parentFiber.child;
1942 + while (child !== null) {
1943 + setCurrentDebugFiberInDEV(child);
1944 + commitMutationEffectsOnFiber(child, root, lanes);
1945 + child = child.sibling;
1946 }
1957 -
1958 - nextEffect = fiber.return;
1947 }
1948 + setCurrentDebugFiberInDEV(prevDebugFiber);
1949 }
1950
1951 function commitMutationEffectsOnFiber(
@@ -1975,6 +1964,7 @@ function commitMutationEffectsOnFiber(
1964 case ForwardRef:
1965 case MemoComponent:
1966 case SimpleMemoComponent: {
1967 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
1968 commitReconciliationEffects(finishedWork);
1969
1970 if (flags & Update) {
@@ -2027,6 +2017,7 @@ function commitMutationEffectsOnFiber(
2017 return;
2018 }
2019 case ClassComponent: {
2020 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2021 commitReconciliationEffects(finishedWork);
2022
2023 if (flags & Ref) {
@@ -2037,6 +2028,7 @@ function commitMutationEffectsOnFiber(
2028 return;
2029 }
2030 case HostComponent: {
2031 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2032 commitReconciliationEffects(finishedWork);
2033
2034 if (flags & Ref) {
@@ -2045,7 +2037,13 @@ function commitMutationEffectsOnFiber(
2037 }
2038 }
2039 if (supportsMutation) {
2048 - if (flags & ContentReset) {
2040 + // TODO: ContentReset gets cleared by the children during the commit
2041 + // phase. This is a refactor hazard because it means we must read
2042 + // flags the flags after `commitReconciliationEffects` has already run;
2043 + // the order matters. We should refactor so that ContentReset does not
2044 + // rely on mutating the flag during commit. Like by setting a flag
2045 + // during the render phase instead.
2046 + if (finishedWork.flags & ContentReset) {
2047 const instance: Instance = finishedWork.stateNode;
2048 try {
2049 resetTextContent(instance);
@@ -2092,6 +2090,7 @@ function commitMutationEffectsOnFiber(
2090 return;
2091 }
2092 case HostText: {
2093 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2094 commitReconciliationEffects(finishedWork);
2095
2096 if (flags & Update) {
@@ -2121,6 +2120,7 @@ function commitMutationEffectsOnFiber(
2120 return;
2121 }
2122 case HostRoot: {
2123 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2124 commitReconciliationEffects(finishedWork);
2125
2126 if (flags & Update) {
@@ -2153,6 +2153,7 @@ function commitMutationEffectsOnFiber(
2153 return;
2154 }
2155 case HostPortal: {
2156 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2157 commitReconciliationEffects(finishedWork);
2158
2159 if (flags & Update) {
@@ -2170,6 +2171,7 @@ function commitMutationEffectsOnFiber(
2171 return;
2172 }
2173 case SuspenseComponent: {
2174 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2175 commitReconciliationEffects(finishedWork);
2176
2177 if (flags & Visibility) {
@@ -2194,6 +2196,7 @@ function commitMutationEffectsOnFiber(
2196 return;
2197 }
2198 case OffscreenComponent: {
2199 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2200 commitReconciliationEffects(finishedWork);
2201
2202 if (flags & Visibility) {
@@ -2231,6 +2234,7 @@ function commitMutationEffectsOnFiber(
2234 return;
2235 }
2236 case SuspenseListComponent: {
2237 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2238 commitReconciliationEffects(finishedWork);
2239
2240 if (flags & Update) {
@@ -2240,6 +2244,7 @@ function commitMutationEffectsOnFiber(
2244 }
2245 case ScopeComponent: {
2246 if (enableScopeAPI) {
2247 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2248 commitReconciliationEffects(finishedWork);
2249
2250 // TODO: This is a temporary solution that allowed us to transition away
@@ -2258,11 +2263,13 @@ function commitMutationEffectsOnFiber(
2263 return;
2264 }
2265 default: {
2266 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2267 commitReconciliationEffects(finishedWork);
2268 +
2269 + return;
2270 }
2271 }
2272 }
2265 -
2273 function commitReconciliationEffects(finishedWork: Fiber) {
2274 // Placement effects (insertions, reorders) can be scheduled on any fiber
2275 // type. They needs to happen after the children effects have fired, but
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+49 -42
@@ -84,6 +84,7 @@ import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFrom
84 import {
85 resetCurrentFiber as resetCurrentDebugFiberInDEV,
86 setCurrentFiber as setCurrentDebugFiberInDEV,
87 + getCurrentFiber as getCurrentDebugFiberInDEV,
88 } from './ReactCurrentFiber';
89 import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
90 import {
@@ -1901,62 +1902,50 @@ export function isSuspenseBoundaryBeingHidden(
1902
1903 export function commitMutationEffects(
1904 root: FiberRoot,
1904 - firstChild: Fiber,
1905 + finishedWork: Fiber,
1906 committedLanes: Lanes,
1907 ) {
1908 inProgressLanes = committedLanes;
1909 inProgressRoot = root;
1909 - nextEffect = firstChild;
1910 + nextEffect = finishedWork;
1911
1911 - commitMutationEffects_begin(root, committedLanes);
1912 + setCurrentDebugFiberInDEV(finishedWork);
1913 + commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
1914 + setCurrentDebugFiberInDEV(finishedWork);
1915
1916 inProgressLanes = null;
1917 inProgressRoot = null;
1918 }
1919
1917 -function commitMutationEffects_begin(root: FiberRoot, lanes: Lanes) {
1918 - while (nextEffect !== null) {
1919 - const fiber = nextEffect;
1920 -
1921 - // TODO: Should wrap this in flags check, too, as optimization
1922 - const deletions = fiber.deletions;
1923 - if (deletions !== null) {
1924 - for (let i = 0; i < deletions.length; i++) {
1925 - const childToDelete = deletions[i];
1926 - try {
1927 - commitDeletion(root, childToDelete, fiber);
1928 - } catch (error) {
1929 - captureCommitPhaseError(childToDelete, fiber, error);
1930 - }
1920 +function recursivelyTraverseMutationEffects(
1921 + root: FiberRoot,
1922 + parentFiber: Fiber,
1923 + lanes: Lanes,
1924 +) {
1925 + // Deletions effects can be scheduled on any fiber type. They need to happen
1926 + // before the children effects hae fired.
1927 + const deletions = parentFiber.deletions;
1928 + if (deletions !== null) {
1929 + for (let i = 0; i < deletions.length; i++) {
1930 + const childToDelete = deletions[i];
1931 + try {
1932 + commitDeletion(root, childToDelete, parentFiber);
1933 + } catch (error) {
1934 + captureCommitPhaseError(childToDelete, parentFiber, error);
1935 }
1936 }
1933 -
1934 - const child = fiber.child;
1935 - if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) {
1936 - child.return = fiber;
1937 - nextEffect = child;
1938 - } else {
1939 - commitMutationEffects_complete(root, lanes);
1940 - }
1937 }
1942 -}
1938
1944 -function commitMutationEffects_complete(root: FiberRoot, lanes: Lanes) {
1945 - while (nextEffect !== null) {
1946 - const fiber = nextEffect;
1947 - setCurrentDebugFiberInDEV(fiber);
1948 - commitMutationEffectsOnFiber(fiber, root, lanes);
1949 - resetCurrentDebugFiberInDEV();
1950 -
1951 - const sibling = fiber.sibling;
1952 - if (sibling !== null) {
1953 - sibling.return = fiber.return;
1954 - nextEffect = sibling;
1955 - return;
1939 + const prevDebugFiber = getCurrentDebugFiberInDEV();
1940 + if (parentFiber.subtreeFlags & MutationMask) {
1941 + let child = parentFiber.child;
1942 + while (child !== null) {
1943 + setCurrentDebugFiberInDEV(child);
1944 + commitMutationEffectsOnFiber(child, root, lanes);
1945 + child = child.sibling;
1946 }
1957 -
1958 - nextEffect = fiber.return;
1947 }
1948 + setCurrentDebugFiberInDEV(prevDebugFiber);
1949 }
1950
1951 function commitMutationEffectsOnFiber(
@@ -1975,6 +1964,7 @@ function commitMutationEffectsOnFiber(
1964 case ForwardRef:
1965 case MemoComponent:
1966 case SimpleMemoComponent: {
1967 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
1968 commitReconciliationEffects(finishedWork);
1969
1970 if (flags & Update) {
@@ -2027,6 +2017,7 @@ function commitMutationEffectsOnFiber(
2017 return;
2018 }
2019 case ClassComponent: {
2020 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2021 commitReconciliationEffects(finishedWork);
2022
2023 if (flags & Ref) {
@@ -2037,6 +2028,7 @@ function commitMutationEffectsOnFiber(
2028 return;
2029 }
2030 case HostComponent: {
2031 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2032 commitReconciliationEffects(finishedWork);
2033
2034 if (flags & Ref) {
@@ -2045,7 +2037,13 @@ function commitMutationEffectsOnFiber(
2037 }
2038 }
2039 if (supportsMutation) {
2048 - if (flags & ContentReset) {
2040 + // TODO: ContentReset gets cleared by the children during the commit
2041 + // phase. This is a refactor hazard because it means we must read
2042 + // flags the flags after `commitReconciliationEffects` has already run;
2043 + // the order matters. We should refactor so that ContentReset does not
2044 + // rely on mutating the flag during commit. Like by setting a flag
2045 + // during the render phase instead.
2046 + if (finishedWork.flags & ContentReset) {
2047 const instance: Instance = finishedWork.stateNode;
2048 try {
2049 resetTextContent(instance);
@@ -2092,6 +2090,7 @@ function commitMutationEffectsOnFiber(
2090 return;
2091 }
2092 case HostText: {
2093 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2094 commitReconciliationEffects(finishedWork);
2095
2096 if (flags & Update) {
@@ -2121,6 +2120,7 @@ function commitMutationEffectsOnFiber(
2120 return;
2121 }
2122 case HostRoot: {
2123 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2124 commitReconciliationEffects(finishedWork);
2125
2126 if (flags & Update) {
@@ -2153,6 +2153,7 @@ function commitMutationEffectsOnFiber(
2153 return;
2154 }
2155 case HostPortal: {
2156 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2157 commitReconciliationEffects(finishedWork);
2158
2159 if (flags & Update) {
@@ -2170,6 +2171,7 @@ function commitMutationEffectsOnFiber(
2171 return;
2172 }
2173 case SuspenseComponent: {
2174 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2175 commitReconciliationEffects(finishedWork);
2176
2177 if (flags & Visibility) {
@@ -2194,6 +2196,7 @@ function commitMutationEffectsOnFiber(
2196 return;
2197 }
2198 case OffscreenComponent: {
2199 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2200 commitReconciliationEffects(finishedWork);
2201
2202 if (flags & Visibility) {
@@ -2231,6 +2234,7 @@ function commitMutationEffectsOnFiber(
2234 return;
2235 }
2236 case SuspenseListComponent: {
2237 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2238 commitReconciliationEffects(finishedWork);
2239
2240 if (flags & Update) {
@@ -2240,6 +2244,7 @@ function commitMutationEffectsOnFiber(
2244 }
2245 case ScopeComponent: {
2246 if (enableScopeAPI) {
2247 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2248 commitReconciliationEffects(finishedWork);
2249
2250 // TODO: This is a temporary solution that allowed us to transition away
@@ -2258,11 +2263,13 @@ function commitMutationEffectsOnFiber(
2263 return;
2264 }
2265 default: {
2266 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2267 commitReconciliationEffects(finishedWork);
2268 +
2269 + return;
2270 }
2271 }
2272 }
2265 -
2273 function commitReconciliationEffects(finishedWork: Fiber) {
2274 // Placement effects (insertions, reorders) can be scheduled on any fiber
2275 // type. They needs to happen after the children effects have fired, but