fix(core): Clear lView from IcuIteratorState when stack is empty to prevent memory leak

If a component template contains an icu expression it is being retained until the next change detection cycle for that template. This results in a net retention of only ever a single copy of the given lView but that creates an opportunity for compounding leaks.

Change the icu i18n_icu_container_visitor to free the IcuIteratorState retained lView when the stack is empty so that garbage collection can occur when the view is discarded.
This commit is contained in:
Matt Lewis
2025-10-19 16:04:27 +01:00
committed by Kristiyan Kostadinov
parent bfcaf17005
commit 59e648913c
2 changed files with 25 additions and 2 deletions
@@ -15,7 +15,7 @@ import {TIcuContainerNode} from '../interfaces/node';
import {RNode} from '../interfaces/renderer_dom';
import {LView, TVIEW} from '../interfaces/view';
interface IcuIteratorState {
export interface IcuIteratorState {
stack: any[];
index: number;
lView?: LView;
@@ -35,7 +35,7 @@ function enterIcu(state: IcuIteratorState, tIcu: TIcu, lView: LView) {
}
}
function icuContainerIteratorNext(state: IcuIteratorState): RNode | null {
export function icuContainerIteratorNext(state: IcuIteratorState): RNode | null {
if (state.index < state.removes!.length) {
const removeOpCode = state.removes![state.index++] as number;
ngDevMode && assertNumber(removeOpCode, 'Expecting OpCode number');
@@ -54,6 +54,8 @@ function icuContainerIteratorNext(state: IcuIteratorState): RNode | null {
}
} else {
if (state.stack.length === 0) {
// Clear the lView reference when iteration completes to allow garbage collection
state.lView = undefined;
return null;
} else {
state.removes = state.stack.pop();
@@ -11,6 +11,10 @@ import {
getTranslationForTemplate,
i18nStartFirstCreatePass,
} from '../../../src/render3/i18n/i18n_parse';
import {
icuContainerIteratorNext,
IcuIteratorState,
} from '../../../src/render3/i18n/i18n_icu_container_visitor';
import {getTIcu} from '../../../src/render3/i18n/i18n_util';
import {TNodeType} from '../../../src/render3/interfaces/node';
@@ -584,6 +588,23 @@ describe('Runtime i18n', () => {
],
});
});
it('should clear lView reference when ICU iteration completes', () => {
const state: IcuIteratorState = {
stack: [],
index: 0,
removes: [] as any,
lView: [] as any, // Mock lView
};
// Call icuContainerIteratorNext with an empty removes array and empty stack
// This simulates the end of iteration
const result = icuContainerIteratorNext(state);
expect(result).toBeNull();
// Verify that lView should be cleared to allow garbage collection
expect(state.lView).toBeUndefined();
});
});
describe(`i18nAttribute`, () => {