What advantages does using an RxJS Subject have over handling multiple event listeners individually in terms of speed

After investigating a page's slow performance, I identified an angular directive as the root cause. The culprit was a piece of code that registered event listeners on the window keydown event multiple times:

@HostListener('window:keydown', ['$event'])
private keydown(e: KeyboardEvent) {
     this.doSomething(e);
}

To address this issue, I decided to create a service utilizing an RxJS Subject to handle the keyboard event more efficiently:

@Injectable()
export class KeyboardService {
    constructor() {
        window.addEventListener('keydown', event => {
            this.keydownSubject.next(event);
        });
    }
}

private keydownSubject: Subject<KeyboardEvent> = new Subject<KeyboardEvent>();

get keydown(): Observable<KeyboardEvent> {
    return this.keydownSubject.asObservable();
}

By removing the @HostListener from the directive and subscribing to the service's subject in ngOnInit, I was able to significantly improve the page's speed:

export class KeydownEventDirective implements OnInit, OnDestroy {
    constructor(private keyboardService: KeyboardService) {}

    private keydown(e: KeyboardEvent) {
        this.doSomething(e);
    }

    private keydownSubscription: Subscription;
    ngOnInit() {
        this.keydownSubscription =
            this.keyboardService.keydown.subscribe(e => {
                this.keydown(e);
            });
    }

    ngOnDestroy() {
        this.keydownSubscription.unsubscribe();
    }

    ...
}

This solution raised questions about why @HostListener or multiple event listeners were impacting performance more than multiple subscriptions to an RxJS Subject. Could it be that angular HostListeners are not passive listeners by default?

Answer №1

The key to understanding Angular's improved performance lies in its utilization of Zone.js. By harnessing the power of Zone.js for change detection, Angular is able to optimize the handling of DOM events. For a more in-depth look at how Zone.js functions within Angular, I highly recommend checking out the insightful articles on thoughtram.io: Understanding Zones and Zones in Angular.

The initial performance issue stemmed from the inefficiency of event handling with each keystroke. This bottleneck was directly related to Angular's approach to change detection. Thanks to Zone.js' ability to monkey patch DOM event listeners, Angular is able to ensure that every triggering of a listener also prompts a change detection process.

An interesting revelation came when it was discovered that multiple instances of a repeated component, each equipped with their own @HostListener on the window, were independently triggering Angular's change detection. As a result, Angular was attempting to scan the entire application for updates with each keystroke initiated by these components listening for keyboard events. Unsurprisingly, this led to significant performance issues.

The implementation of the KeyboardService effectively addressed this dilemma by streamlining the change detection process. With only one central listener in place, the RxJS Subject seamlessly delivers keyboard events to all component subscriptions within the singular Zone.js execution context.

Answer №2

The topic at hand is not connected to this, initially, the injectable service functions as a singleton so it offers one instance for all your directives. Secondly, within that singular service, you include a sole method in the constructor to manage the keydown event which triggers a subject. If you log a message in the console, you will observe that only one call is made.

Conversely, when using Hostlistener through a directive, an event is registered by tag resulting in multiple executions during a keydown event.

Answer №3

When you find yourself trying to capture the same event across multiple components, all subscription methods (

private keydown(e: KeyboardEvent)
) will run regardless of which key is pressed because you're attaching an event listener at the global window level.

The approach you took in your service is commendable, but there's another way you could also use:

public emitter: EventEmitter<any> = new EventEmitter<any>();

@HostListener('window:keydown', ['$event'])
private keydown(e: KeyboardEvent) {
     this.doSomething(e);
}

private doSomething(event: any): void {
     this.emitter.emit(event);
}

Incorporate this snippet within your service and then create a public EventEmitter or Subject for your components to subscribe to and capture the keyDown event.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What steps should I take to transition from using require statements to imports with typescript in my mocha tests?

I have the following values in my package.json file: "scripts": { "test": "mocha -r ts-node/register security.test.ts" }, "type": "module", . . ...

Tips for augmenting cell with additional data in react-datepicker

Is it possible to include additional text in a cell using react-datepicker? For example, similar to what is shown in this image: view image here ...

Exploring the resolution of unit test for an Angular Bootstrap modal using the John Papa ViewModel style

A custom ModalService has been created to display two different types of dialogs, CancelDialog and ErrorDialog, based on the parameter passed to the service. For example, the following code will show an ErrorDialog: ModalService.openModal('Analysis ...

Is there an issue with this npm version number?

I am trying to include the following dependency in the package.json file of my npm package: "redux-saga": "^1.0.0-beta.0 || ^0.16.0"`. When I install this package in a project that already has "redux-saga": "^1.0.0-beta.1 I am expecting npm/yarn to on ...

Display Vue component depending on specified attribute

I have a block of code that I am working with: <div> <b-card no-body> <b-tabs pills card vertical no-key-nav v-model="step"> <b-tab title="Subject" v-for="animal in animals" :key="animal&q ...

How do prototype, the $.extend method, and the concept of "return this" all connect with each other?

I've been assigned a legacy project, and I find myself puzzled by the purpose of this code. define(['jquery', 'components/BaseComponent', 'bootstrap'], function( $, BaseComponent, bootstrap ) { 'use strict&a ...

Is there a way to adjust the border color of my <select> element without disrupting the existing bootstrap CSS styling?

In my Bootstrap (v4.5.3) form, the select options appear differently in Firefox as shown in the image below: https://i.sstatic.net/3suke.png Upon form submission, I have JavaScript code for validation which checks if an option other than "-- Select an Op ...

Typescript - unexpected behavior when using imported JavaScript types:

I am struggling with headaches trying to integrate an automatically generated JavaScript library into TypeScript... I have packaged the JavaScript library and d.ts file into an npm package, installed the npm package, and the typings modules in the TypeScr ...

The Updating Issue: Angular 2 Table Fails to Reflect Value Changes

I have initialized a table with user details using the ngOnInit() method. When I click on the "create user" button, it opens a form to add a new user to the database. However, the table does not update automatically with the new user's information. H ...

Having trouble with Visual Studio 2015 not compiling TypeScript within an ASP.NET Core project?

Seeking assistance with my Angular app development setup in VS2015. Even though it is recognized as a TypeScript Virtual Project, I am facing issues getting the transpiled files into the wwwroot folder within my ASP.NET Core project. Here's an overvie ...

Explore the potentials of @ngrx/effects

A while back, I inquired about the @ngrx/effects library on Stack Overflow, but the responses didn't completely address my issue. After gaining a better understanding of how actions and effects are interconnected, I'm still unclear on their spec ...

Issue with hydration in Next.js while trying to access the persisted "token" variable in Zustand and triggering a loading spinner

Below is the code snippet from _app.tsx where components/pages are wrapped in a PageWrapper component that handles displaying a loading spinner. export default function App(props: MyAppProps) { const updateJWT = useJWTStore((state) => state.setJWT); ...

My Angular project is experiencing issues with Socket.IO functionality

After successfully using a post method in my endpoint, I encountered an error when integrating it with socket io. The error pertained to a connection error and method not being found. Any help or source code provided would be greatly ap ...

Managing enum types with json2typescript

After receiving a JSON response from the back-end that includes an Enum type, I need to deserialize it. The JSON looks like this: { ..., pst:['SMS','EMAIL'], ... } In Typescript, I have defined my enum class as follows: export enum Pos ...

Is it possible to incorporate numerous instances of SlickGrid by utilizing an angular directive?

Just started diving into AngularJS and it's been an exciting journey so far. I've come across the suggestion of wrapping external libraries into directories, which definitely seems like a good practice. While trying to create a 'slickgrid& ...

Counting radio buttons in AngularJS and handling undefined values when passing parameters

After spending countless hours trying to figure out how to count radio buttons, I encountered another issue where the value passed to ng-change would become "undefined". My question is, how can I successfully pass that value? Any assistance with counting o ...

Do you need assistance with downloading files and disconnecting from clients?

When looking at the code snippet below: async function (req, res, next) { const fd = await fs.open("myfile.txt") fs.createReadStream(null, { fd, autoClose: false }) .on('error', next) .on('end', () => fs.close(fd)) . ...

Retrieve information and transform it into a dynamic variable using Firebase

I am trying to retrieve data from the current user, specifically their company named "ZeroMax", and then store this data in a global variable. The purpose of this variable is to define the path for Firebase. I believe my code gives a clear understanding of ...

Trouble encountered with uploading files using Multer

I am facing an issue with uploading images on a website that is built using React. The problem seems to be related to the backend Node.js code. Code: const multer = require("multer"); // Check if the directory exists, if not, create it const di ...

Unable to abort AWS Amplify's REST (POST) request in a React application

Here is a code snippet that creates a POST request using the aws amplify api. I've stored the API.post promise in a variable called promiseToCancel, and when the cancel button is clicked, the cancelRequest() function is called. The promiseToCancel va ...