Skip to content

Commit 9a16e60

Browse files
authored
perf(ripple): optimize event registration (#18633)
* perf(ripple): optimize event registration Improve scalabiliity of Angular Material components. When used in an app with a large and heavy UI with large numbers of material components (for example in table cells), they can quickly become prohibitively expensive due to their heavy DOM structure and lots of up-front work. Each component with a ripple ends up registering 6 event listeners. This PR makes 4 of them registered on-demand. Note, that this is really a stop-gap solution to help improve performance in our application. A proper implementation should utilize a single document-level service that only registers one set of these event listeners, and uses event delegation to apply ripple effects. * Update core.d.ts * Add the @docs-private annotation.
1 parent 1921df3 commit 9a16e60

File tree

2 files changed

+55
-26
lines changed

2 files changed

+55
-26
lines changed

src/material/core/ripple/ripple-renderer.ts

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,20 @@ const ignoreMouseEventsTimeout = 800;
6161
/** Options that apply to all the event listeners that are bound by the ripple renderer. */
6262
const passiveEventOptions = normalizePassiveListenerOptions({passive: true});
6363

64+
/** Events that signal that the pointer is down. */
65+
const pointerDownEvents = ['mousedown', 'touchstart'];
66+
67+
/** Events that signal that the pointer is up. */
68+
const pointerUpEvents = ['mouseup', 'mouseleave', 'touchend', 'touchcancel'];
69+
6470
/**
6571
* Helper service that performs DOM manipulations. Not intended to be used outside this module.
6672
* The constructor takes a reference to the ripple directive's host element and a map of DOM
6773
* event handlers to be installed on the element that triggers ripple animations.
6874
* This will eventually become a custom renderer once Angular support exists.
6975
* @docs-private
7076
*/
71-
export class RippleRenderer {
77+
export class RippleRenderer implements EventListenerObject {
7278
/** Element where the ripples are being added to. */
7379
private _containerElement: HTMLElement;
7480

@@ -78,9 +84,6 @@ export class RippleRenderer {
7884
/** Whether the pointer is currently down or not. */
7985
private _isPointerDown = false;
8086

81-
/** Events to be registered on the trigger element. */
82-
private _triggerEvents = new Map<string, any>();
83-
8487
/** Set of currently active ripple references. */
8588
private _activeRipples = new Set<RippleRef>();
8689

@@ -90,6 +93,9 @@ export class RippleRenderer {
9093
/** Time in milliseconds when the last touchstart event happened. */
9194
private _lastTouchStartEvent: number;
9295

96+
/** Whether pointer-up event listeners have been registered. */
97+
private _pointerUpEventsRegistered = false;
98+
9399
/**
94100
* Cached dimensions of the ripple container. Set when the first
95101
* ripple is shown and cleared once no more ripples are visible.
@@ -104,16 +110,6 @@ export class RippleRenderer {
104110
// Only do anything if we're on the browser.
105111
if (platform.isBrowser) {
106112
this._containerElement = coerceElement(elementOrElementRef);
107-
108-
// Specify events which need to be registered on the trigger.
109-
this._triggerEvents
110-
.set('mousedown', this._onMousedown)
111-
.set('mouseup', this._onPointerUp)
112-
.set('mouseleave', this._onPointerUp)
113-
114-
.set('touchstart', this._onTouchStart)
115-
.set('touchend', this._onPointerUp)
116-
.set('touchcancel', this._onPointerUp);
117113
}
118114
}
119115

@@ -241,17 +237,34 @@ export class RippleRenderer {
241237
// Remove all previously registered event listeners from the trigger element.
242238
this._removeTriggerEvents();
243239

244-
this._ngZone.runOutsideAngular(() => {
245-
this._triggerEvents.forEach((fn, type) => {
246-
element.addEventListener(type, fn, passiveEventOptions);
247-
});
248-
});
249-
250240
this._triggerElement = element;
241+
this._registerEvents(pointerDownEvents);
242+
}
243+
244+
/**
245+
* Handles all registered events.
246+
* @docs-private
247+
*/
248+
handleEvent(event: Event) {
249+
if (event.type === 'mousedown') {
250+
this._onMousedown(event as MouseEvent);
251+
} else if (event.type === 'touchstart') {
252+
this._onTouchStart(event as TouchEvent);
253+
} else {
254+
this._onPointerUp();
255+
}
256+
257+
// If pointer-up events haven't been registered yet, do so now.
258+
// We do this on-demand in order to reduce the total number of event listeners
259+
// registered by the ripples, which speeds up the rendering time for large UIs.
260+
if (!this._pointerUpEventsRegistered) {
261+
this._registerEvents(pointerUpEvents);
262+
this._pointerUpEventsRegistered = true;
263+
}
251264
}
252265

253266
/** Function being called whenever the trigger is being pressed using mouse. */
254-
private _onMousedown = (event: MouseEvent) => {
267+
private _onMousedown(event: MouseEvent) {
255268
// Screen readers will fire fake mouse events for space/enter. Skip launching a
256269
// ripple in this case for consistency with the non-screen-reader experience.
257270
const isFakeMousedown = isFakeMousedownFromScreenReader(event);
@@ -265,7 +278,7 @@ export class RippleRenderer {
265278
}
266279

267280
/** Function being called whenever the trigger is being pressed using touch. */
268-
private _onTouchStart = (event: TouchEvent) => {
281+
private _onTouchStart(event: TouchEvent) {
269282
if (!this._target.rippleDisabled) {
270283
// Some browsers fire mouse events after a `touchstart` event. Those synthetic mouse
271284
// events will launch a second ripple if we don't ignore mouse events for a specific
@@ -284,7 +297,7 @@ export class RippleRenderer {
284297
}
285298

286299
/** Function being called whenever the trigger is being released. */
287-
private _onPointerUp = () => {
300+
private _onPointerUp() {
288301
if (!this._isPointerDown) {
289302
return;
290303
}
@@ -309,12 +322,27 @@ export class RippleRenderer {
309322
this._ngZone.runOutsideAngular(() => setTimeout(fn, delay));
310323
}
311324

325+
/** Registers event listeners for a given list of events. */
326+
private _registerEvents(eventTypes: string[]) {
327+
this._ngZone.runOutsideAngular(() => {
328+
eventTypes.forEach((type) => {
329+
this._triggerElement!.addEventListener(type, this, passiveEventOptions);
330+
});
331+
});
332+
}
333+
312334
/** Removes previously registered event listeners from the trigger element. */
313335
_removeTriggerEvents() {
314336
if (this._triggerElement) {
315-
this._triggerEvents.forEach((fn, type) => {
316-
this._triggerElement!.removeEventListener(type, fn, passiveEventOptions);
337+
pointerDownEvents.forEach((type) => {
338+
this._triggerElement!.removeEventListener(type, this, passiveEventOptions);
317339
});
340+
341+
if (this._pointerUpEventsRegistered) {
342+
pointerUpEvents.forEach((type) => {
343+
this._triggerElement!.removeEventListener(type, this, passiveEventOptions);
344+
});
345+
}
318346
}
319347
}
320348
}

tools/public_api_guard/material/core.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,12 +434,13 @@ export declare class RippleRef {
434434
fadeOut(): void;
435435
}
436436

437-
export declare class RippleRenderer {
437+
export declare class RippleRenderer implements EventListenerObject {
438438
constructor(_target: RippleTarget, _ngZone: NgZone, elementOrElementRef: HTMLElement | ElementRef<HTMLElement>, platform: Platform);
439439
_removeTriggerEvents(): void;
440440
fadeInRipple(x: number, y: number, config?: RippleConfig): RippleRef;
441441
fadeOutAll(): void;
442442
fadeOutRipple(rippleRef: RippleRef): void;
443+
handleEvent(event: Event): void;
443444
setupTriggerEvents(elementOrElementRef: HTMLElement | ElementRef<HTMLElement>): void;
444445
}
445446

0 commit comments

Comments
 (0)