Skip to content

Commit 4192088

Browse files
committed
perf(cdk/table): Further defer direct dom measurement. In all cases I've observed, this fully eliminates layout thrashing from table init.
1 parent 070be9f commit 4192088

File tree

4 files changed

+161
-61
lines changed

4 files changed

+161
-61
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('CdkTableScrollContainer', () => {
3939
}
4040

4141
async function waitForLayout(): Promise<void> {
42-
await new Promise(resolve => setTimeout(resolve));
42+
await new Promise(resolve => setTimeout(resolve, 50));
4343

4444
// In newer versions of Chrome (change was noticed between 114 and 124), the computed
4545
// style of `::-webkit-scrollbar-track` doesn't update until the styles of the container

src/cdk/table/sticky-styler.ts

Lines changed: 123 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ interface UpdateStickyColumnsParams {
2222
stickyEndStates: boolean[];
2323
}
2424

25+
interface UpdateStickRowsParams {
26+
rowsToStick: HTMLElement[];
27+
stickyStates: boolean[];
28+
position: 'top' | 'bottom';
29+
}
30+
2531
/**
2632
* List of all possible directions that can be used for sticky positioning.
2733
* @docs-private
@@ -38,7 +44,10 @@ export class StickyStyler {
3844
? new globalThis.ResizeObserver(entries => this._updateCachedSizes(entries))
3945
: null;
4046
private _updatedStickyColumnsParamsToReplay: UpdateStickyColumnsParams[] = [];
41-
private _stickyColumnsReplayTimeout: number | null = null;
47+
private _updatedStickRowsParamsToReplay: UpdateStickRowsParams[] = [];
48+
private _stickyReplayTimeout: number | null = null;
49+
private _readSizeQueue: HTMLElement[] = [];
50+
private _readSizeTimeout: number | null = null;
4251
private _cachedCellWidths: number[] = [];
4352
private readonly _borderCellCss: Readonly<{[d in StickyDirection]: string}>;
4453

@@ -206,14 +215,27 @@ export class StickyStyler {
206215
* should be stuck in the particular top or bottom position.
207216
* @param position The position direction in which the row should be stuck if that row should be
208217
* sticky.
209-
*
218+
* @param replay Whether to enqueue this call for replay after a ResizeObserver update.
210219
*/
211-
stickRows(rowsToStick: HTMLElement[], stickyStates: boolean[], position: 'top' | 'bottom') {
220+
stickRows(
221+
rowsToStick: HTMLElement[],
222+
stickyStates: boolean[],
223+
position: 'top' | 'bottom',
224+
replay = true,
225+
) {
212226
// Since we can't measure the rows on the server, we can't stick the rows properly.
213227
if (!this._isBrowser) {
214228
return;
215229
}
216230

231+
if (replay) {
232+
this._updateStickRowsReplayQueue({
233+
rowsToStick: [...rowsToStick],
234+
stickyStates: [...stickyStates],
235+
position,
236+
});
237+
}
238+
217239
// Coalesce with other sticky row updates (top/bottom), sticky columns updates
218240
// (and potentially other changes like column resize).
219241
this._coalescedStyleScheduler.schedule(() => {
@@ -440,24 +462,55 @@ export class StickyStyler {
440462

441463
/**
442464
* Retreives the most recently observed size of the specified element from the cache, or
443-
* meaures it directly if not yet cached.
465+
* schedules it to be measured directly if not yet cached.
444466
*/
445467
private _retrieveElementSize(element: HTMLElement): {width: number; height: number} {
446468
const cachedSize = this._elemSizeCache.get(element);
447-
if (cachedSize) {
469+
if (cachedSize != null) {
448470
return cachedSize;
449471
}
450-
451-
const clientRect = element.getBoundingClientRect();
452-
const size = {width: clientRect.width, height: clientRect.height};
453-
454472
if (!this._resizeObserver) {
455-
return size;
473+
return this._retrieveElementSizeImmediate(element);
456474
}
457475

458-
this._elemSizeCache.set(element, size);
459476
this._resizeObserver.observe(element, {box: 'border-box'});
460-
return size;
477+
this._enqueueReadSize(element);
478+
479+
return {width: 0, height: 0};
480+
}
481+
482+
private _enqueueReadSize(element: HTMLElement): void {
483+
this._readSizeQueue.push(element);
484+
485+
if (!this._readSizeTimeout) {
486+
this._readSizeTimeout = setTimeout(() => {
487+
this._readSizeTimeout = null;
488+
489+
let needsReplay = false;
490+
for (const e of this._readSizeQueue) {
491+
if (this._elemSizeCache.get(e) != null) {
492+
continue;
493+
}
494+
495+
const size = this._retrieveElementSizeImmediate(e);
496+
this._elemSizeCache.set(e, size);
497+
needsReplay = true;
498+
}
499+
this._readSizeQueue = [];
500+
501+
if (needsReplay && !this._stickyReplayTimeout) {
502+
this._scheduleStickReplay();
503+
}
504+
}, 10);
505+
}
506+
}
507+
508+
/**
509+
* Returns the size of the specified element by direct measurement.
510+
*/
511+
private _retrieveElementSizeImmediate(element: HTMLElement): {width: number; height: number} {
512+
const clientRect = element.getBoundingClientRect();
513+
return {width: clientRect.width, height: clientRect.height};
461514
}
462515

463516
/**
@@ -468,7 +521,7 @@ export class StickyStyler {
468521
this._removeFromStickyColumnReplayQueue(params.rows);
469522

470523
// No need to replay if a flush is pending.
471-
if (this._stickyColumnsReplayTimeout) {
524+
if (this._stickyReplayTimeout) {
472525
return;
473526
}
474527

@@ -486,9 +539,22 @@ export class StickyStyler {
486539
);
487540
}
488541

542+
private _updateStickRowsReplayQueue(params: UpdateStickRowsParams) {
543+
// No need to replay if a flush is pending.
544+
if (this._stickyReplayTimeout) {
545+
return;
546+
}
547+
548+
this._updatedStickRowsParamsToReplay = this._updatedStickRowsParamsToReplay.filter(
549+
entry => entry.position !== params.position,
550+
);
551+
552+
this._updatedStickRowsParamsToReplay.push(params);
553+
}
554+
489555
/** Update _elemSizeCache with the observed sizes. */
490556
private _updateCachedSizes(entries: ResizeObserverEntry[]) {
491-
let needsColumnUpdate = false;
557+
let needsUpdate = false;
492558
for (const entry of entries) {
493559
const newEntry = entry.borderBoxSize?.length
494560
? {
@@ -500,35 +566,52 @@ export class StickyStyler {
500566
height: entry.contentRect.height,
501567
};
502568

569+
const cachedSize = this._elemSizeCache.get(entry.target as HTMLElement);
503570
if (
504-
newEntry.width !== this._elemSizeCache.get(entry.target as HTMLElement)?.width &&
505-
isCell(entry.target)
571+
(newEntry.width !== cachedSize?.width && isCell(entry.target)) ||
572+
(newEntry.height !== cachedSize?.height && isRow(entry.target))
506573
) {
507-
needsColumnUpdate = true;
574+
needsUpdate = true;
508575
}
509576

510577
this._elemSizeCache.set(entry.target as HTMLElement, newEntry);
511578
}
512579

513-
if (needsColumnUpdate && this._updatedStickyColumnsParamsToReplay.length) {
514-
if (this._stickyColumnsReplayTimeout) {
515-
clearTimeout(this._stickyColumnsReplayTimeout);
516-
}
580+
if (needsUpdate) {
581+
this._scheduleStickReplay();
582+
}
583+
}
517584

518-
this._stickyColumnsReplayTimeout = setTimeout(() => {
519-
for (const update of this._updatedStickyColumnsParamsToReplay) {
520-
this.updateStickyColumns(
521-
update.rows,
522-
update.stickyStartStates,
523-
update.stickyEndStates,
524-
true,
525-
false,
526-
);
527-
}
528-
this._updatedStickyColumnsParamsToReplay = [];
529-
this._stickyColumnsReplayTimeout = null;
530-
}, 0);
585+
/** Schedule a defered replay of enqueued sticky column operations. */
586+
private _scheduleStickReplay() {
587+
if (
588+
!this._updatedStickyColumnsParamsToReplay.length &&
589+
!this._updatedStickRowsParamsToReplay.length
590+
) {
591+
return;
531592
}
593+
594+
if (this._stickyReplayTimeout) {
595+
clearTimeout(this._stickyReplayTimeout);
596+
}
597+
598+
this._stickyReplayTimeout = setTimeout(() => {
599+
for (const update of this._updatedStickyColumnsParamsToReplay) {
600+
this.updateStickyColumns(
601+
update.rows,
602+
update.stickyStartStates,
603+
update.stickyEndStates,
604+
true,
605+
false,
606+
);
607+
}
608+
for (const update of this._updatedStickRowsParamsToReplay) {
609+
this.stickRows(update.rowsToStick, update.stickyStates, update.position, false);
610+
}
611+
this._updatedStickyColumnsParamsToReplay = [];
612+
this._updatedStickRowsParamsToReplay = [];
613+
this._stickyReplayTimeout = null;
614+
}, 0);
532615
}
533616
}
534617

@@ -537,3 +620,9 @@ function isCell(element: Element) {
537620
element.classList.contains(klass),
538621
);
539622
}
623+
624+
function isRow(element: Element) {
625+
return ['cdk-row', 'cdk-header-row', 'cdk-footer-row'].some(klass =>
626+
element.classList.contains(klass),
627+
);
628+
}

0 commit comments

Comments
 (0)