Skip to content

Commit 33ca850

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 33ca850

File tree

3 files changed

+143
-60
lines changed

3 files changed

+143
-60
lines changed

src/cdk/table/sticky-styler.ts

Lines changed: 106 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,8 @@ 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;
4249
private _cachedCellWidths: number[] = [];
4350
private readonly _borderCellCss: Readonly<{[d in StickyDirection]: string}>;
4451

@@ -206,14 +213,27 @@ export class StickyStyler {
206213
* should be stuck in the particular top or bottom position.
207214
* @param position The position direction in which the row should be stuck if that row should be
208215
* sticky.
209-
*
216+
* @param replay Whether to enqueue this call for replay after a ResizeObserver update.
210217
*/
211-
stickRows(rowsToStick: HTMLElement[], stickyStates: boolean[], position: 'top' | 'bottom') {
218+
stickRows(
219+
rowsToStick: HTMLElement[],
220+
stickyStates: boolean[],
221+
position: 'top' | 'bottom',
222+
replay = true,
223+
) {
212224
// Since we can't measure the rows on the server, we can't stick the rows properly.
213225
if (!this._isBrowser) {
214226
return;
215227
}
216228

229+
if (replay) {
230+
this._updateStickRowsReplayQueue({
231+
rowsToStick: [...rowsToStick],
232+
stickyStates: [...stickyStates],
233+
position,
234+
});
235+
}
236+
217237
// Coalesce with other sticky row updates (top/bottom), sticky columns updates
218238
// (and potentially other changes like column resize).
219239
this._coalescedStyleScheduler.schedule(() => {
@@ -440,24 +460,40 @@ export class StickyStyler {
440460

441461
/**
442462
* Retreives the most recently observed size of the specified element from the cache, or
443-
* meaures it directly if not yet cached.
463+
* schedules it to be measured directly if not yet cached.
444464
*/
445465
private _retrieveElementSize(element: HTMLElement): {width: number; height: number} {
446466
const cachedSize = this._elemSizeCache.get(element);
447-
if (cachedSize) {
467+
if (cachedSize != null) {
448468
return cachedSize;
449469
}
450-
451-
const clientRect = element.getBoundingClientRect();
452-
const size = {width: clientRect.width, height: clientRect.height};
453-
454470
if (!this._resizeObserver) {
455-
return size;
471+
return this._retrieveElementSizeImmediate(element);
456472
}
457473

458-
this._elemSizeCache.set(element, size);
459474
this._resizeObserver.observe(element, {box: 'border-box'});
460-
return size;
475+
setTimeout(() => {
476+
if (this._elemSizeCache.get(element) != null) {
477+
return;
478+
}
479+
480+
const size = this._retrieveElementSizeImmediate(element);
481+
this._elemSizeCache.set(element, size);
482+
483+
if (!this._stickyReplayTimeout) {
484+
this._scheduleStickReplay();
485+
}
486+
}, 10);
487+
488+
return {width: 0, height: 0};
489+
}
490+
491+
/**
492+
* Returns the size of the specified element by direct measurement.
493+
*/
494+
private _retrieveElementSizeImmediate(element: HTMLElement): {width: number; height: number} {
495+
const clientRect = element.getBoundingClientRect();
496+
return {width: clientRect.width, height: clientRect.height};
461497
}
462498

463499
/**
@@ -468,7 +504,7 @@ export class StickyStyler {
468504
this._removeFromStickyColumnReplayQueue(params.rows);
469505

470506
// No need to replay if a flush is pending.
471-
if (this._stickyColumnsReplayTimeout) {
507+
if (this._stickyReplayTimeout) {
472508
return;
473509
}
474510

@@ -486,9 +522,22 @@ export class StickyStyler {
486522
);
487523
}
488524

525+
private _updateStickRowsReplayQueue(params: UpdateStickRowsParams) {
526+
// No need to replay if a flush is pending.
527+
if (this._stickyReplayTimeout) {
528+
return;
529+
}
530+
531+
this._updatedStickRowsParamsToReplay = this._updatedStickRowsParamsToReplay.filter(
532+
entry => entry.position !== params.position,
533+
);
534+
535+
this._updatedStickRowsParamsToReplay.push(params);
536+
}
537+
489538
/** Update _elemSizeCache with the observed sizes. */
490539
private _updateCachedSizes(entries: ResizeObserverEntry[]) {
491-
let needsColumnUpdate = false;
540+
let needsUpdate = false;
492541
for (const entry of entries) {
493542
const newEntry = entry.borderBoxSize?.length
494543
? {
@@ -500,35 +549,52 @@ export class StickyStyler {
500549
height: entry.contentRect.height,
501550
};
502551

552+
const cachedSize = this._elemSizeCache.get(entry.target as HTMLElement);
503553
if (
504-
newEntry.width !== this._elemSizeCache.get(entry.target as HTMLElement)?.width &&
505-
isCell(entry.target)
554+
(newEntry.width !== cachedSize?.width && isCell(entry.target)) ||
555+
(newEntry.height !== cachedSize?.height && isRow(entry.target))
506556
) {
507-
needsColumnUpdate = true;
557+
needsUpdate = true;
508558
}
509559

510560
this._elemSizeCache.set(entry.target as HTMLElement, newEntry);
511561
}
512562

513-
if (needsColumnUpdate && this._updatedStickyColumnsParamsToReplay.length) {
514-
if (this._stickyColumnsReplayTimeout) {
515-
clearTimeout(this._stickyColumnsReplayTimeout);
516-
}
563+
if (needsUpdate) {
564+
this._scheduleStickReplay();
565+
}
566+
}
517567

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);
568+
/** Schedule a defered replay of enqueued sticky column operations. */
569+
private _scheduleStickReplay() {
570+
if (
571+
!this._updatedStickyColumnsParamsToReplay.length &&
572+
!this._updatedStickRowsParamsToReplay.length
573+
) {
574+
return;
575+
}
576+
577+
if (this._stickyReplayTimeout) {
578+
clearTimeout(this._stickyReplayTimeout);
531579
}
580+
581+
this._stickyReplayTimeout = setTimeout(() => {
582+
for (const update of this._updatedStickyColumnsParamsToReplay) {
583+
this.updateStickyColumns(
584+
update.rows,
585+
update.stickyStartStates,
586+
update.stickyEndStates,
587+
true,
588+
false,
589+
);
590+
}
591+
for (const update of this._updatedStickRowsParamsToReplay) {
592+
this.stickRows(update.rowsToStick, update.stickyStates, update.position, false);
593+
}
594+
this._updatedStickyColumnsParamsToReplay = [];
595+
this._updatedStickRowsParamsToReplay = [];
596+
this._stickyReplayTimeout = null;
597+
}, 0);
532598
}
533599
}
534600

@@ -537,3 +603,9 @@ function isCell(element: Element) {
537603
element.classList.contains(klass),
538604
);
539605
}
606+
607+
function isRow(element: Element) {
608+
return ['cdk-row', 'cdk-header-row', 'cdk-footer-row'].some(klass =>
609+
element.classList.contains(klass),
610+
);
611+
}

0 commit comments

Comments
 (0)