Skip to content

Commit 46bafb0

Browse files
crisbetoAndrewKushnir
authored andcommitted
fix(core): clean up afterRender after it is executed (#58119)
We stop tracking `afterRender` hooks as soon as they execute, but their on destroy callbacks stay registered until either the injector is destroyed or the user calls `destroy` manually. This was leading to memory leaks in the `@defer` triggers based on top of `afterRender` when placed inside long-lived views, because the callback would execute, but its destroy logic was waiting for the view to be destroyed. These changes resolve the issue by destroying the `AfterRenderRef` once it is executed. PR Close #58119
1 parent 9ca9db4 commit 46bafb0

File tree

2 files changed

+37
-0
lines changed

2 files changed

+37
-0
lines changed

packages/core/src/render3/after_render/manager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ export class AfterRenderImpl {
8282
sequence.afterRun();
8383
if (sequence.once) {
8484
this.sequences.delete(sequence);
85+
// Destroy the sequence so its on destroy callbacks can be cleaned up
86+
// immediately, instead of waiting until the injector is destroyed.
87+
sequence.destroy();
8588
}
8689
}
8790

packages/core/test/acceptance/after_render_hook_spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,40 @@ describe('after render hooks', () => {
13941394
appRef.tick();
13951395
}).toThrowError(/NG0103.*(Infinite change detection while refreshing application views)/);
13961396
});
1397+
1398+
it('should destroy after the hook has run', () => {
1399+
let hookRef: AfterRenderRef | null = null;
1400+
let afterRenderCount = 0;
1401+
1402+
@Component({selector: 'comp'})
1403+
class Comp {
1404+
constructor() {
1405+
hookRef = afterNextRender(() => {
1406+
afterRenderCount++;
1407+
});
1408+
}
1409+
}
1410+
1411+
TestBed.configureTestingModule({
1412+
declarations: [Comp],
1413+
...COMMON_CONFIGURATION,
1414+
});
1415+
createAndAttachComponent(Comp);
1416+
const appRef = TestBed.inject(ApplicationRef);
1417+
const destroySpy = spyOn(hookRef!, 'destroy').and.callThrough();
1418+
expect(afterRenderCount).toBe(0);
1419+
expect(destroySpy).not.toHaveBeenCalled();
1420+
1421+
// Run once and ensure that it was called and then cleaned up.
1422+
appRef.tick();
1423+
expect(afterRenderCount).toBe(1);
1424+
expect(destroySpy).toHaveBeenCalledTimes(1);
1425+
1426+
// Make sure we're not retaining it.
1427+
appRef.tick();
1428+
expect(afterRenderCount).toBe(1);
1429+
expect(destroySpy).toHaveBeenCalledTimes(1);
1430+
});
13971431
});
13981432

13991433
describe('server', () => {

0 commit comments

Comments
 (0)