Skip to content

Commit 9f73fed

Browse files
authored
perf(cdk-experimental/column-resize): Use ResizeObserver to avoid layout thrashing (#30215)
1 parent 25cbbd1 commit 9f73fed

File tree

2 files changed

+66
-21
lines changed

2 files changed

+66
-21
lines changed

src/cdk-experimental/column-resize/resizable.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Injector,
1515
NgZone,
1616
OnDestroy,
17+
OnInit,
1718
Type,
1819
ViewContainerRef,
1920
ChangeDetectorRef,
@@ -44,7 +45,7 @@ const OVERLAY_ACTIVE_CLASS = 'cdk-resizable-overlay-thumb-active';
4445
*/
4546
@Directive()
4647
export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
47-
implements AfterViewInit, OnDestroy
48+
implements AfterViewInit, OnDestroy, OnInit
4849
{
4950
protected minWidthPxInternal: number = 0;
5051
protected maxWidthPxInternal: number = Number.MAX_SAFE_INTEGER;
@@ -99,6 +100,10 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
99100
}
100101
}
101102

103+
ngOnInit() {
104+
this.resizeStrategy.registerColumn(this.elementRef.nativeElement);
105+
}
106+
102107
ngAfterViewInit() {
103108
this._listenForRowHoverEvents();
104109
this._listenForResizeEvents();
@@ -310,14 +315,13 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
310315
}
311316

312317
private _appendInlineHandle(): void {
313-
this.styleScheduler.schedule(() => {
314-
this.inlineHandle = this.document.createElement('div');
315-
this.inlineHandle.tabIndex = 0;
316-
this.inlineHandle.className = this.getInlineHandleCssClassName();
318+
this.inlineHandle = this.document.createElement('div');
319+
// TODO: re-apply tab index once this element has behavior.
320+
// this.inlineHandle.tabIndex = 0;
321+
this.inlineHandle.className = this.getInlineHandleCssClassName();
317322

318-
// TODO: Apply correct aria role (probably slider) after a11y spec questions resolved.
323+
// TODO: Apply correct aria role (probably slider) after a11y spec questions resolved.
319324

320-
this.elementRef.nativeElement!.appendChild(this.inlineHandle);
321-
});
325+
this.elementRef.nativeElement!.appendChild(this.inlineHandle);
322326
}
323327
}

src/cdk-experimental/column-resize/resize-strategy.ts

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@ import {ColumnResize} from './column-resize';
1818
* The details of how resizing works for tables for flex mat-tables are quite different.
1919
*/
2020
@Injectable()
21-
export abstract class ResizeStrategy {
21+
export abstract class ResizeStrategy implements OnDestroy {
2222
protected abstract readonly columnResize: ColumnResize;
2323
protected abstract readonly styleScheduler: _CoalescedStyleScheduler;
2424
protected abstract readonly table: CdkTable<unknown>;
2525

2626
private _pendingResizeDelta: number | null = null;
27+
private _tableObserved = false;
28+
private _elemSizeCache = new WeakMap<HTMLElement, {width: number; height: number}>();
29+
private _resizeObserver = globalThis?.ResizeObserver
30+
? new globalThis.ResizeObserver(entries => this._updateCachedSizes(entries))
31+
: null;
2732

2833
/** Updates the width of the specified column. */
2934
abstract applyColumnSize(
@@ -51,7 +56,7 @@ export abstract class ResizeStrategy {
5156
protected updateTableWidthAndStickyColumns(delta: number): void {
5257
if (this._pendingResizeDelta === null) {
5358
const tableElement = this.columnResize.elementRef.nativeElement;
54-
const tableWidth = getElementWidth(tableElement);
59+
const tableWidth = this.getElementWidth(tableElement);
5560

5661
this.styleScheduler.schedule(() => {
5762
tableElement.style.width = coerceCssPixelValue(tableWidth + this._pendingResizeDelta!);
@@ -66,6 +71,48 @@ export abstract class ResizeStrategy {
6671

6772
this._pendingResizeDelta = (this._pendingResizeDelta ?? 0) + delta;
6873
}
74+
75+
/** Gets the style.width pixels on the specified element if present, otherwise its offsetWidth. */
76+
protected getElementWidth(element: HTMLElement) {
77+
// Optimization: Check style.width first as we probably set it already before reading
78+
// offsetWidth which triggers layout.
79+
return (
80+
coercePixelsFromCssValue(element.style.width) ||
81+
this._elemSizeCache.get(element)?.width ||
82+
element.offsetWidth
83+
);
84+
}
85+
86+
/** Informs the ResizeStrategy instance of a column that may be resized in the future. */
87+
registerColumn(column: HTMLElement) {
88+
if (!this._tableObserved) {
89+
this._tableObserved = true;
90+
this._resizeObserver?.observe(this.columnResize.elementRef.nativeElement, {
91+
box: 'border-box',
92+
});
93+
}
94+
this._resizeObserver?.observe(column, {box: 'border-box'});
95+
}
96+
97+
ngOnDestroy(): void {
98+
this._resizeObserver?.disconnect();
99+
}
100+
101+
private _updateCachedSizes(entries: ResizeObserverEntry[]) {
102+
for (const entry of entries) {
103+
const newEntry = entry.borderBoxSize?.length
104+
? {
105+
width: entry.borderBoxSize[0].inlineSize,
106+
height: entry.borderBoxSize[0].blockSize,
107+
}
108+
: {
109+
width: entry.contentRect.width,
110+
height: entry.contentRect.height,
111+
};
112+
113+
this._elemSizeCache.set(entry.target as HTMLElement, newEntry);
114+
}
115+
}
69116
}
70117

71118
/**
@@ -87,7 +134,7 @@ export class TableLayoutFixedResizeStrategy extends ResizeStrategy {
87134
sizeInPx: number,
88135
previousSizeInPx?: number,
89136
): void {
90-
const delta = sizeInPx - (previousSizeInPx ?? getElementWidth(columnHeader));
137+
const delta = sizeInPx - (previousSizeInPx ?? this.getElementWidth(columnHeader));
91138

92139
if (delta === 0) {
93140
return;
@@ -101,14 +148,14 @@ export class TableLayoutFixedResizeStrategy extends ResizeStrategy {
101148
}
102149

103150
applyMinColumnSize(_: string, columnHeader: HTMLElement, sizeInPx: number): void {
104-
const currentWidth = getElementWidth(columnHeader);
151+
const currentWidth = this.getElementWidth(columnHeader);
105152
const newWidth = Math.max(currentWidth, sizeInPx);
106153

107154
this.applyColumnSize(_, columnHeader, newWidth, currentWidth);
108155
}
109156

110157
applyMaxColumnSize(_: string, columnHeader: HTMLElement, sizeInPx: number): void {
111-
const currentWidth = getElementWidth(columnHeader);
158+
const currentWidth = this.getElementWidth(columnHeader);
112159
const newWidth = Math.min(currentWidth, sizeInPx);
113160

114161
this.applyColumnSize(_, columnHeader, newWidth, currentWidth);
@@ -189,7 +236,8 @@ export class CdkFlexTableResizeStrategy extends ResizeStrategy implements OnDest
189236
return `cdk-column-${cssFriendlyColumnName}`;
190237
}
191238

192-
ngOnDestroy(): void {
239+
override ngOnDestroy(): void {
240+
super.ngOnDestroy();
193241
this._styleElement?.remove();
194242
this._styleElement = undefined;
195243
}
@@ -277,13 +325,6 @@ function coercePixelsFromCssValue(cssValue: string): number {
277325
return Number(cssValue.match(/(\d+)px/)?.[1]);
278326
}
279327

280-
/** Gets the style.width pixels on the specified element if present, otherwise its offsetWidth. */
281-
function getElementWidth(element: HTMLElement) {
282-
// Optimization: Check style.width first as we probably set it already before reading
283-
// offsetWidth which triggers layout.
284-
return coercePixelsFromCssValue(element.style.width) || element.offsetWidth;
285-
}
286-
287328
/**
288329
* Converts CSS flex values as set in CdkFlexTableResizeStrategy to numbers,
289330
* eg "0 0.01 123px" to 123.

0 commit comments

Comments
 (0)