Skip to content

Commit 923c105

Browse files
committed
perf(column-resize): Coalesce dom updates in column resize to reduce layout thrashing.
1 parent 24d1124 commit 923c105

File tree

8 files changed

+86
-40
lines changed

8 files changed

+86
-40
lines changed

src/cdk-experimental/column-resize/overlay-handle.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {AfterViewInit, Directive, ElementRef, OnDestroy, NgZone} from '@angular/
1010
import {coerceCssPixelValue} from '@angular/cdk/coercion';
1111
import {Directionality} from '@angular/cdk/bidi';
1212
import {ESCAPE} from '@angular/cdk/keycodes';
13-
import {CdkColumnDef} from '@angular/cdk/table';
13+
import {CdkColumnDef, _CoalescedStyleScheduler} from '@angular/cdk/table';
1414
import {fromEvent, Subject, merge} from 'rxjs';
1515
import {
1616
distinctUntilChanged,
@@ -47,6 +47,7 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy {
4747
protected abstract readonly ngZone: NgZone;
4848
protected abstract readonly resizeNotifier: ColumnResizeNotifierSource;
4949
protected abstract readonly resizeRef: ResizeRef;
50+
protected abstract readonly styleScheduler: _CoalescedStyleScheduler;
5051

5152
ngAfterViewInit() {
5253
this._listenForMouseEvents();
@@ -91,7 +92,7 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy {
9192
const startX = mousedownEvent.screenX;
9293

9394
const initialSize = this._getOriginWidth();
94-
let overlayOffset = this._getOverlayOffset();
95+
let overlayOffset = 0;
9596
let originOffset = this._getOriginOffset();
9697
let size = initialSize;
9798
let overshot = 0;
@@ -140,17 +141,19 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy {
140141
this.resizeNotifier.triggerResize.next(
141142
{columnId: this.columnDef.name, size: computedNewSize, previousSize: size});
142143

143-
const originNewSize = this._getOriginWidth();
144-
const originNewOffset = this._getOriginOffset();
145-
const originOffsetDeltaX = originNewOffset - originOffset;
146-
const originSizeDeltaX = originNewSize - size;
147-
size = originNewSize;
148-
originOffset = originNewOffset;
144+
this.styleScheduler.scheduleEnd(() => {
145+
const originNewSize = this._getOriginWidth();
146+
const originNewOffset = this._getOriginOffset();
147+
const originOffsetDeltaX = originNewOffset - originOffset;
148+
const originSizeDeltaX = originNewSize - size;
149+
size = originNewSize;
150+
originOffset = originNewOffset;
149151

150-
overshot += deltaX + (this._isLtr() ? -originSizeDeltaX : originSizeDeltaX);
151-
overlayOffset += originOffsetDeltaX + (this._isLtr() ? originSizeDeltaX : 0);
152+
overshot += deltaX + (this._isLtr() ? -originSizeDeltaX : originSizeDeltaX);
153+
overlayOffset += originOffsetDeltaX + (this._isLtr() ? originSizeDeltaX : 0);
152154

153-
this._updateOverlayOffset(overlayOffset);
155+
this._updateOverlayOffset(overlayOffset);
156+
});
154157
});
155158
}
156159

@@ -167,12 +170,9 @@ export abstract class ResizeOverlayHandle implements AfterViewInit, OnDestroy {
167170
return this.resizeRef.origin.nativeElement!.offsetLeft;
168171
}
169172

170-
private _getOverlayOffset(): number {
171-
return parseInt(this.resizeRef.overlayRef.overlayElement.style.left!, 10);
172-
}
173-
174173
private _updateOverlayOffset(offset: number): void {
175-
this.resizeRef.overlayRef.overlayElement.style.left = coerceCssPixelValue(offset);
174+
this.resizeRef.overlayRef.overlayElement.style.transform =
175+
`translateX(${coerceCssPixelValue(offset)})`;
176176
}
177177

178178
private _isLtr(): boolean {

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import {Directionality} from '@angular/cdk/bidi';
2121
import {ComponentPortal, PortalInjector} from '@angular/cdk/portal';
2222
import {Overlay, OverlayRef} from '@angular/cdk/overlay';
23-
import {CdkColumnDef} from '@angular/cdk/table';
23+
import {CdkColumnDef, _CoalescedStyleScheduler} from '@angular/cdk/table';
2424
import {merge, Subject} from 'rxjs';
2525
import {filter, takeUntil} from 'rxjs/operators';
2626

@@ -61,18 +61,21 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
6161
protected abstract readonly overlay: Overlay;
6262
protected abstract readonly resizeNotifier: ColumnResizeNotifierSource;
6363
protected abstract readonly resizeStrategy: ResizeStrategy;
64+
protected abstract readonly styleScheduler: _CoalescedStyleScheduler;
6465
protected abstract readonly viewContainerRef: ViewContainerRef;
6566
protected abstract readonly changeDetectorRef: ChangeDetectorRef;
6667

68+
private _viewInitialized = false;
69+
6770
/** The minimum width to allow the column to be sized to. */
6871
get minWidthPx(): number {
6972
return this.minWidthPxInternal;
7073
}
7174
set minWidthPx(value: number) {
7275
this.minWidthPxInternal = value;
7376

74-
if (this.elementRef.nativeElement) {
75-
this.columnResize.setResized();
77+
this.columnResize.setResized();
78+
if (this.elementRef.nativeElement && this._viewInitialized) {
7679
this._applyMinWidthPx();
7780
}
7881
}
@@ -84,13 +87,15 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
8487
set maxWidthPx(value: number) {
8588
this.maxWidthPxInternal = value;
8689

87-
if (this.elementRef.nativeElement) {
88-
this.columnResize.setResized();
90+
this.columnResize.setResized();
91+
if (this.elementRef.nativeElement && this._viewInitialized) {
8992
this._applyMaxWidthPx();
9093
}
9194
}
9295

9396
ngAfterViewInit() {
97+
this._viewInitialized = true;
98+
9499
this._listenForRowHoverEvents();
95100
this._listenForResizeEvents();
96101
this._appendInlineHandle();
@@ -251,12 +256,14 @@ export abstract class Resizable<HandleComponent extends ResizeOverlayHandle>
251256
}
252257

253258
private _appendInlineHandle(): void {
254-
this.inlineHandle = this.document.createElement('div');
255-
this.inlineHandle.tabIndex = 0;
256-
this.inlineHandle.className = this.getInlineHandleCssClassName();
259+
this.styleScheduler.schedule(() => {
260+
this.inlineHandle = this.document.createElement('div');
261+
this.inlineHandle.tabIndex = 0;
262+
this.inlineHandle.className = this.getInlineHandleCssClassName();
257263

258-
// TODO: Apply correct aria role (probably slider) after a11y spec questions resolved.
264+
// TODO: Apply correct aria role (probably slider) after a11y spec questions resolved.
259265

260-
this.elementRef.nativeElement!.appendChild(this.inlineHandle);
266+
this.elementRef.nativeElement!.appendChild(this.inlineHandle);
267+
});
261268
}
262269
}

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

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {Inject, Injectable, OnDestroy, Provider} from '@angular/core';
1010
import {DOCUMENT} from '@angular/common';
1111
import {coerceCssPixelValue} from '@angular/cdk/coercion';
12+
import {_CoalescedStyleScheduler} from '@angular/cdk/table';
1213

1314
import {ColumnResize} from './column-resize';
1415

@@ -19,6 +20,7 @@ import {ColumnResize} from './column-resize';
1920
@Injectable()
2021
export abstract class ResizeStrategy {
2122
protected abstract readonly columnResize: ColumnResize;
23+
protected abstract readonly styleScheduler: _CoalescedStyleScheduler;
2224

2325
/** Updates the width of the specified column. */
2426
abstract applyColumnSize(
@@ -42,9 +44,16 @@ export abstract class ResizeStrategy {
4244
/** Adjusts the width of the table element by the specified delta. */
4345
protected updateTableWidth(delta: number): void {
4446
const table = this.columnResize.elementRef.nativeElement;
45-
const tableWidth = getElementWidth(table);
4647

47-
table.style.width = coerceCssPixelValue(tableWidth + delta);
48+
if (!pendingTableResizes.has(table)) {
49+
const tableWidth = getElementWidth(table);
50+
this.styleScheduler.schedule(() => {
51+
table.style.width = coerceCssPixelValue(tableWidth + pendingTableResizes.get(table)!);
52+
pendingTableResizes.delete(table);
53+
});
54+
}
55+
56+
pendingTableResizes.set(table, (pendingTableResizes.get(table) ?? 0) + delta);
4857
}
4958
}
5059

@@ -57,15 +66,23 @@ export abstract class ResizeStrategy {
5766
*/
5867
@Injectable()
5968
export class TableLayoutFixedResizeStrategy extends ResizeStrategy {
60-
constructor(protected readonly columnResize: ColumnResize) {
69+
constructor(
70+
protected readonly columnResize: ColumnResize,
71+
protected readonly styleScheduler: _CoalescedStyleScheduler) {
6172
super();
6273
}
6374

6475
applyColumnSize(_: string, columnHeader: HTMLElement, sizeInPx: number,
6576
previousSizeInPx?: number): void {
6677
const delta = sizeInPx - (previousSizeInPx ?? getElementWidth(columnHeader));
6778

68-
columnHeader.style.width = coerceCssPixelValue(sizeInPx);
79+
if (delta === 0) {
80+
return;
81+
}
82+
83+
this.styleScheduler.schedule(() => {
84+
columnHeader.style.width = coerceCssPixelValue(sizeInPx);
85+
});
6986

7087
this.updateTableWidth(delta);
7188
}
@@ -105,6 +122,7 @@ export class CdkFlexTableResizeStrategy extends ResizeStrategy implements OnDest
105122

106123
constructor(
107124
protected readonly columnResize: ColumnResize,
125+
protected readonly styleScheduler: _CoalescedStyleScheduler,
108126
@Inject(DOCUMENT) document: any) {
109127
super();
110128
this._document = document;
@@ -165,12 +183,14 @@ export class CdkFlexTableResizeStrategy extends ResizeStrategy implements OnDest
165183
enable = true): void {
166184
const properties = this._getColumnPropertiesMap(cssFriendlyColumnName);
167185

168-
if (enable) {
169-
properties.set(key, value);
170-
} else {
171-
properties.delete(key);
172-
}
173-
this._applySizeCss(cssFriendlyColumnName);
186+
this.styleScheduler.schedule(() => {
187+
if (enable) {
188+
properties.set(key, value);
189+
} else {
190+
properties.delete(key);
191+
}
192+
this._applySizeCss(cssFriendlyColumnName);
193+
});
174194
}
175195

176196
private _getStyleSheet(): CSSStyleSheet {
@@ -239,6 +259,8 @@ function coercePixelsFromFlexValue(flexValue: string|undefined): number {
239259
return Number(flexValue?.match(/0 0\.01 (\d+)px/)?.[1]);
240260
}
241261

262+
const pendingTableResizes = new WeakMap<Element, number>();
263+
242264
export const TABLE_LAYOUT_FIXED_RESIZE_STRATEGY_PROVIDER: Provider = {
243265
provide: ResizeStrategy,
244266
useClass: TableLayoutFixedResizeStrategy,

src/cdk/table/coalesced-style-scheduler.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ export class _CoalescedStyleScheduler {
3131
this._currentSchedule!.subscribe(task);
3232
}
3333

34+
/**
35+
* Schedules the specified task to run after all scheduled tasks.
36+
* This is useful when measuring after style updates is required.
37+
*/
38+
scheduleEnd(task: () => unknown) : void {
39+
this._createScheduleIfNeeded();
40+
41+
this._currentSchedule!.subscribe(() => {
42+
this.schedule(task);
43+
});
44+
}
45+
3446
private _createScheduleIfNeeded() {
3547
if (this._currentSchedule) { return; }
3648

src/material-experimental/column-resize/overlay-handle.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
ViewEncapsulation,
1616
} from '@angular/core';
1717
import {DOCUMENT} from '@angular/common';
18-
import {CdkColumnDef} from '@angular/cdk/table';
18+
import {CdkColumnDef, _CoalescedStyleScheduler} from '@angular/cdk/table';
1919
import {Directionality} from '@angular/cdk/bidi';
2020
import {
2121
ColumnResize,
@@ -49,6 +49,7 @@ export class MatColumnResizeOverlayHandle extends ResizeOverlayHandle {
4949
protected readonly ngZone: NgZone,
5050
protected readonly resizeNotifier: ColumnResizeNotifierSource,
5151
protected readonly resizeRef: ResizeRef,
52+
protected readonly styleScheduler: _CoalescedStyleScheduler,
5253
@Inject(DOCUMENT) document: any) {
5354
super();
5455
this.document = document;

src/material-experimental/column-resize/resizable-directives/default-enabled-resizable.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
import {DOCUMENT} from '@angular/common';
1919
import {Directionality} from '@angular/cdk/bidi';
2020
import {Overlay} from '@angular/cdk/overlay';
21-
import {CdkColumnDef} from '@angular/cdk/table';
21+
import {CdkColumnDef, _CoalescedStyleScheduler} from '@angular/cdk/table';
2222
import {
2323
ColumnResize,
2424
ColumnResizeNotifierSource,
@@ -52,6 +52,7 @@ export class MatDefaultResizable extends AbstractMatResizable {
5252
protected readonly overlay: Overlay,
5353
protected readonly resizeNotifier: ColumnResizeNotifierSource,
5454
protected readonly resizeStrategy: ResizeStrategy,
55+
protected readonly styleScheduler: _CoalescedStyleScheduler,
5556
protected readonly viewContainerRef: ViewContainerRef,
5657
protected readonly changeDetectorRef: ChangeDetectorRef) {
5758
super();

src/material-experimental/column-resize/resizable-directives/resizable.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
import {DOCUMENT} from '@angular/common';
1919
import {Directionality} from '@angular/cdk/bidi';
2020
import {Overlay} from '@angular/cdk/overlay';
21-
import {CdkColumnDef} from '@angular/cdk/table';
21+
import {CdkColumnDef, _CoalescedStyleScheduler} from '@angular/cdk/table';
2222
import {
2323
ColumnResize,
2424
ColumnResizeNotifierSource,
@@ -51,6 +51,7 @@ export class MatResizable extends AbstractMatResizable {
5151
protected readonly overlay: Overlay,
5252
protected readonly resizeNotifier: ColumnResizeNotifierSource,
5353
protected readonly resizeStrategy: ResizeStrategy,
54+
protected readonly styleScheduler: _CoalescedStyleScheduler,
5455
protected readonly viewContainerRef: ViewContainerRef,
5556
protected readonly changeDetectorRef: ChangeDetectorRef) {
5657
super();

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {Inject, Injectable, Provider} from '@angular/core';
1010
import {DOCUMENT} from '@angular/common';
11+
import {_CoalescedStyleScheduler} from '@angular/cdk/table';
1112

1213
import {
1314
ColumnResize,
@@ -25,8 +26,9 @@ export {TABLE_LAYOUT_FIXED_RESIZE_STRATEGY_PROVIDER};
2526
export class MatFlexTableResizeStrategy extends CdkFlexTableResizeStrategy {
2627
constructor(
2728
columnResize: ColumnResize,
29+
styleScheduler: _CoalescedStyleScheduler,
2830
@Inject(DOCUMENT) document: any) {
29-
super(columnResize, document);
31+
super(columnResize, styleScheduler, document);
3032
}
3133

3234
protected getColumnCssClass(cssFriendlyColumnName: string): string {

0 commit comments

Comments
 (0)