Skip to content

Commit 7a7cd11

Browse files
kseamonandrewseguin
authored andcommitted
perf(cdk/table): Use afterRender hooks (#28354)
(cherry picked from commit 81cb5ac)
1 parent e707f6c commit 7a7cd11

File tree

6 files changed

+172
-77
lines changed

6 files changed

+172
-77
lines changed

src/cdk-experimental/column-resize/resize-strategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ export abstract class ResizeStrategy {
5555

5656
this.styleScheduler.schedule(() => {
5757
tableElement.style.width = coerceCssPixelValue(tableWidth + this._pendingResizeDelta!);
58-
5958
this._pendingResizeDelta = null;
6059
});
6160

6261
this.styleScheduler.scheduleEnd(() => {
6362
this.table.updateStickyColumnStyles();
63+
this.styleScheduler.flushAfterRender();
6464
});
6565
}
6666

src/cdk-experimental/table-scroll-container/table-scroll-container.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,21 +72,34 @@ export class CdkTableScrollContainer implements StickyPositioningListener, OnDes
7272
}
7373

7474
stickyColumnsUpdated({sizes}: StickyUpdate): void {
75+
if (arrayEquals(this._startSizes, sizes)) {
76+
return;
77+
}
7578
this._startSizes = sizes;
7679
this._updateScrollbar();
7780
}
7881

7982
stickyEndColumnsUpdated({sizes}: StickyUpdate): void {
83+
if (arrayEquals(this._endSizes, sizes)) {
84+
return;
85+
}
8086
this._endSizes = sizes;
8187
this._updateScrollbar();
8288
}
8389

8490
stickyHeaderRowsUpdated({sizes}: StickyUpdate): void {
91+
if (arrayEquals(this._headerSizes, sizes)) {
92+
return;
93+
}
8594
this._headerSizes = sizes;
8695
this._updateScrollbar();
8796
}
8897

8998
stickyFooterRowsUpdated({sizes}: StickyUpdate): void {
99+
console.log('sizes', this._footerSizes, sizes, arrayEquals(this._footerSizes, sizes));
100+
if (arrayEquals(this._footerSizes, sizes)) {
101+
return;
102+
}
90103
this._footerSizes = sizes;
91104
this._updateScrollbar();
92105
}
@@ -130,9 +143,13 @@ export class CdkTableScrollContainer implements StickyPositioningListener, OnDes
130143
/** Updates the stylesheet with the specified scrollbar style. */
131144
private _applyCss(value: string) {
132145
this._clearCss();
133-
134146
const selector = `.${this._uniqueClassName}::-webkit-scrollbar-track`;
135147
this._getStyleSheet().insertRule(`${selector} {margin: ${value}}`, 0);
148+
149+
// Force the scrollbar to paint.
150+
const display = this._elementRef.nativeElement.style.display;
151+
this._elementRef.nativeElement.style.display = 'none';
152+
this._elementRef.nativeElement.style.display = display;
136153
}
137154

138155
private _clearCss() {
@@ -153,3 +170,20 @@ function computeMargin(sizes: (number | null | undefined)[]): number {
153170
}
154171
return margin;
155172
}
173+
174+
function arrayEquals(a1: unknown[], a2: unknown[]) {
175+
if (a1 === a2) {
176+
return true;
177+
}
178+
if (a1.length !== a2.length) {
179+
return false;
180+
}
181+
182+
for (let index = 0; index < a1.length; index++) {
183+
if (a1[index] !== a2[index]) {
184+
return false;
185+
}
186+
}
187+
188+
return true;
189+
}

src/cdk/table/coalesced-style-scheduler.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Injectable, NgZone, OnDestroy, InjectionToken} from '@angular/core';
9+
import {
10+
Injectable,
11+
NgZone,
12+
OnDestroy,
13+
InjectionToken,
14+
afterRender,
15+
AfterRenderPhase,
16+
} from '@angular/core';
1017
import {from, Subject} from 'rxjs';
1118
import {take, takeUntil} from 'rxjs/operators';
1219

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

38-
constructor(private readonly _ngZone: NgZone) {}
45+
private readonly _earlyReadTasks: (() => unknown)[] = [];
46+
private readonly _writeTasks: (() => unknown)[] = [];
47+
private readonly _readTasks: (() => unknown)[] = [];
48+
49+
constructor(private readonly _ngZone: NgZone) {
50+
afterRender(() => flushTasks(this._earlyReadTasks), {phase: AfterRenderPhase.EarlyRead});
51+
afterRender(() => flushTasks(this._writeTasks), {phase: AfterRenderPhase.Write});
52+
afterRender(() => flushTasks(this._readTasks), {phase: AfterRenderPhase.Read});
53+
}
54+
55+
/**
56+
* Like afterNextRender(fn, AfterRenderPhase.EarlyRead), but can be called
57+
* outside of injection context. Runs after current/next CD.
58+
*/
59+
scheduleEarlyRead(task: () => unknown): void {
60+
this._earlyReadTasks.push(task);
61+
}
62+
63+
/**
64+
* Like afterNextRender(fn, AfterRenderPhase.Write), but can be called
65+
* outside of injection context. Runs after current/next CD.
66+
*/
67+
scheduleWrite(task: () => unknown): void {
68+
this._writeTasks.push(task);
69+
}
70+
71+
/**
72+
* Like afterNextRender(fn, AfterRenderPhase.Read), but can be called
73+
* outside of injection context. Runs after current/next CD.
74+
*/
75+
scheduleRead(task: () => unknown): void {
76+
this._readTasks.push(task);
77+
}
78+
79+
/** Greedily triggers pending EarlyRead, Write, and Read tasks, in that order. */
80+
flushAfterRender() {
81+
flushTasks(this._earlyReadTasks);
82+
flushTasks(this._writeTasks);
83+
flushTasks(this._readTasks);
84+
}
3985

4086
/**
4187
* Schedules the specified task to run at the end of the current VM turn.
@@ -99,3 +145,14 @@ export class _CoalescedStyleScheduler implements OnDestroy {
99145
: this._ngZone.onStable.pipe(take(1));
100146
}
101147
}
148+
149+
/**
150+
* Runs and removes tasks from the passed array in order.
151+
* Tasks appended mid-flight will also be flushed.
152+
*/
153+
function flushTasks(tasks: (() => unknown)[]) {
154+
let task: (() => unknown) | undefined;
155+
while ((task = tasks.shift())) {
156+
task();
157+
}
158+
}

src/cdk/table/sticky-styler.ts

Lines changed: 71 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,9 @@ export class StickyStyler {
8282
}
8383
}
8484

85-
// Coalesce with sticky row/column updates (and potentially other changes like column resize).
86-
this._coalescedStyleScheduler.schedule(() => {
87-
for (const element of elementsToClear) {
88-
this._removeStickyStyle(element, stickyDirections);
89-
}
90-
});
85+
for (const element of elementsToClear) {
86+
this._removeStickyStyle(element, stickyDirections);
87+
}
9188
}
9289

9390
/**
@@ -113,61 +110,63 @@ export class StickyStyler {
113110
!(stickyStartStates.some(state => state) || stickyEndStates.some(state => state))
114111
) {
115112
if (this._positionListener) {
116-
this._positionListener.stickyColumnsUpdated({sizes: []});
117-
this._positionListener.stickyEndColumnsUpdated({sizes: []});
113+
this._coalescedStyleScheduler.scheduleWrite(() => {
114+
this._positionListener!.stickyColumnsUpdated({sizes: []});
115+
this._positionListener!.stickyEndColumnsUpdated({sizes: []});
116+
});
118117
}
119118

120119
return;
121120
}
122121

123-
// Coalesce with sticky row updates (and potentially other changes like column resize).
124-
this._coalescedStyleScheduler.schedule(() => {
122+
this._coalescedStyleScheduler.scheduleEarlyRead(() => {
125123
const firstRow = rows[0];
126124
const numCells = firstRow.children.length;
127-
const cellWidths: number[] = this._getCellWidths(firstRow, recalculateCellWidths);
128-
129-
const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates);
130-
const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates);
131-
132125
const lastStickyStart = stickyStartStates.lastIndexOf(true);
133126
const firstStickyEnd = stickyEndStates.indexOf(true);
134127

135-
const isRtl = this.direction === 'rtl';
136-
const start = isRtl ? 'right' : 'left';
137-
const end = isRtl ? 'left' : 'right';
138-
139-
for (const row of rows) {
140-
for (let i = 0; i < numCells; i++) {
141-
const cell = row.children[i] as HTMLElement;
142-
if (stickyStartStates[i]) {
143-
this._addStickyStyle(cell, start, startPositions[i], i === lastStickyStart);
144-
}
128+
const cellWidths = this._getCellWidths(firstRow, recalculateCellWidths);
129+
const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates);
130+
const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates);
145131

146-
if (stickyEndStates[i]) {
147-
this._addStickyStyle(cell, end, endPositions[i], i === firstStickyEnd);
132+
this._coalescedStyleScheduler.scheduleWrite(() => {
133+
const isRtl = this.direction === 'rtl';
134+
const start = isRtl ? 'right' : 'left';
135+
const end = isRtl ? 'left' : 'right';
136+
137+
for (const row of rows) {
138+
for (let i = 0; i < numCells; i++) {
139+
const cell = row.children[i] as HTMLElement;
140+
if (stickyStartStates[i]) {
141+
this._addStickyStyle(cell, start, startPositions![i], i === lastStickyStart);
142+
}
143+
144+
if (stickyEndStates[i]) {
145+
this._addStickyStyle(cell, end, endPositions![i], i === firstStickyEnd);
146+
}
148147
}
149148
}
150-
}
151149

152-
if (this._positionListener) {
153-
this._positionListener.stickyColumnsUpdated({
154-
sizes:
155-
lastStickyStart === -1
156-
? []
157-
: cellWidths
158-
.slice(0, lastStickyStart + 1)
159-
.map((width, index) => (stickyStartStates[index] ? width : null)),
160-
});
161-
this._positionListener.stickyEndColumnsUpdated({
162-
sizes:
163-
firstStickyEnd === -1
164-
? []
165-
: cellWidths
166-
.slice(firstStickyEnd)
167-
.map((width, index) => (stickyEndStates[index + firstStickyEnd] ? width : null))
168-
.reverse(),
169-
});
170-
}
150+
if (this._positionListener) {
151+
this._positionListener.stickyColumnsUpdated({
152+
sizes:
153+
lastStickyStart === -1
154+
? []
155+
: cellWidths!
156+
.slice(0, lastStickyStart + 1)
157+
.map((width, index) => (stickyStartStates[index] ? width : null)),
158+
});
159+
this._positionListener.stickyEndColumnsUpdated({
160+
sizes:
161+
firstStickyEnd === -1
162+
? []
163+
: cellWidths!
164+
.slice(firstStickyEnd)
165+
.map((width, index) => (stickyEndStates[index + firstStickyEnd] ? width : null))
166+
.reverse(),
167+
});
168+
}
169+
});
171170
});
172171
}
173172

@@ -188,9 +187,7 @@ export class StickyStyler {
188187
return;
189188
}
190189

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

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

223-
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
224-
if (!states[rowIndex]) {
225-
continue;
225+
const offset = stickyOffsets[rowIndex];
226+
const isBorderedRowIndex = rowIndex === borderedRowIndex;
227+
for (const element of elementsToStick[rowIndex]) {
228+
this._addStickyStyle(element, position, offset, isBorderedRowIndex);
229+
}
226230
}
227231

228-
const offset = stickyOffsets[rowIndex];
229-
const isBorderedRowIndex = rowIndex === borderedRowIndex;
230-
for (const element of elementsToStick[rowIndex]) {
231-
this._addStickyStyle(element, position, offset, isBorderedRowIndex);
232+
if (position === 'top') {
233+
this._positionListener?.stickyHeaderRowsUpdated({
234+
sizes: stickyCellHeights,
235+
offsets: stickyOffsets,
236+
elements: elementsToStick,
237+
});
238+
} else {
239+
this._positionListener?.stickyFooterRowsUpdated({
240+
sizes: stickyCellHeights,
241+
offsets: stickyOffsets,
242+
elements: elementsToStick,
243+
});
232244
}
233-
}
234-
235-
if (position === 'top') {
236-
this._positionListener?.stickyHeaderRowsUpdated({
237-
sizes: stickyCellHeights,
238-
offsets: stickyOffsets,
239-
elements: elementsToStick,
240-
});
241-
} else {
242-
this._positionListener?.stickyFooterRowsUpdated({
243-
sizes: stickyCellHeights,
244-
offsets: stickyOffsets,
245-
elements: elementsToStick,
246-
});
247-
}
245+
});
248246
});
249247
}
250248

@@ -260,7 +258,7 @@ export class StickyStyler {
260258
}
261259

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

266264
if (stickyStates.some(state => !state)) {

src/cdk/table/table.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,9 +727,11 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
727727
if (this._ngZone && NgZone.isInAngularZone()) {
728728
this._ngZone.onStable.pipe(take(1), takeUntil(this._onDestroy)).subscribe(() => {
729729
this.updateStickyColumnStyles();
730+
this._coalescedStyleScheduler.flushAfterRender();
730731
});
731732
} else {
732733
this.updateStickyColumnStyles();
734+
this._coalescedStyleScheduler.flushAfterRender();
733735
}
734736

735737
this.contentChanged.next();

tools/public_api_guard/cdk/table.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,9 +423,13 @@ export const _COALESCED_STYLE_SCHEDULER: InjectionToken<_CoalescedStyleScheduler
423423
// @public
424424
export class _CoalescedStyleScheduler implements OnDestroy {
425425
constructor(_ngZone: NgZone);
426+
flushAfterRender(): void;
426427
ngOnDestroy(): void;
427428
schedule(task: () => unknown): void;
429+
scheduleEarlyRead(task: () => unknown): void;
428430
scheduleEnd(task: () => unknown): void;
431+
scheduleRead(task: () => unknown): void;
432+
scheduleWrite(task: () => unknown): void;
429433
// (undocumented)
430434
static ɵfac: i0.ɵɵFactoryDeclaration<_CoalescedStyleScheduler, never>;
431435
// (undocumented)

0 commit comments

Comments
 (0)