Angular Window Resize performance

47 Views Asked by At

I'm creating an Angular responsive app where I have more than 10 breakpoints in which I have to group the elements in different containers. Having this in mind I believe that I can't get advantage of the css mediaQueris and I want to have the innerWidth property of the window within the components.

In order to get it I have created the following directive and I extend the components with it:

import { Directive, NgZone, OnDestroy } from "@angular/core";
import { Subject, fromEvent } from "rxjs";
import { debounceTime, distinctUntilChanged, map, takeUntil } from "rxjs/operators";

@Directive()
export abstract class WindowResizeDirective implements OnDestroy {
    winWidth: number = window.innerWidth;

    protected destroy$: Subject<boolean> = new Subject();

    constructor(private _zone: NgZone) {
        this._zone.runOutsideAngular(() => {
            fromEvent(window, 'resize').pipe(
                debounceTime(300),
                map((ev: any) => ev.target.innerWidth),
                distinctUntilChanged(),
                takeUntil(this.destroy$)
            ).subscribe(width => {
                this._zone.run(() => {
                    this.winWidth = width;
                })
            });
        });
    }

    ngOnDestroy(): void {
        this.destroy$.next(true);
        this.destroy$.complete();
      }
}

However many times I need to use this directive of the page component once and than on many components which are children of the page component, thus a lot of change detection cycles are triggered for one and the same reason -> resize of the window. Can you suggest a way of improving the performance?

  1. I've been thinking of using a service instead of directive which is provided on parent level and than each child can get the same instance of the service.
  2. I'm not sure if the code within the directive is optimal at all.

Any suggestions would be appreciated.

2

There are 2 best solutions below

1
Hezy Ziv On

I guess a service will be better here, create a Resize Service to handle the window resize event across multiple components.

Use BehaviorSubject to hold the current window width

something like this :

import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { BehaviorSubject, fromEvent } from 'rxjs';
import { debounceTime, map, takeUntil } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class ResizeService implements OnDestroy {
  private windowSize = new BehaviorSubject<number>(window.innerWidth);
  private destroy$ = new Subject<void>();

  constructor(private zone: NgZone) {
    this.zone.runOutsideAngular(() => {
      fromEvent(window, 'resize').pipe(
        debounceTime(300),
        map((event: any) => event.target.innerWidth),
        takeUntil(this.destroy$),
      ).subscribe(width => {
        this.zone.run(() => {
          this.windowSize.next(width);
        });
      });
    });
  }

  getWindowSize(): BehaviorSubject<number> {
    return this.windowSize;
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

inject it to your component

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { ResizeService } from './resize.service';

@Component({
  selector: 'app-some-component',
  template: `<!-- Your template here -->`,
})
export class SomeComponent implements OnInit, OnDestroy {
  private resizeSubscription: Subscription;
  winWidth: number;

  constructor(private resizeService: ResizeService) {}

  ngOnInit() {
    this.resizeSubscription = this.resizeService.getWindowSize().subscribe(width => {
      this.winWidth = width;
    });
  }

  ngOnDestroy() {
    if (this.resizeSubscription) {
      this.resizeSubscription.unsubscribe();
    }
  }
}
1
Naren Murali On

Since below code does not trigger change detection, we can just add an hostlistener to listen for resize event, I am also adding an additional decorator ngDebounce its a cool decorator to debounce methods directly, please find below the working example for reference!

ngDebounce article

code

ts

import { Component, HostListener } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
import { ngDebounce } from './ngDebounce';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h1>Hello from {{ name }}!</h1>
    <a target="_blank" href="https://angular.dev/overview">
      Learn more about Angular
    </a><br/><br/><br/><br/>
    winWidth: {{winWidth}}
  `,
})
export class App {
  winWidth: number = window.innerWidth;
  name = 'Angular';

  @HostListener('window:resize', ['$event'])
  @ngDebounce(500)
  onResize(event: any) {
    console.log('resize');
    this.winWidth = event.target.innerWidth;
  }
}

bootstrapApplication(App);

ngdebounce.ts

export function ngDebounce(timeout: number) {
  // store timeout value for cancel the timeout
  let timeoutRef: any = null;

  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    // store original function for future use
    const original = descriptor.value;

    // override original function body
    descriptor.value = function debounce(...args: any[]) {
      // clear previous timeout
      clearTimeout(timeoutRef);

      // sechudle timer
      timeoutRef = setTimeout(() => {
        // call original function
        original.apply(this, args);
      }, timeout);
    };

    // return descriptor with new value
    return descriptor;
  };
}

Stackblitz Demo


Just use a getter method to fetch the latest value of window.innerWidth whenever necessary

get windowInnerWidth() {
    return window?.innerWidth || 0;
}