Skip to content

Commit fd9b3e2

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 fd9b3e2

File tree

4 files changed

+159
-61
lines changed

4 files changed

+159
-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, 10));
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: 121 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,53 @@ 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+
for (const e of this._readSizeQueue) {
490+
if (this._elemSizeCache.get(e) != null) {
491+
continue;
492+
}
493+
494+
const size = this._retrieveElementSizeImmediate(e);
495+
this._elemSizeCache.set(e, size);
496+
}
497+
this._readSizeQueue = [];
498+
499+
if (!this._stickyReplayTimeout) {
500+
this._scheduleStickReplay();
501+
}
502+
}, 10);
503+
}
504+
}
505+
506+
/**
507+
* Returns the size of the specified element by direct measurement.
508+
*/
509+
private _retrieveElementSizeImmediate(element: HTMLElement): {width: number; height: number} {
510+
const clientRect = element.getBoundingClientRect();
511+
return {width: clientRect.width, height: clientRect.height};
461512
}
462513

463514
/**
@@ -468,7 +519,7 @@ export class StickyStyler {
468519
this._removeFromStickyColumnReplayQueue(params.rows);
469520

470521
// No need to replay if a flush is pending.
471-
if (this._stickyColumnsReplayTimeout) {
522+
if (this._stickyReplayTimeout) {
472523
return;
473524
}
474525

@@ -486,9 +537,22 @@ export class StickyStyler {
486537
);
487538
}
488539

540+
private _updateStickRowsReplayQueue(params: UpdateStickRowsParams) {
541+
// No need to replay if a flush is pending.
542+
if (this._stickyReplayTimeout) {
543+
return;
544+
}
545+
546+
this._updatedStickRowsParamsToReplay = this._updatedStickRowsParamsToReplay.filter(
547+
entry => entry.position !== params.position,
548+
);
549+
550+
this._updatedStickRowsParamsToReplay.push(params);
551+
}
552+
489553
/** Update _elemSizeCache with the observed sizes. */
490554
private _updateCachedSizes(entries: ResizeObserverEntry[]) {
491-
let needsColumnUpdate = false;
555+
let needsUpdate = false;
492556
for (const entry of entries) {
493557
const newEntry = entry.borderBoxSize?.length
494558
? {
@@ -500,35 +564,52 @@ export class StickyStyler {
500564
height: entry.contentRect.height,
501565
};
502566

567+
const cachedSize = this._elemSizeCache.get(entry.target as HTMLElement);
503568
if (
504-
newEntry.width !== this._elemSizeCache.get(entry.target as HTMLElement)?.width &&
505-
isCell(entry.target)
569+
(newEntry.width !== cachedSize?.width && isCell(entry.target)) ||
570+
(newEntry.height !== cachedSize?.height && isRow(entry.target))
506571
) {
507-
needsColumnUpdate = true;
572+
needsUpdate = true;
508573
}
509574

510575
this._elemSizeCache.set(entry.target as HTMLElement, newEntry);
511576
}
512577

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

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);
583+
/** Schedule a defered replay of enqueued sticky column operations. */
584+
private _scheduleStickReplay() {
585+
if (
586+
!this._updatedStickyColumnsParamsToReplay.length &&
587+
!this._updatedStickRowsParamsToReplay.length
588+
) {
589+
return;
531590
}
591+
592+
if (this._stickyReplayTimeout) {
593+
clearTimeout(this._stickyReplayTimeout);
594+
}
595+
596+
this._stickyReplayTimeout = setTimeout(() => {
597+
for (const update of this._updatedStickyColumnsParamsToReplay) {
598+
this.updateStickyColumns(
599+
update.rows,
600+
update.stickyStartStates,
601+
update.stickyEndStates,
602+
true,
603+
false,
604+
);
605+
}
606+
for (const update of this._updatedStickRowsParamsToReplay) {
607+
this.stickRows(update.rowsToStick, update.stickyStates, update.position, false);
608+
}
609+
this._updatedStickyColumnsParamsToReplay = [];
610+
this._updatedStickRowsParamsToReplay = [];
611+
this._stickyReplayTimeout = null;
612+
}, 0);
532613
}
533614
}
534615

@@ -537,3 +618,9 @@ function isCell(element: Element) {
537618
element.classList.contains(klass),
538619
);
539620
}
621+
622+
function isRow(element: Element) {
623+
return ['cdk-row', 'cdk-header-row', 'cdk-footer-row'].some(klass =>
624+
element.classList.contains(klass),
625+
);
626+
}

0 commit comments

Comments
 (0)