Skip to content

Commit 0289fd3

Browse files
committed
perf(table) Coalesces style updates after style measurements to reduce layout thrashing Exposes the CoalescedStyleScheduler for use by other related components in a table such as column resize.
1 parent dfc279f commit 0289fd3

File tree

8 files changed

+181
-78
lines changed

8 files changed

+181
-78
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {Injectable, NgZone} from '@angular/core';
2+
import {from, Observable} from 'rxjs';
3+
4+
@Injectable()
5+
export class CoalescedStyleScheduler {
6+
private _currentSchedule: Observable<void>|null = null;
7+
8+
constructor(private readonly _ngZone: NgZone) {}
9+
10+
schedule(task: () => unknown): void {
11+
this._createScheduleIfNeeded();
12+
13+
this._currentSchedule!.subscribe(task);
14+
}
15+
16+
private _createScheduleIfNeeded() {
17+
if (this._currentSchedule) { return; }
18+
19+
this._ngZone.runOutsideAngular(() => {
20+
this._currentSchedule = from(new Promise<void>((resolve) => {
21+
this._currentSchedule = null;
22+
resolve(undefined);
23+
}));
24+
});
25+
}
26+
}

src/cdk/table/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
export * from './table';
1010
export * from './cell';
11+
export * from './coalesced-style-scheduler';
1112
export * from './row';
1213
export * from './table-module';
1314
export * from './sticky-styler';

src/cdk/table/sticky-styler.ts

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* @docs-private
1212
*/
1313
import {Direction} from '@angular/cdk/bidi';
14+
import {CoalescedStyleScheduler} from './coalesced-style-scheduler';
1415

1516
export type StickyDirection = 'top' | 'bottom' | 'left' | 'right';
1617

@@ -37,6 +38,7 @@ export class StickyStyler {
3738
constructor(private _isNativeHtmlTable: boolean,
3839
private _stickCellCss: string,
3940
public direction: Direction,
41+
private _coalescedStyleScheduler: CoalescedStyleScheduler,
4042
private _isBrowser = true) { }
4143

4244
/**
@@ -46,20 +48,25 @@ export class StickyStyler {
4648
* @param stickyDirections The directions that should no longer be set as sticky on the rows.
4749
*/
4850
clearStickyPositioning(rows: HTMLElement[], stickyDirections: StickyDirection[]) {
51+
const elementsToClear: HTMLElement[] = [];
4952
for (const row of rows) {
5053
// If the row isn't an element (e.g. if it's an `ng-container`),
5154
// it won't have inline styles or `children` so we skip it.
5255
if (row.nodeType !== row.ELEMENT_NODE) {
5356
continue;
5457
}
5558

56-
this._removeStickyStyle(row, stickyDirections);
57-
59+
elementsToClear.push(row);
5860
for (let i = 0; i < row.children.length; i++) {
59-
const cell = row.children[i] as HTMLElement;
60-
this._removeStickyStyle(cell, stickyDirections);
61+
elementsToClear.push(row.children[i] as HTMLElement);
6162
}
6263
}
64+
65+
this._coalescedStyleScheduler.schedule(() => {
66+
for (const element of elementsToClear) {
67+
this._removeStickyStyle(element, stickyDirections);
68+
}
69+
});
6370
}
6471

6572
/**
@@ -73,9 +80,8 @@ export class StickyStyler {
7380
*/
7481
updateStickyColumns(
7582
rows: HTMLElement[], stickyStartStates: boolean[], stickyEndStates: boolean[]) {
76-
const hasStickyColumns =
77-
stickyStartStates.some(state => state) || stickyEndStates.some(state => state);
78-
if (!rows.length || !hasStickyColumns || !this._isBrowser) {
83+
if (!rows.length || !this._isBrowser || !(stickyStartStates.some(state => state) ||
84+
stickyEndStates.some(state => state))) {
7985
return;
8086
}
8187

@@ -85,20 +91,25 @@ export class StickyStyler {
8591

8692
const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates);
8793
const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates);
88-
const isRtl = this.direction === 'rtl';
89-
90-
for (const row of rows) {
91-
for (let i = 0; i < numCells; i++) {
92-
const cell = row.children[i] as HTMLElement;
93-
if (stickyStartStates[i]) {
94-
this._addStickyStyle(cell, isRtl ? 'right' : 'left', startPositions[i]);
95-
}
9694

97-
if (stickyEndStates[i]) {
98-
this._addStickyStyle(cell, isRtl ? 'left' : 'right', endPositions[i]);
95+
this._coalescedStyleScheduler.schedule(() => {
96+
const isRtl = this.direction === 'rtl';
97+
const start = isRtl ? 'right' : 'left';
98+
const end = isRtl ? 'left' : 'right';
99+
100+
for (const row of rows) {
101+
for (let i = 0; i < numCells; i++) {
102+
const cell = row.children[i] as HTMLElement;
103+
if (stickyStartStates[i]) {
104+
this._addStickyStyle(cell, start, startPositions[i]);
105+
}
106+
107+
if (stickyEndStates[i]) {
108+
this._addStickyStyle(cell, end, endPositions[i]);
109+
}
99110
}
100111
}
101-
}
112+
});
102113
}
103114

104115
/**
@@ -124,30 +135,37 @@ export class StickyStyler {
124135
const rows = position === 'bottom' ? rowsToStick.slice().reverse() : rowsToStick;
125136
const states = position === 'bottom' ? stickyStates.slice().reverse() : stickyStates;
126137

127-
let stickyHeight = 0;
128-
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
138+
// Measure row heights all at once before adding sticky styles to reduce layout thrashing.
139+
const stickyHeights: number[] = [];
140+
const elementsToStick: HTMLElement[][] = [];
141+
for (let rowIndex = 0, stickyHeight = 0; rowIndex < rows.length; rowIndex++) {
142+
stickyHeights[rowIndex] = stickyHeight;
143+
129144
if (!states[rowIndex]) {
130145
continue;
131146
}
132147

133148
const row = rows[rowIndex];
134-
if (this._isNativeHtmlTable) {
135-
for (let j = 0; j < row.children.length; j++) {
136-
const cell = row.children[j] as HTMLElement;
137-
this._addStickyStyle(cell, position, stickyHeight);
138-
}
139-
} else {
140-
// Flex does not respect the stick positioning on the cells, needs to be applied to the row.
141-
// If this is applied on a native table, Safari causes the header to fly in wrong direction.
142-
this._addStickyStyle(row, position, stickyHeight);
143-
}
149+
elementsToStick[rowIndex] = this._isNativeHtmlTable ?
150+
Array.from(row.children) as HTMLElement[] : [row];
144151

145-
if (rowIndex === rows.length - 1) {
146-
// prevent unnecessary reflow from getBoundingClientRect()
147-
return;
152+
if (rowIndex !== rows.length - 1) {
153+
stickyHeight += row.getBoundingClientRect().height;
148154
}
149-
stickyHeight += row.getBoundingClientRect().height;
150155
}
156+
157+
this._coalescedStyleScheduler.schedule(() => {
158+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
159+
if (!states[rowIndex]) {
160+
continue;
161+
}
162+
163+
const height = stickyHeights[rowIndex];
164+
for (const element of elementsToStick[rowIndex]) {
165+
this._addStickyStyle(element, position, height);
166+
}
167+
}
168+
});
151169
}
152170

153171
/**
@@ -162,11 +180,14 @@ export class StickyStyler {
162180
}
163181

164182
const tfoot = tableElement.querySelector('tfoot')!;
165-
if (stickyStates.some(state => !state)) {
166-
this._removeStickyStyle(tfoot, ['bottom']);
167-
} else {
168-
this._addStickyStyle(tfoot, 'bottom', 0);
169-
}
183+
184+
this._coalescedStyleScheduler.schedule(() => {
185+
if (stickyStates.some(state => !state)) {
186+
this._removeStickyStyle(tfoot, ['bottom']);
187+
} else {
188+
this._addStickyStyle(tfoot, 'bottom', 0);
189+
}
190+
});
170191
}
171192

172193
/**

0 commit comments

Comments
 (0)