Search code examples
angularperformanceinternet-explorer-11

Angular 4 performance / CPU usage on IE11


I'm working on a angular 4 application. It runs fine on chrome & firefox but the performance on IE11 the load times are unacceptable. Router changes on Chrome/FF are about 0.5s (for more elaborate pages) and about 5s on IE11. Opening a dropdown (by adding/removing a CSS class to set display:none) is instant on Chrome/FF and takes about 3 seconds on IE11.
The IE11 profiler shows the following when opening a dropdown:
1 2
And the timeline is completly full of classList.add() and classList.remove() calls:
1 2
It's the same couple of elements on which classes are added/removed over and over again. Buttons

The dropdown simply uses the [ngClass] directive:

  <div class="dropdown-menu"
       [ngClass]="{
       'show': open,
       'mod-dropup': dropup,
       'mod-right': rightAlign
       }">
    <ng-content></ng-content>
  </div>

I added all of the required polyfills in the polyfills.ts. What could be the cause of this?

Versions:
Angular 4.4.3
Internet Explorer 11.1884.14393.0


Solution

  • It turns out the problem was too many document event listeners.

    I had a onClickOutside directive that registers a document click listener and every entry in the table had a dropdown that uses this directve. So when I display 100 entries every click anywhere causes 100 event listeners to fire.

    I optimized this by only having a single document click listener in a click service and the directive only (un-)registers the element with the service.

    Here's the service:

    /**
     * A event emitter that should be notified about some kind of click related to the element
     */
    export interface ClickListener {
      element : ElementRef;
      emitter : EventEmitter<Event>;
    }
    
    /**
     * A central service for managing click handlers
     * This is mainly an optimization to reduce the amount of global click event handler
     */
    @Injectable()
    export class ClickService {
    
      private clickOutsideListeners : ClickListener[] = [];
    
      constructor() {
        this.registerClickHandlers();
      }
    
      private registerClickHandlers() : void {
        document.addEventListener('click', (event : Event) => {
          this.handleClickOutside(event);
        });
      }
    
      /**
       * Emits for every registered callback if the click is outside of the element
       */
      private handleClickOutside(event : Event) : void {
        this.clickOutsideListeners
          .filter(entry => !entry.element.nativeElement.contains(event.target))
          .forEach(entry => {
            entry.emitter.emit(event);
          });
      }
    
      /**
       * Registers a listener to be notified whenever there is a click outside of the element
       */
      public addOutsideClickListener(element : ClickListener) : void {
        if (this.clickOutsideListeners.indexOf(element) !== -1) return;
        this.clickOutsideListeners.push(element);
      }
    
      /**
       * Unregisters the listener
       */
      public removeOutsideClickListener(element : ClickListener) : void {
        const index = this.clickOutsideListeners.findIndex(entry => entry === element);
        if (index === -1) return;
        this.clickOutsideListeners.splice(index, 1);
      }
    }
    

    The Directive:

    /**
     * Emits a event when there has been a click outside of the host element
     */
    @Directive({
      selector: '[mdeOnClickOutside]',
    })
    export class OnClickOutsideDirective implements OnInit, OnDestroy {
    
      @Output() mdeOnClickOutside : EventEmitter<Event> = new EventEmitter();
    
      /**
       * (Un-)registers a click listener with the global click service
       */
      @Input() set mdeOnClickOutsideActive(active : boolean) {
        this.inputReceived = true;
        const clickCallback = {
          element: this.element,
          emitter: this.mdeOnClickOutside,
        };
        if (active) {
          this.clickService.addOutsideClickListener(clickCallback);
        } else {
          this.clickService.removeOutsideClickListener(clickCallback);
        }
      }
    
      private inputReceived : boolean;
    
      constructor(private element : ElementRef, private clickService : ClickService) {
      }
    
      /**
       * Activates click listener if it has not explicitly been set
       */
      ngOnInit() : void {
        if (this.inputReceived) return;
        this.mdeOnClickOutsideActive = true;
      }
    
      ngOnDestroy() : void {
        this.mdeOnClickOutsideActive = false;
      }
    
    }
    

    and then use it like this in the template of some other component:

    <div (mdeOnClickOutside)="clickOutside()">Something</div>
    

    with the function in the component:

    clickOutside() {
        console.log('Somebody clicked somwhere outside');
      }