Profiler onRender only called when we do work (#19885)
If we didn't perform any work in the subtree, skip calling onRender.
Brian Vaughn committed
Sep 22, 2020 at 14:47 UTC
87c023b1c1b00d6776b7031f6e105913ead355da
4 files changed
+54
-28
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+3
@@ -1148,6 +1148,9 @@ function updateHostComponent(
1148
workInProgress.flags |= ContentReset;
1149
}
1150
1151
+ // React DevTools reads this flag.
1152
+ workInProgress.flags |= PerformedWork;
1153
+
1154
markRef(current, workInProgress);
1155
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1156
return workInProgress.child;
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+8
-3
@@ -77,6 +77,7 @@ import {
77
LayoutMask,
78
PassiveMask,
79
StaticMask,
80
+ PerformedWork,
81
} from './ReactFiberFlags';
82
import invariant from 'shared/invariant';
83
@@ -987,9 +988,13 @@ function completeWork(
988
const flags = workInProgress.flags;
989
let newFlags = flags;
990
990
- // Call onRender any time this fiber or its subtree are worked on, even
991
- // if there are no effects
992
- newFlags |= OnRenderFlag;
991
+ // Call onRender any time this fiber or its subtree are worked on.
992
+ if (
993
+ (flags & PerformedWork) !== NoFlags ||
994
+ (subtreeFlags & PerformedWork) !== NoFlags
995
+ ) {
996
+ newFlags |= OnRenderFlag;
997
+ }
998
999
// Call onCommit only if the subtree contains layout work, or if it
1000
// contains deletions, since those might result in unmount work, which
packages/react/src/__tests__/ReactDOMTracing-test.internal.js
+21
-12
@@ -151,12 +151,17 @@ describe('ReactDOMTracing', () => {
151
expect(
152
onInteractionScheduledWorkCompleted,
153
).toHaveBeenLastNotifiedOfInteraction(interaction);
154
- // TODO: This is 4 instead of 3 because this update was scheduled at
155
- // idle priority, and idle updates are slightly higher priority than
156
- // offscreen work. So it takes two render passes to finish it. Profiler
157
- // calls `onRender` for the first render even though everything
158
- // bails out.
159
- expect(onRender).toHaveBeenCalledTimes(4);
154
+
155
+ if (gate(flags => flags.new)) {
156
+ expect(onRender).toHaveBeenCalledTimes(3);
157
+ } else {
158
+ // TODO: This is 4 instead of 3 because this update was scheduled at
159
+ // idle priority, and idle updates are slightly higher priority than
160
+ // offscreen work. So it takes two render passes to finish it. Profiler
161
+ // calls `onRender` for the first render even though everything
162
+ // bails out.
163
+ expect(onRender).toHaveBeenCalledTimes(4);
164
+ }
165
expect(onRender).toHaveLastRenderedWithInteractions(
166
new Set([interaction]),
167
);
@@ -305,12 +310,16 @@ describe('ReactDOMTracing', () => {
310
expect(
311
onInteractionScheduledWorkCompleted,
312
).toHaveBeenLastNotifiedOfInteraction(interaction);
308
- // TODO: This is 4 instead of 3 because this update was scheduled at
309
- // idle priority, and idle updates are slightly higher priority than
310
- // offscreen work. So it takes two render passes to finish it. Profiler
311
- // calls `onRender` for the first render even though everything
312
- // bails out.
313
- expect(onRender).toHaveBeenCalledTimes(4);
313
+ if (gate(flags => flags.new)) {
314
+ expect(onRender).toHaveBeenCalledTimes(3);
315
+ } else {
316
+ // TODO: This is 4 instead of 3 because this update was scheduled at
317
+ // idle priority, and idle updates are slightly higher priority than
318
+ // offscreen work. So it takes two render passes to finish it. Profiler
319
+ // calls `onRender` for the first render even though everything
320
+ // bails out.
321
+ expect(onRender).toHaveBeenCalledTimes(4);
322
+ }
323
expect(onRender).toHaveLastRenderedWithInteractions(
324
new Set([interaction]),
325
);
packages/react/src/__tests__/ReactProfiler-test.internal.js
+22
-13
@@ -362,24 +362,33 @@ describe('Profiler', () => {
362
363
Scheduler.unstable_advanceTime(20); // 10 -> 30
364
365
- // Updating a parent should report a re-render,
366
- // since React technically did a little bit of work between the Profiler and the bailed out subtree.
365
renderer.update(<App />);
366
369
- expect(callback).toHaveBeenCalledTimes(1);
367
+ if (gate(flags => flags.new)) {
368
+ // None of the Profiler's subtree was rendered because App bailed out before the Profiler.
369
+ // So we expect onRender not to be called.
370
+ expect(callback).not.toHaveBeenCalled();
371
+ } else {
372
+ // Updating a parent reports a re-render,
373
+ // since React technically did a little bit of work between the Profiler and the bailed out subtree.
374
+ // This is not optimal but it's how the old reconciler fork works.
375
+ expect(callback).toHaveBeenCalledTimes(1);
376
371
- call = callback.mock.calls[0];
377
+ call = callback.mock.calls[0];
378
373
- expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
374
- expect(call[0]).toBe('test');
375
- expect(call[1]).toBe('update');
376
- expect(call[2]).toBe(0); // actual time
377
- expect(call[3]).toBe(10); // base time
378
- expect(call[4]).toBe(30); // start time
379
- expect(call[5]).toBe(30); // commit time
380
- expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
379
+ expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
380
+ expect(call[0]).toBe('test');
381
+ expect(call[1]).toBe('update');
382
+ expect(call[2]).toBe(0); // actual time
383
+ expect(call[3]).toBe(10); // base time
384
+ expect(call[4]).toBe(30); // start time
385
+ expect(call[5]).toBe(30); // commit time
386
+ expect(call[6]).toEqual(
387
+ enableSchedulerTracing ? new Set() : undefined,
388
+ ); // interaction events
389
382
- callback.mockReset();
390
+ callback.mockReset();
391
+ }
392
393
Scheduler.unstable_advanceTime(20); // 30 -> 50
394