Skip to content

Commit 9a35adc

Browse files
author
Brian Vaughn
authored
Only call Profiler onRender when a descendant had work (#17223)
1 parent 8eee0eb commit 9a35adc

File tree

2 files changed

+95
-27
lines changed

2 files changed

+95
-27
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2893,7 +2893,12 @@ function beginWork(
28932893
}
28942894
case Profiler:
28952895
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+
}
28972902
}
28982903
break;
28992904
case SuspenseComponent: {

packages/react/src/__tests__/ReactProfiler-test.internal.js

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,79 @@ describe('Profiler', () => {
295295
);
296296
});
297297

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+
298371
it('logs render times for both mount and update', () => {
299372
const callback = jest.fn();
300373

@@ -372,15 +445,15 @@ describe('Profiler', () => {
372445
Scheduler.unstable_advanceTime(5); // 0 -> 5
373446

374447
ReactTestRenderer.create(
375-
<>
448+
<React.Fragment>
376449
<React.Profiler id="parent" onRender={callback}>
377450
<AdvanceTime byAmount={10}>
378451
<React.Profiler id="child" onRender={callback}>
379452
<AdvanceTime byAmount={20} />
380453
</React.Profiler>
381454
</AdvanceTime>
382455
</React.Profiler>
383-
</>,
456+
</React.Fragment>,
384457
);
385458

386459
expect(callback).toHaveBeenCalledTimes(2);
@@ -407,14 +480,14 @@ describe('Profiler', () => {
407480
Scheduler.unstable_advanceTime(5); // 0 -> 5
408481

409482
ReactTestRenderer.create(
410-
<>
483+
<React.Fragment>
411484
<React.Profiler id="first" onRender={callback}>
412485
<AdvanceTime byAmount={20} />
413486
</React.Profiler>
414487
<React.Profiler id="second" onRender={callback}>
415488
<AdvanceTime byAmount={5} />
416489
</React.Profiler>
417-
</>,
490+
</React.Fragment>,
418491
);
419492

420493
expect(callback).toHaveBeenCalledTimes(2);
@@ -440,13 +513,13 @@ describe('Profiler', () => {
440513
Scheduler.unstable_advanceTime(5); // 0 -> 5
441514

442515
ReactTestRenderer.create(
443-
<>
516+
<React.Fragment>
444517
<AdvanceTime byAmount={20} />
445518
<React.Profiler id="test" onRender={callback}>
446519
<AdvanceTime byAmount={5} />
447520
</React.Profiler>
448521
<AdvanceTime byAmount={20} />
449-
</>,
522+
</React.Fragment>,
450523
);
451524

452525
expect(callback).toHaveBeenCalledTimes(1);
@@ -471,28 +544,18 @@ describe('Profiler', () => {
471544
}
472545
}
473546

474-
class Pure extends React.PureComponent {
475-
render() {
476-
return this.props.children;
477-
}
478-
}
479-
480547
const renderer = ReactTestRenderer.create(
481548
<React.Profiler id="outer" onRender={callback}>
482549
<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 />
489552
</React.Profiler>
490553
</Updater>
491554
</React.Profiler>,
492555
);
493556

494557
// All profile callbacks are called for initial render
495-
expect(callback).toHaveBeenCalledTimes(3);
558+
expect(callback).toHaveBeenCalledTimes(2);
496559

497560
callback.mockReset();
498561

@@ -502,11 +565,11 @@ describe('Profiler', () => {
502565
});
503566
});
504567

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');
510573
});
511574

512575
it('decreases actual time but not base time when sCU prevents an update', () => {
@@ -1455,11 +1518,11 @@ describe('Profiler', () => {
14551518
render() {
14561519
instance = this;
14571520
return (
1458-
<>
1521+
<React.Fragment>
14591522
<Yield value="first" />
14601523
{this.state.count}
14611524
<Yield value="last" />
1462-
</>
1525+
</React.Fragment>
14631526
);
14641527
}
14651528
}

0 commit comments

Comments
 (0)