Skip to content

Commit 5957299

Browse files
committed
perf(cdk/table): Use afterNextRender for sticky styling. Fixes a performance regression dating back to #28393 and removes need for coalesced sticky styler.
1 parent a6a70f6 commit 5957299

File tree

2 files changed

+166
-132
lines changed

2 files changed

+166
-132
lines changed

src/cdk/table/sticky-styler.ts

Lines changed: 161 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
* Directions that can be used when setting sticky positioning.
1111
* @docs-private
1212
*/
13+
import {afterNextRender, Injector} from '@angular/core';
1314
import {Direction} from '@angular/cdk/bidi';
14-
import {_CoalescedStyleScheduler} from './coalesced-style-scheduler';
1515
import {StickyPositioningListener} from './sticky-position-listener';
1616

1717
export type StickyDirection = 'top' | 'bottom' | 'left' | 'right';
@@ -41,6 +41,7 @@ export class StickyStyler {
4141
private _stickyColumnsReplayTimeout: number | null = null;
4242
private _cachedCellWidths: number[] = [];
4343
private readonly _borderCellCss: Readonly<{[d in StickyDirection]: string}>;
44+
private _destroyed = false;
4445

4546
/**
4647
* @param _isNativeHtmlTable Whether the sticky logic should be based on a table
@@ -60,10 +61,10 @@ export class StickyStyler {
6061
private _isNativeHtmlTable: boolean,
6162
private _stickCellCss: string,
6263
public direction: Direction,
63-
private _coalescedStyleScheduler: _CoalescedStyleScheduler,
6464
private _isBrowser = true,
6565
private readonly _needsPositionStickyOnElement = true,
6666
private readonly _positionListener?: StickyPositioningListener,
67+
private readonly _tableInjector?: Injector,
6768
) {
6869
this._borderCellCss = {
6970
'top': `${_stickCellCss}-border-elem-top`,
@@ -92,18 +93,20 @@ export class StickyStyler {
9293
continue;
9394
}
9495

95-
elementsToClear.push(row);
96-
for (let i = 0; i < row.children.length; i++) {
97-
elementsToClear.push(row.children[i] as HTMLElement);
98-
}
96+
elementsToClear.push(row, ...(Array.from(row.children) as HTMLElement[]));
9997
}
10098

10199
// Coalesce with sticky row/column updates (and potentially other changes like column resize).
102-
this._coalescedStyleScheduler.schedule(() => {
103-
for (const element of elementsToClear) {
104-
this._removeStickyStyle(element, stickyDirections);
105-
}
106-
});
100+
afterNextRender(
101+
{
102+
write: () => {
103+
for (const element of elementsToClear) {
104+
this._removeStickyStyle(element, stickyDirections);
105+
}
106+
},
107+
},
108+
{injector: this._tableInjector},
109+
);
107110
}
108111

109112
/**
@@ -147,54 +150,67 @@ export class StickyStyler {
147150
}
148151

149152
// Coalesce with sticky row updates (and potentially other changes like column resize).
150-
this._coalescedStyleScheduler.schedule(() => {
151-
const firstRow = rows[0];
152-
const numCells = firstRow.children.length;
153-
const cellWidths: number[] = this._getCellWidths(firstRow, recalculateCellWidths);
154-
155-
const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates);
156-
const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates);
157-
158-
const lastStickyStart = stickyStartStates.lastIndexOf(true);
159-
const firstStickyEnd = stickyEndStates.indexOf(true);
160-
161-
const isRtl = this.direction === 'rtl';
162-
const start = isRtl ? 'right' : 'left';
163-
const end = isRtl ? 'left' : 'right';
164-
165-
for (const row of rows) {
166-
for (let i = 0; i < numCells; i++) {
167-
const cell = row.children[i] as HTMLElement;
168-
if (stickyStartStates[i]) {
169-
this._addStickyStyle(cell, start, startPositions[i], i === lastStickyStart);
153+
const firstRow = rows[0];
154+
const numCells = firstRow.children.length;
155+
156+
const isRtl = this.direction === 'rtl';
157+
const start = isRtl ? 'right' : 'left';
158+
const end = isRtl ? 'left' : 'right';
159+
160+
const lastStickyStart = stickyStartStates.lastIndexOf(true);
161+
const firstStickyEnd = stickyEndStates.indexOf(true);
162+
163+
let cellWidths: number[];
164+
let startPositions: number[];
165+
let endPositions: number[];
166+
167+
afterNextRender(
168+
{
169+
earlyRead: () => {
170+
cellWidths = this._getCellWidths(firstRow, recalculateCellWidths);
171+
172+
startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates);
173+
endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates);
174+
},
175+
write: () => {
176+
for (const row of rows) {
177+
for (let i = 0; i < numCells; i++) {
178+
const cell = row.children[i] as HTMLElement;
179+
if (stickyStartStates[i]) {
180+
this._addStickyStyle(cell, start, startPositions[i], i === lastStickyStart);
181+
}
182+
183+
if (stickyEndStates[i]) {
184+
this._addStickyStyle(cell, end, endPositions[i], i === firstStickyEnd);
185+
}
186+
}
170187
}
171188

172-
if (stickyEndStates[i]) {
173-
this._addStickyStyle(cell, end, endPositions[i], i === firstStickyEnd);
189+
if (this._positionListener) {
190+
this._positionListener.stickyColumnsUpdated({
191+
sizes:
192+
lastStickyStart === -1
193+
? []
194+
: cellWidths
195+
.slice(0, lastStickyStart + 1)
196+
.map((width, index) => (stickyStartStates[index] ? width : null)),
197+
});
198+
this._positionListener.stickyEndColumnsUpdated({
199+
sizes:
200+
firstStickyEnd === -1
201+
? []
202+
: cellWidths
203+
.slice(firstStickyEnd)
204+
.map((width, index) =>
205+
stickyEndStates[index + firstStickyEnd] ? width : null,
206+
)
207+
.reverse(),
208+
});
174209
}
175-
}
176-
}
177-
178-
if (this._positionListener) {
179-
this._positionListener.stickyColumnsUpdated({
180-
sizes:
181-
lastStickyStart === -1
182-
? []
183-
: cellWidths
184-
.slice(0, lastStickyStart + 1)
185-
.map((width, index) => (stickyStartStates[index] ? width : null)),
186-
});
187-
this._positionListener.stickyEndColumnsUpdated({
188-
sizes:
189-
firstStickyEnd === -1
190-
? []
191-
: cellWidths
192-
.slice(firstStickyEnd)
193-
.map((width, index) => (stickyEndStates[index + firstStickyEnd] ? width : null))
194-
.reverse(),
195-
});
196-
}
197-
});
210+
},
211+
},
212+
{injector: this._tableInjector},
213+
);
198214
}
199215

200216
/**
@@ -214,64 +230,70 @@ export class StickyStyler {
214230
return;
215231
}
216232

217-
// Coalesce with other sticky row updates (top/bottom), sticky columns updates
218-
// (and potentially other changes like column resize).
219-
this._coalescedStyleScheduler.schedule(() => {
220-
// If positioning the rows to the bottom, reverse their order when evaluating the sticky
221-
// position such that the last row stuck will be "bottom: 0px" and so on. Note that the
222-
// sticky states need to be reversed as well.
223-
const rows = position === 'bottom' ? rowsToStick.slice().reverse() : rowsToStick;
224-
const states = position === 'bottom' ? stickyStates.slice().reverse() : stickyStates;
225-
226-
// Measure row heights all at once before adding sticky styles to reduce layout thrashing.
227-
const stickyOffsets: number[] = [];
228-
const stickyCellHeights: (number | undefined)[] = [];
229-
const elementsToStick: HTMLElement[][] = [];
230-
231-
for (let rowIndex = 0, stickyOffset = 0; rowIndex < rows.length; rowIndex++) {
232-
if (!states[rowIndex]) {
233-
continue;
234-
}
233+
// If positioning the rows to the bottom, reverse their order when evaluating the sticky
234+
// position such that the last row stuck will be "bottom: 0px" and so on. Note that the
235+
// sticky states need to be reversed as well.
236+
const rows = position === 'bottom' ? rowsToStick.slice().reverse() : rowsToStick;
237+
const states = position === 'bottom' ? stickyStates.slice().reverse() : stickyStates;
235238

236-
stickyOffsets[rowIndex] = stickyOffset;
237-
const row = rows[rowIndex];
238-
elementsToStick[rowIndex] = this._isNativeHtmlTable
239-
? (Array.from(row.children) as HTMLElement[])
240-
: [row];
241-
242-
const height = this._retrieveElementSize(row).height;
243-
stickyOffset += height;
244-
stickyCellHeights[rowIndex] = height;
245-
}
239+
// Measure row heights all at once before adding sticky styles to reduce layout thrashing.
240+
const stickyOffsets: number[] = [];
241+
const stickyCellHeights: (number | undefined)[] = [];
242+
const elementsToStick: HTMLElement[][] = [];
246243

247-
const borderedRowIndex = states.lastIndexOf(true);
248-
249-
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
250-
if (!states[rowIndex]) {
251-
continue;
252-
}
253-
254-
const offset = stickyOffsets[rowIndex];
255-
const isBorderedRowIndex = rowIndex === borderedRowIndex;
256-
for (const element of elementsToStick[rowIndex]) {
257-
this._addStickyStyle(element, position, offset, isBorderedRowIndex);
258-
}
259-
}
244+
// Coalesce with other sticky row updates (top/bottom), sticky columns updates
245+
// (and potentially other changes like column resize).
246+
afterNextRender(
247+
{
248+
earlyRead: () => {
249+
for (let rowIndex = 0, stickyOffset = 0; rowIndex < rows.length; rowIndex++) {
250+
if (!states[rowIndex]) {
251+
continue;
252+
}
253+
254+
stickyOffsets[rowIndex] = stickyOffset;
255+
const row = rows[rowIndex];
256+
elementsToStick[rowIndex] = this._isNativeHtmlTable
257+
? (Array.from(row.children) as HTMLElement[])
258+
: [row];
259+
260+
const height = this._retrieveElementSize(row).height;
261+
stickyOffset += height;
262+
stickyCellHeights[rowIndex] = height;
263+
}
264+
},
265+
write: () => {
266+
const borderedRowIndex = states.lastIndexOf(true);
267+
268+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
269+
if (!states[rowIndex]) {
270+
continue;
271+
}
272+
273+
const offset = stickyOffsets[rowIndex];
274+
const isBorderedRowIndex = rowIndex === borderedRowIndex;
275+
for (const element of elementsToStick[rowIndex]) {
276+
this._addStickyStyle(element, position, offset, isBorderedRowIndex);
277+
}
278+
}
260279

261-
if (position === 'top') {
262-
this._positionListener?.stickyHeaderRowsUpdated({
263-
sizes: stickyCellHeights,
264-
offsets: stickyOffsets,
265-
elements: elementsToStick,
266-
});
267-
} else {
268-
this._positionListener?.stickyFooterRowsUpdated({
269-
sizes: stickyCellHeights,
270-
offsets: stickyOffsets,
271-
elements: elementsToStick,
272-
});
273-
}
274-
});
280+
if (position === 'top') {
281+
this._positionListener?.stickyHeaderRowsUpdated({
282+
sizes: stickyCellHeights,
283+
offsets: stickyOffsets,
284+
elements: elementsToStick,
285+
});
286+
} else {
287+
this._positionListener?.stickyFooterRowsUpdated({
288+
sizes: stickyCellHeights,
289+
offsets: stickyOffsets,
290+
elements: elementsToStick,
291+
});
292+
}
293+
},
294+
},
295+
{injector: this._tableInjector},
296+
);
275297
}
276298

277299
/**
@@ -286,17 +308,30 @@ export class StickyStyler {
286308
}
287309

288310
// Coalesce with other sticky updates (and potentially other changes like column resize).
289-
this._coalescedStyleScheduler.schedule(() => {
290-
const tfoot = tableElement.querySelector('tfoot')!;
291-
292-
if (tfoot) {
293-
if (stickyStates.some(state => !state)) {
294-
this._removeStickyStyle(tfoot, ['bottom']);
295-
} else {
296-
this._addStickyStyle(tfoot, 'bottom', 0, false);
297-
}
298-
}
299-
});
311+
afterNextRender(
312+
{
313+
write: () => {
314+
const tfoot = tableElement.querySelector('tfoot')!;
315+
316+
if (tfoot) {
317+
if (stickyStates.some(state => !state)) {
318+
this._removeStickyStyle(tfoot, ['bottom']);
319+
} else {
320+
this._addStickyStyle(tfoot, 'bottom', 0, false);
321+
}
322+
}
323+
},
324+
},
325+
{injector: this._tableInjector},
326+
);
327+
}
328+
329+
destroy() {
330+
if (this._stickyColumnsReplayTimeout) {
331+
clearTimeout(this._stickyColumnsReplayTimeout);
332+
}
333+
334+
this._destroyed = true;
300335
}
301336

302337
/**
@@ -516,6 +551,10 @@ export class StickyStyler {
516551
}
517552

518553
this._stickyColumnsReplayTimeout = setTimeout(() => {
554+
if (this._destroyed) {
555+
return;
556+
}
557+
519558
for (const update of this._updatedStickyColumnsParamsToReplay) {
520559
this.updateStickyColumns(
521560
update.rows,

0 commit comments

Comments
 (0)