Skip to content

Commit a7c20f5

Browse files
committed
fix(cdk/scrolling): Prevent virtual scroll 'flickering' with zoneless
This fixes the perceived flickering due to the transform sometimes becoming visible with zoneless due to the macrotask-based scheduler. The transform cannot be simply moved inside `afterNextRender` because it causes differences in timing that can break certain scroll implementations (http://b/335066372). This approach fiddles with the details of `ngZone.run` and avoiding unnecessary extra ApplicationRef.tick calls. We could instead put everything inside an ngZone.run and call ApplicationRef.tick in there, but that would result in a second tick when the `ngZone.run` exits.
1 parent 43d3403 commit a7c20f5

File tree

1 file changed

+36
-17
lines changed

1 file changed

+36
-17
lines changed

src/cdk/scrolling/virtual-scroll-viewport.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {ListRange} from '../collections';
1010
import {Platform} from '../platform';
1111
import {
1212
afterNextRender,
13+
ApplicationRef,
1314
booleanAttribute,
1415
ChangeDetectionStrategy,
1516
ChangeDetectorRef,
@@ -181,6 +182,8 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
181182

182183
private _injector = inject(Injector);
183184

185+
private readonly _applicationRef = inject(ApplicationRef);
186+
184187
private _isDestroyed = false;
185188

186189
constructor(...args: unknown[]);
@@ -506,29 +509,45 @@ export class CdkVirtualScrollViewport extends CdkVirtualScrollable implements On
506509
return;
507510
}
508511

512+
// Apply the content transform. The transform can't be set via an Angular binding because
513+
// bypassSecurityTrustStyle is banned in Google. However the value is safe, it's composed of
514+
// string literals, a variable that can only be 'X' or 'Y', and user input that is run through
515+
// the `Number` function first to coerce it to a numeric value.
516+
this._contentWrapper.nativeElement.style.transform = this._renderedContentTransform;
517+
518+
let rendered = false;
519+
afterNextRender(
520+
() => {
521+
this._isChangeDetectionPending = false;
522+
const runAfterChangeDetection = this._runAfterChangeDetection;
523+
this._runAfterChangeDetection = [];
524+
for (const fn of runAfterChangeDetection) {
525+
fn();
526+
}
527+
rendered = true;
528+
},
529+
{injector: this._injector},
530+
);
531+
509532
this.ngZone.run(() => {
510533
// Apply changes to Angular bindings. Note: We must call `markForCheck` to run change detection
511534
// from the root, since the repeated items are content projected in. Calling `detectChanges`
512535
// instead does not properly check the projected content.
513536
this._changeDetectorRef.markForCheck();
537+
});
514538

515-
// Apply the content transform. The transform can't be set via an Angular binding because
516-
// bypassSecurityTrustStyle is banned in Google. However the value is safe, it's composed of
517-
// string literals, a variable that can only be 'X' or 'Y', and user input that is run through
518-
// the `Number` function first to coerce it to a numeric value.
519-
this._contentWrapper.nativeElement.style.transform = this._renderedContentTransform;
520-
521-
afterNextRender(
522-
() => {
523-
this._isChangeDetectionPending = false;
524-
const runAfterChangeDetection = this._runAfterChangeDetection;
525-
this._runAfterChangeDetection = [];
526-
for (const fn of runAfterChangeDetection) {
527-
fn();
528-
}
529-
},
530-
{injector: this._injector},
531-
);
539+
// In applications with NgZone, the above NgZone.run is likely to cause synchronous ApplicationRef.tick
540+
// because we execute this function outside the zone and run coalescing is usually off.
541+
// App synchronization needs to happen within the same microtask loop after applying the transform.
542+
// Otherwise, the transform can become visible and look like a "flicker" when scrolling due to
543+
// potential delays between the browser paint and the next tick.
544+
this.ngZone.runOutsideAngular(async () => {
545+
await Promise.resolve();
546+
if (!rendered && this._runAfterChangeDetection.length > 0) {
547+
this.ngZone.run(() => {
548+
this._applicationRef.tick();
549+
});
550+
}
532551
});
533552
}
534553

0 commit comments

Comments
 (0)