diff --git a/packages/core/src/render3/instructions/defer_events.ts b/packages/core/src/render3/instructions/defer_events.ts
index 4df351f10afb9..9e271d9b6d53d 100644
--- a/packages/core/src/render3/instructions/defer_events.ts
+++ b/packages/core/src/render3/instructions/defer_events.ts
@@ -176,6 +176,11 @@ class DeferIntersectionManager {
entry.callbacks.add(callback);
return () => {
+ // It's possible that a different cleanup callback fully removed this element already.
+ if (!this.viewportTriggers.has(trigger)) {
+ return;
+ }
+
entry!.callbacks.delete(callback);
if (entry!.callbacks.size === 0) {
diff --git a/packages/core/test/acceptance/defer_spec.ts b/packages/core/test/acceptance/defer_spec.ts
index 04f7d0d4b9167..5c1f96a741d74 100644
--- a/packages/core/test/acceptance/defer_spec.ts
+++ b/packages/core/test/acceptance/defer_spec.ts
@@ -3610,5 +3610,44 @@ describe('@defer', () => {
expect(loadingFnInvokedTimes).toBe(1);
}));
+
+ it('should load deferred content in a loop', fakeAsync(() => {
+ @Component({
+ standalone: true,
+ template: `
+ @for (item of items; track item) {
+ @defer (on viewport) {d{{item}} }
+ @placeholder {}
+ }
+ `
+ })
+ class MyCmp {
+ items = [1, 2, 3, 4, 5, 6];
+ }
+
+ const fixture = TestBed.createComponent(MyCmp);
+ fixture.detectChanges();
+ const buttons = Array.from(fixture.nativeElement.querySelectorAll('button'));
+ const items = fixture.componentInstance.items;
+
+ // None of the blocks are loaded yet.
+ expect(fixture.nativeElement.textContent.trim()).toBe('p1 p2 p3 p4 p5 p6');
+
+ // First half of the blocks is loaded.
+ for (let i = 0; i < items.length / 2; i++) {
+ MockIntersectionObserver.invokeCallbacksForElement(buttons[i], true);
+ fixture.detectChanges();
+ flush();
+ }
+ expect(fixture.nativeElement.textContent.trim()).toBe('d1 d2 d3 p4 p5 p6');
+
+ // Second half of the blocks is loaded.
+ for (let i = items.length / 2; i < items.length; i++) {
+ MockIntersectionObserver.invokeCallbacksForElement(buttons[i], true);
+ fixture.detectChanges();
+ flush();
+ }
+ expect(fixture.nativeElement.textContent.trim()).toBe('d1 d2 d3 d4 d5 d6');
+ }));
});
});