typescript
59 lines · 8 steps
Throttling pointer events with NgZone in Angular
An Angular directive tracks mouse movement outside change detection and emits coalesced points once per animation frame.
Explained by
highlit
1import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
2
3interface Point {
4 x: number;
5 y: number;
6}
7
8@Directive({
9 selector: '[appPointerTrail]',
10 standalone: true,
11})
12export class PointerTrailDirective implements OnInit, OnDestroy {
13 @Output() trailPoint = new EventEmitter<Point>();
14
15 private removeListener?: () => void;
16 private rafId: number | null = null;
17 private pending: Point | null = null;
18
19 constructor(
20 private readonly host: ElementRef<HTMLElement>,
21 private readonly zone: NgZone,
22 ) {}
23
24 ngOnInit(): void {
25 this.zone.runOutsideAngular(() => {
26 const el = this.host.nativeElement;
27 const onMove = (event: MouseEvent) => {
28 const rect = el.getBoundingClientRect();
29 this.pending = { x: event.clientX - rect.left, y: event.clientY - rect.top };
30 this.scheduleFlush();
31 };
32
33 el.addEventListener('mousemove', onMove, { passive: true });
34 this.removeListener = () => el.removeEventListener('mousemove', onMove);
35 });
36 }
37
38 private scheduleFlush(): void {
39 if (this.rafId !== null) {
40 return;
41 }
42 this.rafId = requestAnimationFrame(() => {
43 this.rafId = null;
44 const point = this.pending;
45 if (!point) {
46 return;
47 }
48 this.pending = null;
49 this.zone.run(() => this.trailPoint.emit(point));
50 });
51 }
52
53 ngOnDestroy(): void {
54 this.removeListener?.();
55 if (this.rafId !== null) {
56 cancelAnimationFrame(this.rafId);
57 }
58 }
59}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Running high-frequency listeners outside NgZone avoids triggering change detection on every event.
- 2Coalescing updates with requestAnimationFrame caps work to one emission per frame no matter how fast events fire.
- 3Directives that attach native listeners must remove them and cancel pending frames on destroy to avoid leaks.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
typescript
import { Injectable, Scope, Inject, NotFoundException } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Request } from 'express'; import { DataSource } from 'typeorm';
Per-tenant database connections in NestJS
multi-tenancy
connection-pooling
dependency-injection
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/throttling-pointer-events-with-ngzone-in-angular-explained-typescript-0cef/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.