Skip to content

Commit

Permalink
perf(cdk/table): Use afterRender hooks (#28354)
Browse files Browse the repository at this point in the history
(cherry picked from commit 81cb5ac)
  • Loading branch information
kseamon authored and andrewseguin committed Jan 22, 2024
1 parent e707f6c commit 7a7cd11
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 77 deletions.
2 changes: 1 addition & 1 deletion src/cdk-experimental/column-resize/resize-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ export abstract class ResizeStrategy {

this.styleScheduler.schedule(() => {
tableElement.style.width = coerceCssPixelValue(tableWidth + this._pendingResizeDelta!);

this._pendingResizeDelta = null;
});

this.styleScheduler.scheduleEnd(() => {
this.table.updateStickyColumnStyles();
this.styleScheduler.flushAfterRender();
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,34 @@ export class CdkTableScrollContainer implements StickyPositioningListener, OnDes
}

stickyColumnsUpdated({sizes}: StickyUpdate): void {
if (arrayEquals(this._startSizes, sizes)) {
return;
}
this._startSizes = sizes;
this._updateScrollbar();
}

stickyEndColumnsUpdated({sizes}: StickyUpdate): void {
if (arrayEquals(this._endSizes, sizes)) {
return;
}
this._endSizes = sizes;
this._updateScrollbar();
}

stickyHeaderRowsUpdated({sizes}: StickyUpdate): void {
if (arrayEquals(this._headerSizes, sizes)) {
return;
}
this._headerSizes = sizes;
this._updateScrollbar();
}

stickyFooterRowsUpdated({sizes}: StickyUpdate): void {
console.log('sizes', this._footerSizes, sizes, arrayEquals(this._footerSizes, sizes));
if (arrayEquals(this._footerSizes, sizes)) {
return;
}
this._footerSizes = sizes;
this._updateScrollbar();
}
Expand Down Expand Up @@ -130,9 +143,13 @@ export class CdkTableScrollContainer implements StickyPositioningListener, OnDes
/** Updates the stylesheet with the specified scrollbar style. */
private _applyCss(value: string) {
this._clearCss();

const selector = `.${this._uniqueClassName}::-webkit-scrollbar-track`;
this._getStyleSheet().insertRule(`${selector} {margin: ${value}}`, 0);

// Force the scrollbar to paint.
const display = this._elementRef.nativeElement.style.display;
this._elementRef.nativeElement.style.display = 'none';
this._elementRef.nativeElement.style.display = display;
}

private _clearCss() {
Expand All @@ -153,3 +170,20 @@ function computeMargin(sizes: (number | null | undefined)[]): number {
}
return margin;
}

function arrayEquals(a1: unknown[], a2: unknown[]) {
if (a1 === a2) {
return true;
}
if (a1.length !== a2.length) {
return false;
}

for (let index = 0; index < a1.length; index++) {
if (a1[index] !== a2[index]) {
return false;
}
}

return true;
}
61 changes: 59 additions & 2 deletions src/cdk/table/coalesced-style-scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Injectable, NgZone, OnDestroy, InjectionToken} from '@angular/core';
import {
Injectable,
NgZone,
OnDestroy,
InjectionToken,
afterRender,
AfterRenderPhase,
} from '@angular/core';
import {from, Subject} from 'rxjs';
import {take, takeUntil} from 'rxjs/operators';

Expand Down Expand Up @@ -35,7 +42,46 @@ export class _CoalescedStyleScheduler implements OnDestroy {
private _currentSchedule: _Schedule | null = null;
private readonly _destroyed = new Subject<void>();

constructor(private readonly _ngZone: NgZone) {}
private readonly _earlyReadTasks: (() => unknown)[] = [];
private readonly _writeTasks: (() => unknown)[] = [];
private readonly _readTasks: (() => unknown)[] = [];

constructor(private readonly _ngZone: NgZone) {
afterRender(() => flushTasks(this._earlyReadTasks), {phase: AfterRenderPhase.EarlyRead});
afterRender(() => flushTasks(this._writeTasks), {phase: AfterRenderPhase.Write});
afterRender(() => flushTasks(this._readTasks), {phase: AfterRenderPhase.Read});
}

/**
* Like afterNextRender(fn, AfterRenderPhase.EarlyRead), but can be called
* outside of injection context. Runs after current/next CD.
*/
scheduleEarlyRead(task: () => unknown): void {
this._earlyReadTasks.push(task);
}

/**
* Like afterNextRender(fn, AfterRenderPhase.Write), but can be called
* outside of injection context. Runs after current/next CD.
*/
scheduleWrite(task: () => unknown): void {
this._writeTasks.push(task);
}

/**
* Like afterNextRender(fn, AfterRenderPhase.Read), but can be called
* outside of injection context. Runs after current/next CD.
*/
scheduleRead(task: () => unknown): void {
this._readTasks.push(task);
}

/** Greedily triggers pending EarlyRead, Write, and Read tasks, in that order. */
flushAfterRender() {
flushTasks(this._earlyReadTasks);
flushTasks(this._writeTasks);
flushTasks(this._readTasks);
}

/**
* Schedules the specified task to run at the end of the current VM turn.
Expand Down Expand Up @@ -99,3 +145,14 @@ export class _CoalescedStyleScheduler implements OnDestroy {
: this._ngZone.onStable.pipe(take(1));
}
}

/**
* Runs and removes tasks from the passed array in order.
* Tasks appended mid-flight will also be flushed.
*/
function flushTasks(tasks: (() => unknown)[]) {
let task: (() => unknown) | undefined;
while ((task = tasks.shift())) {
task();
}
}
144 changes: 71 additions & 73 deletions src/cdk/table/sticky-styler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,9 @@ export class StickyStyler {
}
}

// Coalesce with sticky row/column updates (and potentially other changes like column resize).
this._coalescedStyleScheduler.schedule(() => {
for (const element of elementsToClear) {
this._removeStickyStyle(element, stickyDirections);
}
});
for (const element of elementsToClear) {
this._removeStickyStyle(element, stickyDirections);
}
}

/**
Expand All @@ -113,61 +110,63 @@ export class StickyStyler {
!(stickyStartStates.some(state => state) || stickyEndStates.some(state => state))
) {
if (this._positionListener) {
this._positionListener.stickyColumnsUpdated({sizes: []});
this._positionListener.stickyEndColumnsUpdated({sizes: []});
this._coalescedStyleScheduler.scheduleWrite(() => {
this._positionListener!.stickyColumnsUpdated({sizes: []});
this._positionListener!.stickyEndColumnsUpdated({sizes: []});
});
}

return;
}

// Coalesce with sticky row updates (and potentially other changes like column resize).
this._coalescedStyleScheduler.schedule(() => {
this._coalescedStyleScheduler.scheduleEarlyRead(() => {
const firstRow = rows[0];
const numCells = firstRow.children.length;
const cellWidths: number[] = this._getCellWidths(firstRow, recalculateCellWidths);

const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates);
const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates);

const lastStickyStart = stickyStartStates.lastIndexOf(true);
const firstStickyEnd = stickyEndStates.indexOf(true);

const isRtl = this.direction === 'rtl';
const start = isRtl ? 'right' : 'left';
const end = isRtl ? 'left' : 'right';

for (const row of rows) {
for (let i = 0; i < numCells; i++) {
const cell = row.children[i] as HTMLElement;
if (stickyStartStates[i]) {
this._addStickyStyle(cell, start, startPositions[i], i === lastStickyStart);
}
const cellWidths = this._getCellWidths(firstRow, recalculateCellWidths);
const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates);
const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates);

if (stickyEndStates[i]) {
this._addStickyStyle(cell, end, endPositions[i], i === firstStickyEnd);
this._coalescedStyleScheduler.scheduleWrite(() => {
const isRtl = this.direction === 'rtl';
const start = isRtl ? 'right' : 'left';
const end = isRtl ? 'left' : 'right';

for (const row of rows) {
for (let i = 0; i < numCells; i++) {
const cell = row.children[i] as HTMLElement;
if (stickyStartStates[i]) {
this._addStickyStyle(cell, start, startPositions![i], i === lastStickyStart);
}

if (stickyEndStates[i]) {
this._addStickyStyle(cell, end, endPositions![i], i === firstStickyEnd);
}
}
}
}

if (this._positionListener) {
this._positionListener.stickyColumnsUpdated({
sizes:
lastStickyStart === -1
? []
: cellWidths
.slice(0, lastStickyStart + 1)
.map((width, index) => (stickyStartStates[index] ? width : null)),
});
this._positionListener.stickyEndColumnsUpdated({
sizes:
firstStickyEnd === -1
? []
: cellWidths
.slice(firstStickyEnd)
.map((width, index) => (stickyEndStates[index + firstStickyEnd] ? width : null))
.reverse(),
});
}
if (this._positionListener) {
this._positionListener.stickyColumnsUpdated({
sizes:
lastStickyStart === -1
? []
: cellWidths!
.slice(0, lastStickyStart + 1)
.map((width, index) => (stickyStartStates[index] ? width : null)),
});
this._positionListener.stickyEndColumnsUpdated({
sizes:
firstStickyEnd === -1
? []
: cellWidths!
.slice(firstStickyEnd)
.map((width, index) => (stickyEndStates[index + firstStickyEnd] ? width : null))
.reverse(),
});
}
});
});
}

Expand All @@ -188,9 +187,7 @@ export class StickyStyler {
return;
}

// Coalesce with other sticky row updates (top/bottom), sticky columns updates
// (and potentially other changes like column resize).
this._coalescedStyleScheduler.schedule(() => {
this._coalescedStyleScheduler.scheduleEarlyRead(() => {
// If positioning the rows to the bottom, reverse their order when evaluating the sticky
// position such that the last row stuck will be "bottom: 0px" and so on. Note that the
// sticky states need to be reversed as well.
Expand Down Expand Up @@ -219,32 +216,33 @@ export class StickyStyler {
}

const borderedRowIndex = states.lastIndexOf(true);
this._coalescedStyleScheduler.scheduleWrite(() => {
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
if (!states[rowIndex]) {
continue;
}

for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
if (!states[rowIndex]) {
continue;
const offset = stickyOffsets[rowIndex];
const isBorderedRowIndex = rowIndex === borderedRowIndex;
for (const element of elementsToStick[rowIndex]) {
this._addStickyStyle(element, position, offset, isBorderedRowIndex);
}
}

const offset = stickyOffsets[rowIndex];
const isBorderedRowIndex = rowIndex === borderedRowIndex;
for (const element of elementsToStick[rowIndex]) {
this._addStickyStyle(element, position, offset, isBorderedRowIndex);
if (position === 'top') {
this._positionListener?.stickyHeaderRowsUpdated({
sizes: stickyCellHeights,
offsets: stickyOffsets,
elements: elementsToStick,
});
} else {
this._positionListener?.stickyFooterRowsUpdated({
sizes: stickyCellHeights,
offsets: stickyOffsets,
elements: elementsToStick,
});
}
}

if (position === 'top') {
this._positionListener?.stickyHeaderRowsUpdated({
sizes: stickyCellHeights,
offsets: stickyOffsets,
elements: elementsToStick,
});
} else {
this._positionListener?.stickyFooterRowsUpdated({
sizes: stickyCellHeights,
offsets: stickyOffsets,
elements: elementsToStick,
});
}
});
});
}

Expand All @@ -260,7 +258,7 @@ export class StickyStyler {
}

// Coalesce with other sticky updates (and potentially other changes like column resize).
this._coalescedStyleScheduler.schedule(() => {
this._coalescedStyleScheduler.scheduleWrite(() => {
const tfoot = tableElement.querySelector('tfoot')!;

if (stickyStates.some(state => !state)) {
Expand Down
2 changes: 2 additions & 0 deletions src/cdk/table/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,9 +727,11 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
if (this._ngZone && NgZone.isInAngularZone()) {
this._ngZone.onStable.pipe(take(1), takeUntil(this._onDestroy)).subscribe(() => {
this.updateStickyColumnStyles();
this._coalescedStyleScheduler.flushAfterRender();
});
} else {
this.updateStickyColumnStyles();
this._coalescedStyleScheduler.flushAfterRender();
}

this.contentChanged.next();
Expand Down
4 changes: 4 additions & 0 deletions tools/public_api_guard/cdk/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,9 +423,13 @@ export const _COALESCED_STYLE_SCHEDULER: InjectionToken<_CoalescedStyleScheduler
// @public
export class _CoalescedStyleScheduler implements OnDestroy {
constructor(_ngZone: NgZone);
flushAfterRender(): void;
ngOnDestroy(): void;
schedule(task: () => unknown): void;
scheduleEarlyRead(task: () => unknown): void;
scheduleEnd(task: () => unknown): void;
scheduleRead(task: () => unknown): void;
scheduleWrite(task: () => unknown): void;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<_CoalescedStyleScheduler, never>;
// (undocumented)
Expand Down

0 comments on commit 7a7cd11

Please sign in to comment.