From 870f2c2d5271c171ee62bd197b773558e0dbc4de Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Wed, 18 Dec 2024 15:48:12 -0500 Subject: [PATCH] perf(cdk-experimental/column-resize): Use ResizeObserver to avoid layout thrashing --- .../column-resize/resizable.ts | 20 +++--- .../column-resize/resize-strategy.ts | 67 +++++++++++++++---- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/src/cdk-experimental/column-resize/resizable.ts b/src/cdk-experimental/column-resize/resizable.ts index 06ce875f0eae..26016cf76f7f 100644 --- a/src/cdk-experimental/column-resize/resizable.ts +++ b/src/cdk-experimental/column-resize/resizable.ts @@ -14,6 +14,7 @@ import { Injector, NgZone, OnDestroy, + OnInit, Type, ViewContainerRef, ChangeDetectorRef, @@ -44,7 +45,7 @@ const OVERLAY_ACTIVE_CLASS = 'cdk-resizable-overlay-thumb-active'; */ @Directive() export abstract class Resizable - implements AfterViewInit, OnDestroy + implements AfterViewInit, OnDestroy, OnInit { protected minWidthPxInternal: number = 0; protected maxWidthPxInternal: number = Number.MAX_SAFE_INTEGER; @@ -99,6 +100,10 @@ export abstract class Resizable } } + ngOnInit() { + this.resizeStrategy.registerColumn(this.elementRef.nativeElement); + } + ngAfterViewInit() { this._listenForRowHoverEvents(); this._listenForResizeEvents(); @@ -310,14 +315,13 @@ export abstract class Resizable } private _appendInlineHandle(): void { - this.styleScheduler.schedule(() => { - this.inlineHandle = this.document.createElement('div'); - this.inlineHandle.tabIndex = 0; - this.inlineHandle.className = this.getInlineHandleCssClassName(); + this.inlineHandle = this.document.createElement('div'); + // TODO: re-apply tab index once this element has behavior. + // this.inlineHandle.tabIndex = 0; + this.inlineHandle.className = this.getInlineHandleCssClassName(); - // TODO: Apply correct aria role (probably slider) after a11y spec questions resolved. + // TODO: Apply correct aria role (probably slider) after a11y spec questions resolved. - this.elementRef.nativeElement!.appendChild(this.inlineHandle); - }); + this.elementRef.nativeElement!.appendChild(this.inlineHandle); } } diff --git a/src/cdk-experimental/column-resize/resize-strategy.ts b/src/cdk-experimental/column-resize/resize-strategy.ts index 51fe6ec0f1ff..a37ea5db0b43 100644 --- a/src/cdk-experimental/column-resize/resize-strategy.ts +++ b/src/cdk-experimental/column-resize/resize-strategy.ts @@ -18,12 +18,17 @@ import {ColumnResize} from './column-resize'; * The details of how resizing works for tables for flex mat-tables are quite different. */ @Injectable() -export abstract class ResizeStrategy { +export abstract class ResizeStrategy implements OnDestroy { protected abstract readonly columnResize: ColumnResize; protected abstract readonly styleScheduler: _CoalescedStyleScheduler; protected abstract readonly table: CdkTable; private _pendingResizeDelta: number | null = null; + private _tableObserved = false; + private _elemSizeCache = new WeakMap(); + private _resizeObserver = globalThis?.ResizeObserver + ? new globalThis.ResizeObserver(entries => this._updateCachedSizes(entries)) + : null; /** Updates the width of the specified column. */ abstract applyColumnSize( @@ -51,7 +56,7 @@ export abstract class ResizeStrategy { protected updateTableWidthAndStickyColumns(delta: number): void { if (this._pendingResizeDelta === null) { const tableElement = this.columnResize.elementRef.nativeElement; - const tableWidth = getElementWidth(tableElement); + const tableWidth = this.getElementWidth(tableElement); this.styleScheduler.schedule(() => { tableElement.style.width = coerceCssPixelValue(tableWidth + this._pendingResizeDelta!); @@ -66,6 +71,48 @@ export abstract class ResizeStrategy { this._pendingResizeDelta = (this._pendingResizeDelta ?? 0) + delta; } + + /** Gets the style.width pixels on the specified element if present, otherwise its offsetWidth. */ + protected getElementWidth(element: HTMLElement) { + // Optimization: Check style.width first as we probably set it already before reading + // offsetWidth which triggers layout. + return ( + coercePixelsFromCssValue(element.style.width) || + this._elemSizeCache.get(element)?.width || + element.offsetWidth + ); + } + + /** Informs the ResizeStrategy instance of a column that may be resized in the future. */ + registerColumn(column: HTMLElement) { + if (!this._tableObserved) { + this._tableObserved = true; + this._resizeObserver?.observe(this.columnResize.elementRef.nativeElement, { + box: 'border-box', + }); + } + this._resizeObserver?.observe(column, {box: 'border-box'}); + } + + ngOnDestroy(): void { + this._resizeObserver?.disconnect(); + } + + private _updateCachedSizes(entries: ResizeObserverEntry[]) { + for (const entry of entries) { + const newEntry = entry.borderBoxSize?.length + ? { + width: entry.borderBoxSize[0].inlineSize, + height: entry.borderBoxSize[0].blockSize, + } + : { + width: entry.contentRect.width, + height: entry.contentRect.height, + }; + + this._elemSizeCache.set(entry.target as HTMLElement, newEntry); + } + } } /** @@ -87,7 +134,7 @@ export class TableLayoutFixedResizeStrategy extends ResizeStrategy { sizeInPx: number, previousSizeInPx?: number, ): void { - const delta = sizeInPx - (previousSizeInPx ?? getElementWidth(columnHeader)); + const delta = sizeInPx - (previousSizeInPx ?? this.getElementWidth(columnHeader)); if (delta === 0) { return; @@ -101,14 +148,14 @@ export class TableLayoutFixedResizeStrategy extends ResizeStrategy { } applyMinColumnSize(_: string, columnHeader: HTMLElement, sizeInPx: number): void { - const currentWidth = getElementWidth(columnHeader); + const currentWidth = this.getElementWidth(columnHeader); const newWidth = Math.max(currentWidth, sizeInPx); this.applyColumnSize(_, columnHeader, newWidth, currentWidth); } applyMaxColumnSize(_: string, columnHeader: HTMLElement, sizeInPx: number): void { - const currentWidth = getElementWidth(columnHeader); + const currentWidth = this.getElementWidth(columnHeader); const newWidth = Math.min(currentWidth, sizeInPx); this.applyColumnSize(_, columnHeader, newWidth, currentWidth); @@ -189,7 +236,8 @@ export class CdkFlexTableResizeStrategy extends ResizeStrategy implements OnDest return `cdk-column-${cssFriendlyColumnName}`; } - ngOnDestroy(): void { + override ngOnDestroy(): void { + super.ngOnDestroy(); this._styleElement?.remove(); this._styleElement = undefined; } @@ -277,13 +325,6 @@ function coercePixelsFromCssValue(cssValue: string): number { return Number(cssValue.match(/(\d+)px/)?.[1]); } -/** Gets the style.width pixels on the specified element if present, otherwise its offsetWidth. */ -function getElementWidth(element: HTMLElement) { - // Optimization: Check style.width first as we probably set it already before reading - // offsetWidth which triggers layout. - return coercePixelsFromCssValue(element.style.width) || element.offsetWidth; -} - /** * Converts CSS flex values as set in CdkFlexTableResizeStrategy to numbers, * eg "0 0.01 123px" to 123.