Skip to content

Commit

Permalink
perf(cdk-experimental/column-resize): Use ResizeObserver to avoid lay…
Browse files Browse the repository at this point in the history
…out thrashing (#30215)
  • Loading branch information
kseamon authored Dec 19, 2024
1 parent 25cbbd1 commit 9f73fed
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 21 deletions.
20 changes: 12 additions & 8 deletions src/cdk-experimental/column-resize/resizable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Injector,
NgZone,
OnDestroy,
OnInit,
Type,
ViewContainerRef,
ChangeDetectorRef,
Expand Down Expand Up @@ -44,7 +45,7 @@ const OVERLAY_ACTIVE_CLASS = 'cdk-resizable-overlay-thumb-active';
*/
@Directive()
export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
implements AfterViewInit, OnDestroy
implements AfterViewInit, OnDestroy, OnInit
{
protected minWidthPxInternal: number = 0;
protected maxWidthPxInternal: number = Number.MAX_SAFE_INTEGER;
Expand Down Expand Up @@ -99,6 +100,10 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
}
}

ngOnInit() {
this.resizeStrategy.registerColumn(this.elementRef.nativeElement);
}

ngAfterViewInit() {
this._listenForRowHoverEvents();
this._listenForResizeEvents();
Expand Down Expand Up @@ -310,14 +315,13 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
}

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);
}
}
67 changes: 54 additions & 13 deletions src/cdk-experimental/column-resize/resize-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>;

private _pendingResizeDelta: number | null = null;
private _tableObserved = false;
private _elemSizeCache = new WeakMap<HTMLElement, {width: number; height: number}>();
private _resizeObserver = globalThis?.ResizeObserver
? new globalThis.ResizeObserver(entries => this._updateCachedSizes(entries))
: null;

/** Updates the width of the specified column. */
abstract applyColumnSize(
Expand Down Expand Up @@ -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!);
Expand All @@ -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);
}
}
}

/**
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 9f73fed

Please sign in to comment.