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