Martin
Martin

Reputation: 66

Angular Window Resize performance

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.

Upvotes: 1

Views: 879

Answers (3)

Eliseo
Eliseo

Reputation: 58039

I feel to improve the performance it's better use an unique "fromEvent.Resize" in the main.app that take account all the elements with a directive

You have a simple directive like

@Directive({ selector: '[checkWidth]' })
export abstract class CheckWidthDirective{
    winWidth: number = window.innerWidth;
}

If in your main.app you have a

@ContentChildren(CheckWidthDirective,{descendant:true}) 
                                    resizes!:QueryList<CheckWidthDirective>

ngAfterViewInit(){
   fromEvent('window.resize').pipe(
        debounceTime(300),
        map((event: any) => event.target.innerWidth),
        takeUntil(this.destroy$),
      ).subscribe(width => {
          this.resizes.winWidth=width
      });
}

Well, your directive can use a setter if you want, or signals or...

Upvotes: 0

Naren Murali
Naren Murali

Reputation: 58244

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, which is a cool decorator to debounce methods directly. Please find below the working example.

ngDebounce article

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;
}

Upvotes: 0

Hezy Ziv
Hezy Ziv

Reputation: 5558

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();
    }
  }
}

Upvotes: 0

Related Questions