Optimizing Angular apps

Disclaimer

This is a set of tips I've learned from people way smarter than me

Not as a call for a revolution in our apps

Treat this as opportunity to supplement your knowledge

🤯

🤔

⚔️

General

Angular

General

Managing your NPM packages

Source: https://www.youtube.com/watch?v=M3BM9TB-8yA 

(10 Things I Regret About Node.js - Ryan Dahl)

Review your dependencies

source-map-explorer

Check the size first

Deduplicate 

Not everything belongs to "dependencies"

Keeping bundle size in check

(Tree) Shake It!

General

JavaScript

Generally, do what you always do

  • use the algorithms with the least computational complexity
  • avoid recursive calls
  • put the calculations and calls to functions that are repeated to variables
  • etc.

Beware fancy code

var numbers = [2, 4, 12, 6, 8, 29, 5, 10, 87, 11, 7];

function process(arr) {
    let newArr = arr.slice();
    newArr[0]++;
    for (let i = 1; i < newArr.length; i++) {
        const current = newArr[i] + 1;
        let leftIndex = i - 1;

        while (leftIndex >= 0 && newArr[leftIndex] > current) {
            newArr[leftIndex + 1] = newArr[leftIndex];
            leftIndex = leftIndex - 1;
        }
        newArr[leftIndex + 1] = current;
    }
    return newArr;
}

const newArray = process(numbers);

Beware fancy code

const process = arr => arr
  .map(num => num + 1)
  .sort((a, b) => a - b);

const newArray = process(numbers);

... which is 75% slower

than the previous solution.

Web Workers

// web-worker.worker.ts

addEventListener('message', ({ data }) => {
  const val = someHeavyOperation();
  postMessage(val);
});
// some.component.ts  

startWebWorker() {
    if (typeof Worker !== 'undefined') {
      const worker = new Worker(
        './web-worker.worker', 
        { type: 'module' }
      );
      worker.onmessage = ({ data }) => {
        // Hide loading widget
        this.loading = false; 
      };
      // Display loading widget
      this.loading = true; 
      worker.postMessage(this.dataSize);
    }
  }

Profiling Tools: Chrome's Lighthouse

Profiling Tools: Node Clinic

Angular

"Optimizing an Angular application" - Minko Gechev

Minko's tips

  • keep in mind the Angular's change detection (use ChangeDetectionStrategy.OnPush and separate your components)
  • use immutable data structures (Immer, Immutable.js )
  • use pure pipes instead invoking function in a template
  • cache outputs (memoizing)
  • think about an optimal strategy for refreshing data from a server

Lazy-loading modules

const routes: Routes = [
  {
    path: 'customers',
    loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule)
  },
  {
    path: 'orders',
    loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule)
  }
];

Use trackBy in *ngFor

@Component({
  selector: 'app', 
  template: `
    <ul>
      <li *ngFor="let item of items; trackBy: trackById">
        {{item.name}}
      </li>
    </ul>`
})
class AppComponent {
  items = [
    {
      id: 1,
      name: 'item 1'
    }, 
    {
      id: 2,
      name: 'item 2'
    }
  ];

  trackById(index, item) {
    return item.id;
  }
}

Don't leave your Observables subscribed or Promises pending

@Component(...)
class AppComponent implements OnInit, OnDestroy {
  customers$ = new Subject<Customer[]>();
  sub = new Subscription();

  ...
  
  ngOnInit() {
    this.sub = this.customers$.subscribe(...);
  }

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

Don't manipulate DOM directly or use addEventListener

@Component({
  selector: 'app-click-me',
  template: `
    <button #click-me-btn>Click me!</button>
    {{clickMessage}}
  `
})
export class ClickMeComponent {
  clickMessage = '';

  ngOnInit() {
    const el = document.getElementById('click-me-btn'); // ⚠️ #1
    el.addEventListener('click', event => {	        // ⚠️ #2
      this.onClickMe();
      el.style.color = 'green';                         // ⚠️ #3
      console.log('Clicked!');                          // ⚠️ #4
    });
  }

  onClickMe = () => {
    this.clickMessage = 'You are my hero!';
  }
}
  • addEventListener may result in memory leaks
  • direct DOM manipulation is slow and inefficient (Angular uses virtual DOM)
  • even using console.log() slows down the app!

Leveraging the NgZone

@Component(...)
export class DragAndDropComponent {
  element: HTMLElement;

  constructor(private zone: NgZone) {}

  mouseDown(event) {
    this.element = event.target;

    this.zone.runOutsideAngular(() => {
      window.document.addEventListener('mousemove', this.mouseMove);
    });
  }

  mouseMove = (event) => {
    event.preventDefault();
    this.element.setAttribute('x', event...);
    this.element.setAttribute('y', event...);
  }
  
  mouseUp(event) {
    this.zone.run(() => {
      this.updateBox(...);
    });

    window.document.removeEventListener('mousemove', this.mouseMove);
  }
}

Progressive Web Apps

Angular Ivy compiler

  • 🚀 better build times (with a more incremental compilation)
  •  🔥 better build sizes (with a generated code more compatible with tree-shaking)
  • 🔓 new potential features

Final, general tips

  • compress/transform your images and static files
  • remove unused CSS

Thank you!

✋😎

@kajetansw

kajetan.dev

Optimizing Angular app

By Kajetan Świątek

Optimizing Angular app

  • 332