Skip to content

Commit 4a040cb

Browse files
kseamonAndrew Seguin
authored and
Andrew Seguin
committed
perf(cdk-experimental/column-resize): Use ResizeObserver to avoid layout thrashing (#30215)
(cherry picked from commit 9f73fed)
1 parent 10e752a commit 4a040cb

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
@@ -13,6 +13,7 @@ import {
1313
Injector,
1414
NgZone,
1515
OnDestroy,
16+
OnInit,
1617
Type,
1718
ViewContainerRef,
1819
ChangeDetectorRef,
@@ -42,7 +43,7 @@ const OVERLAY_ACTIVE_CLASS = 'cdk-resizable-overlay-thumb-active';
4243
*/
4344
@Directive()
4445
export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
45-
implements AfterViewInit, OnDestroy
46+
implements AfterViewInit, OnDestroy, OnInit
4647
{
4748
protected minWidthPxInternal: number = 0;
4849
protected maxWidthPxInternal: number = Number.MAX_SAFE_INTEGER;
@@ -95,6 +96,10 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
9596
}
9697
}
9798

99+
ngOnInit() {
100+
this.resizeStrategy.registerColumn(this.elementRef.nativeElement);
101+
}
102+
98103
ngAfterViewInit() {
99104
this._listenForRowHoverEvents();
100105
this._listenForResizeEvents();
@@ -282,14 +287,13 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
282287
}
283288

284289
private _appendInlineHandle(): void {
285-
this.styleScheduler.schedule(() => {
286-
this.inlineHandle = this.document.createElement('div');
287-
this.inlineHandle.tabIndex = 0;
288-
this.inlineHandle.className = this.getInlineHandleCssClassName();
290+
this.inlineHandle = this.document.createElement('div');
291+
// TODO: re-apply tab index once this element has behavior.
292+
// this.inlineHandle.tabIndex = 0;
293+
this.inlineHandle.className = this.getInlineHandleCssClassName();
289294

290-
// TODO: Apply correct aria role (probably slider) after a11y spec questions resolved.
295+
// TODO: Apply correct aria role (probably slider) after a11y spec questions resolved.
291296

292-
this.elementRef.nativeElement!.appendChild(this.inlineHandle);
293-
});
297+
this.elementRef.nativeElement!.appendChild(this.inlineHandle);
294298
}
295299
}

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)