Only call Profiler onRender when a descendant had work (#17223)
Brian Vaughn committed
Oct 30, 2019 at 11:08 UTC
9a35adc96d6ff99de779aee52bb78d3c24f86c1d
2 files changed
+95
-27
packages/react-reconciler/src/ReactFiberBeginWork.js
+6
-1
@@ -2893,7 +2893,12 @@ function beginWork(
2893
}
2894
case Profiler:
2895
if (enableProfilerTimer) {
2896
- workInProgress.effectTag |= Update;
2896
+ // Profiler should only call onRender when one of its descendants actually rendered.
2897
+ const hasChildWork =
2898
+ workInProgress.childExpirationTime >= renderExpirationTime;
2899
+ if (hasChildWork) {
2900
+ workInProgress.effectTag |= Update;
2901
+ }
2902
}
2903
break;
2904
case SuspenseComponent: {
packages/react/src/__tests__/ReactProfiler-test.internal.js
+89
-26
@@ -295,6 +295,79 @@ describe('Profiler', () => {
295
);
296
});
297
298
+ it('does not report work done on a sibling', () => {
299
+ const callback = jest.fn();
300
+
301
+ const DoesNotUpdate = React.memo(function DoesNotUpdateInner() {
302
+ Scheduler.unstable_advanceTime(10);
303
+ return null;
304
+ }, () => true);
305
+
306
+ let updateProfilerSibling;
307
+
308
+ function ProfilerSibling() {
309
+ const [count, setCount] = React.useState(0);
310
+ updateProfilerSibling = () => setCount(count + 1);
311
+ return null;
312
+ }
313
+
314
+ function App() {
315
+ return (
316
+ <React.Fragment>
317
+ <React.Profiler id="test" onRender={callback}>
318
+ <DoesNotUpdate />
319
+ </React.Profiler>
320
+ <ProfilerSibling />
321
+ </React.Fragment>
322
+ );
323
+ }
324
+
325
+ const renderer = ReactTestRenderer.create(<App />);
326
+
327
+ expect(callback).toHaveBeenCalledTimes(1);
328
+
329
+ let call = callback.mock.calls[0];
330
+
331
+ expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
332
+ expect(call[0]).toBe('test');
333
+ expect(call[1]).toBe('mount');
334
+ expect(call[2]).toBe(10); // actual time
335
+ expect(call[3]).toBe(10); // base time
336
+ expect(call[4]).toBe(0); // start time
337
+ expect(call[5]).toBe(10); // commit time
338
+ expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
339
+
340
+ callback.mockReset();
341
+
342
+ Scheduler.unstable_advanceTime(20); // 10 -> 30
343
+
344
+ // Updating a parent should report a re-render,
345
+ // since React technically did a little bit of work between the Profiler and the bailed out subtree.
346
+ renderer.update(<App />);
347
+
348
+ expect(callback).toHaveBeenCalledTimes(1);
349
+
350
+ call = callback.mock.calls[0];
351
+
352
+ expect(call).toHaveLength(enableSchedulerTracing ? 7 : 6);
353
+ expect(call[0]).toBe('test');
354
+ expect(call[1]).toBe('update');
355
+ expect(call[2]).toBe(0); // actual time
356
+ expect(call[3]).toBe(10); // base time
357
+ expect(call[4]).toBe(30); // start time
358
+ expect(call[5]).toBe(30); // commit time
359
+ expect(call[6]).toEqual(enableSchedulerTracing ? new Set() : undefined); // interaction events
360
+
361
+ callback.mockReset();
362
+
363
+ Scheduler.unstable_advanceTime(20); // 30 -> 50
364
+
365
+ // Updating a sibling should not report a re-render.
366
+ ReactTestRenderer.act(updateProfilerSibling);
367
+
368
+ expect(callback).not.toHaveBeenCalled();
369
+ });
370
+
371
it('logs render times for both mount and update', () => {
372
const callback = jest.fn();
373
@@ -372,7 +445,7 @@ describe('Profiler', () => {
445
Scheduler.unstable_advanceTime(5); // 0 -> 5
446
447
ReactTestRenderer.create(
375
- <>
448
+ <React.Fragment>
449
<React.Profiler id="parent" onRender={callback}>
450
<AdvanceTime byAmount={10}>
451
<React.Profiler id="child" onRender={callback}>
@@ -380,7 +453,7 @@ describe('Profiler', () => {
453
</React.Profiler>
454
</AdvanceTime>
455
</React.Profiler>
383
- </>,
456
+ </React.Fragment>,
457
);
458
459
expect(callback).toHaveBeenCalledTimes(2);
@@ -407,14 +480,14 @@ describe('Profiler', () => {
480
Scheduler.unstable_advanceTime(5); // 0 -> 5
481
482
ReactTestRenderer.create(
410
- <>
483
+ <React.Fragment>
484
<React.Profiler id="first" onRender={callback}>
485
<AdvanceTime byAmount={20} />
486
</React.Profiler>
487
<React.Profiler id="second" onRender={callback}>
488
<AdvanceTime byAmount={5} />
489
</React.Profiler>
417
- </>,
490
+ </React.Fragment>,
491
);
492
493
expect(callback).toHaveBeenCalledTimes(2);
@@ -440,13 +513,13 @@ describe('Profiler', () => {
513
Scheduler.unstable_advanceTime(5); // 0 -> 5
514
515
ReactTestRenderer.create(
443
- <>
516
+ <React.Fragment>
517
<AdvanceTime byAmount={20} />
518
<React.Profiler id="test" onRender={callback}>
519
<AdvanceTime byAmount={5} />
520
</React.Profiler>
521
<AdvanceTime byAmount={20} />
449
- </>,
522
+ </React.Fragment>,
523
);
524
525
expect(callback).toHaveBeenCalledTimes(1);
@@ -471,28 +544,18 @@ describe('Profiler', () => {
544
}
545
}
546
474
- class Pure extends React.PureComponent {
475
- render() {
476
- return this.props.children;
477
- }
478
- }
479
-
547
const renderer = ReactTestRenderer.create(
548
<React.Profiler id="outer" onRender={callback}>
549
<Updater>
483
- <React.Profiler id="middle" onRender={callback}>
484
- <Pure>
485
- <React.Profiler id="inner" onRender={callback}>
486
- <div />
487
- </React.Profiler>
488
- </Pure>
550
+ <React.Profiler id="inner" onRender={callback}>
551
+ <div />
552
</React.Profiler>
553
</Updater>
554
</React.Profiler>,
555
);
556
557
// All profile callbacks are called for initial render
495
- expect(callback).toHaveBeenCalledTimes(3);
558
+ expect(callback).toHaveBeenCalledTimes(2);
559
560
callback.mockReset();
561
@@ -502,11 +565,11 @@ describe('Profiler', () => {
565
});
566
});
567
505
- // Only call profile updates for paths that have re-rendered
506
- // Since "inner" is beneath a pure component, it isn't called
507
- expect(callback).toHaveBeenCalledTimes(2);
508
- expect(callback.mock.calls[0][0]).toBe('middle');
509
- expect(callback.mock.calls[1][0]).toBe('outer');
568
+ // Only call onRender for paths that have re-rendered.
569
+ // Since the Updater's props didn't change,
570
+ // React does not re-render its children.
571
+ expect(callback).toHaveBeenCalledTimes(1);
572
+ expect(callback.mock.calls[0][0]).toBe('outer');
573
});
574
575
it('decreases actual time but not base time when sCU prevents an update', () => {
@@ -1455,11 +1518,11 @@ describe('Profiler', () => {
1518
render() {
1519
instance = this;
1520
return (
1458
- <>
1521
+ <React.Fragment>
1522
<Yield value="first" />
1523
{this.state.count}
1524
<Yield value="last" />
1462
- </>
1525
+ </React.Fragment>
1526
);
1527
}
1528
}