From 8819fb6b42c5473be8a4da5405429be1661acf8c Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Mon, 9 Jun 2025 12:34:43 -0700 Subject: [PATCH 1/3] fix(cdk/scrolling): Prevent virtual scroll 'flickering' with zoneless This commit reworks the change detection coalescing to use signal and effects, which ensures the transform and the afterNextRender are applied within the same application tick without any awkward workarounds or fiddling with being inside or outside the zone --- src/cdk/scrolling/virtual-scroll-viewport.ts | 36 +++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/cdk/scrolling/virtual-scroll-viewport.ts b/src/cdk/scrolling/virtual-scroll-viewport.ts index 693c3cd92553..0abb4fa09993 100644 --- a/src/cdk/scrolling/virtual-scroll-viewport.ts +++ b/src/cdk/scrolling/virtual-scroll-viewport.ts @@ -10,10 +10,13 @@ import {ListRange} from '../collections'; import {Platform} from '../platform'; import { afterNextRender, + ApplicationRef, booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, + DestroyRef, + effect, ElementRef, inject, Inject, @@ -170,8 +173,7 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On */ private _renderedContentOffsetNeedsRewrite = false; - /** Whether there is a pending change detection cycle. */ - private _isChangeDetectionPending = false; + private _changeDetectionNeeded = signal(false); /** A list of functions to run after the next change detection cycle. */ private _runAfterChangeDetection: Function[] = []; @@ -202,6 +204,17 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On this.elementRef.nativeElement.classList.add('cdk-virtual-scrollable'); this.scrollable = this; } + + const ref = effect( + () => { + if (!this._changeDetectionNeeded()) { + return; + } + this._doChangeDetection(); + }, + {injector: inject(ApplicationRef).injector}, + ); + inject(DestroyRef).onDestroy(() => void ref.destroy()); } override ngOnInit() { @@ -488,16 +501,13 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On this._runAfterChangeDetection.push(runAfter); } - // Use a Promise to batch together calls to `_doChangeDetection`. This way if we set a bunch of - // properties sequentially we only have to run `_doChangeDetection` once at the end. - if (!this._isChangeDetectionPending) { - this._isChangeDetectionPending = true; - this.ngZone.runOutsideAngular(() => - Promise.resolve().then(() => { - this._doChangeDetection(); - }), - ); - } + this.ngZone.runOutsideAngular(() => { + Promise.resolve().then(() => { + this.ngZone.run(() => { + this._changeDetectionNeeded.set(true); + }); + }); + }); } /** Run change detection. */ @@ -520,7 +530,7 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On afterNextRender( () => { - this._isChangeDetectionPending = false; + this._changeDetectionNeeded.set(false); const runAfterChangeDetection = this._runAfterChangeDetection; this._runAfterChangeDetection = []; for (const fn of runAfterChangeDetection) { From 8e6ef8f9b5608305f51353e81942a4c79e907ae8 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 15 Jul 2025 13:23:06 -0700 Subject: [PATCH 2/3] fixup! fix(cdk/scrolling): Prevent virtual scroll 'flickering' with zoneless --- src/cdk/scrolling/virtual-scroll-viewport.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cdk/scrolling/virtual-scroll-viewport.ts b/src/cdk/scrolling/virtual-scroll-viewport.ts index 0abb4fa09993..98f46910be30 100644 --- a/src/cdk/scrolling/virtual-scroll-viewport.ts +++ b/src/cdk/scrolling/virtual-scroll-viewport.ts @@ -207,11 +207,12 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On const ref = effect( () => { - if (!this._changeDetectionNeeded()) { - return; + if (this._changeDetectionNeeded()) { + this._doChangeDetection(); } - this._doChangeDetection(); }, + // Using ApplicationRef injector is important here because we want this to be a root + // effect that runs before change detection of any application views (since we're depending on markForCheck marking parents dirty) {injector: inject(ApplicationRef).injector}, ); inject(DestroyRef).onDestroy(() => void ref.destroy()); From 9fde0d4a359830f7d0ced8c66612857c57a05dbd Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Wed, 16 Jul 2025 09:30:16 -0700 Subject: [PATCH 3/3] fixup! fix(cdk/scrolling): Prevent virtual scroll 'flickering' with zoneless --- src/cdk/scrolling/virtual-scroll-viewport.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cdk/scrolling/virtual-scroll-viewport.ts b/src/cdk/scrolling/virtual-scroll-viewport.ts index 98f46910be30..d37e3675607d 100644 --- a/src/cdk/scrolling/virtual-scroll-viewport.ts +++ b/src/cdk/scrolling/virtual-scroll-viewport.ts @@ -27,6 +27,7 @@ import { Optional, Output, signal, + untracked, ViewChild, ViewEncapsulation, } from '@angular/core'; @@ -502,6 +503,9 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On this._runAfterChangeDetection.push(runAfter); } + if (untracked(this._changeDetectionNeeded)) { + return; + } this.ngZone.runOutsideAngular(() => { Promise.resolve().then(() => { this.ngZone.run(() => {