Skip to content

Commit

Permalink
perf(cdk/table): Use afterRender hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
kseamon committed Jan 9, 2024
1 parent 244bed4 commit d5f5cc1
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 44 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,33 @@ 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 {
if (arrayEquals(this._footerSizes, sizes)) {
return;
}
this._footerSizes = sizes;
this._updateScrollbar();
}
Expand Down Expand Up @@ -130,9 +142,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 +169,7 @@ function computeMargin(sizes: (number | null | undefined)[]): number {
}
return margin;
}

function arrayEquals(a1: unknown[], a2: unknown[]) {
return a1 === a2 || (a1.length === a2.length && a1.every((val, index) => a2[index] === val));
}
57 changes: 55 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,10 @@ export class _CoalescedStyleScheduler implements OnDestroy {
: this._ngZone.onStable.pipe(take(1));
}
}

function flushTasks(tasks: (() => unknown)[]) {
let task: (() => unknown) | undefined;
while ((task = tasks.shift())) {
task();
}
}
67 changes: 38 additions & 29 deletions src/cdk/table/sticky-styler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class StickyStyler {
}

// Coalesce with sticky row/column updates (and potentially other changes like column resize).
this._coalescedStyleScheduler.schedule(() => {
this._coalescedStyleScheduler.scheduleWrite(() => {
for (const element of elementsToClear) {
this._removeStickyStyle(element, stickyDirections);
}
Expand Down Expand Up @@ -113,25 +113,32 @@ 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;
}

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);

let cellWidths: number[];
let startPositions: number[];
let endPositions: number[];

this._coalescedStyleScheduler.scheduleEarlyRead(() => {
cellWidths = this._getCellWidths(firstRow, recalculateCellWidths);
startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates);
endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates);
});

// Coalesce with sticky row updates (and potentially other changes like column resize).
this._coalescedStyleScheduler.schedule(() => {
this._coalescedStyleScheduler.scheduleWrite(() => {
const isRtl = this.direction === 'rtl';
const start = isRtl ? 'right' : 'left';
const end = isRtl ? 'left' : 'right';
Expand All @@ -140,11 +147,11 @@ export class StickyStyler {
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);
this._addStickyStyle(cell, start, startPositions![i], i === lastStickyStart);
}

if (stickyEndStates[i]) {
this._addStickyStyle(cell, end, endPositions[i], i === firstStickyEnd);
this._addStickyStyle(cell, end, endPositions![i], i === firstStickyEnd);
}
}
}
Expand All @@ -154,15 +161,15 @@ export class StickyStyler {
sizes:
lastStickyStart === -1
? []
: cellWidths
: cellWidths!
.slice(0, lastStickyStart + 1)
.map((width, index) => (stickyStartStates[index] ? width : null)),
});
this._positionListener.stickyEndColumnsUpdated({
sizes:
firstStickyEnd === -1
? []
: cellWidths
: cellWidths!
.slice(firstStickyEnd)
.map((width, index) => (stickyEndStates[index + firstStickyEnd] ? width : null))
.reverse(),
Expand Down Expand Up @@ -198,27 +205,29 @@ export class StickyStyler {
const stickyOffsets: number[] = [];
const stickyCellHeights: (number | undefined)[] = [];
const elementsToStick: HTMLElement[][] = [];
for (let rowIndex = 0, stickyOffset = 0; rowIndex < rows.length; rowIndex++) {
if (!states[rowIndex]) {
continue;
}
const borderedRowIndex = states.lastIndexOf(true);

stickyOffsets[rowIndex] = stickyOffset;
const row = rows[rowIndex];
elementsToStick[rowIndex] = this._isNativeHtmlTable
? (Array.from(row.children) as HTMLElement[])
: [row];
this._coalescedStyleScheduler.scheduleEarlyRead(() => {
for (let rowIndex = 0, stickyOffset = 0; rowIndex < rows.length; rowIndex++) {
if (!states[rowIndex]) {
continue;
}

const height = row.getBoundingClientRect().height;
stickyOffset += height;
stickyCellHeights[rowIndex] = height;
}
stickyOffsets[rowIndex] = stickyOffset;
const row = rows[rowIndex];
elementsToStick[rowIndex] = this._isNativeHtmlTable
? (Array.from(row.children) as HTMLElement[])
: [row];

const borderedRowIndex = states.lastIndexOf(true);
const height = row.getBoundingClientRect().height;
stickyOffset += height;
stickyCellHeights[rowIndex] = height;
}
});

// Coalesce with other sticky row updates (top/bottom), sticky columns updates
// (and potentially other changes like column resize).
this._coalescedStyleScheduler.schedule(() => {
this._coalescedStyleScheduler.scheduleWrite(() => {
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
if (!states[rowIndex]) {
continue;
Expand Down Expand Up @@ -259,7 +268,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
13 changes: 2 additions & 11 deletions src/cdk/table/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import {
Subject,
Subscription,
} from 'rxjs';
import {take, takeUntil} from 'rxjs/operators';
import {takeUntil} from 'rxjs/operators';
import {CdkColumnDef} from './cell';
import {_CoalescedStyleScheduler, _COALESCED_STYLE_SCHEDULER} from './coalesced-style-scheduler';
import {
Expand Down Expand Up @@ -721,16 +721,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
});

this._updateNoDataRow();

// Allow the new row data to render before measuring it.
// @breaking-change 14.0.0 Remove undefined check once _ngZone is required.
if (this._ngZone && NgZone.isInAngularZone()) {
this._ngZone.onStable.pipe(take(1), takeUntil(this._onDestroy)).subscribe(() => {
this.updateStickyColumnStyles();
});
} else {
this.updateStickyColumnStyles();
}
this.updateStickyColumnStyles();

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 d5f5cc1

Please sign in to comment.