43
Small computers, big performance: Optimize your Angular

Angular performance slides

Embed Size (px)

Citation preview

Small computers, big performance:Optimize your Angular

SpeakersDavid Barreto

Andrew Smith

Agenda

1. Ahead of Time Compilation2. Lazy Loading3. Change Detection4. Memory Leaks5. Server Side Rendering

Rangle Academy

Goal and Structure:

Program to share knowledge within the companyIt follows a "workshop" structureUsually 2 hours longCovers hard and soft skills

Some workshops available:

WebpackReactReact NativeGoogle AnalyticsUnit TestingIntroduction to Payment GatewaysContinuous Delivery to ProductionConflict Management

About the Demo App

Characteristics:

Built using Angular 2.4.1

Uses Angular CLI beta-26

Redux store with ngrx

Tachyons for CSS

Server side rendering with Universal

All the numbers shown are based on:

Low end device emulation (5x slowdown)

Good 3G connection emulation

Ahead of Time Compilation (AoT)

Compilation Modes

Just in Time Compilation (JiT):

Compilation performed in the browser at run timeBigger bundle size (includes the compiler)Takes longer to boot the app

$ ng serve --prod

Ahead of Time Compilation (AoT):

Compilation performed in the server at build timeSmaller bundle size (doesn't include the compiler)The app boots faster

$ ng serve --prod --aot

JiT vs AoT in Demo App (Prod + Gzip)

CSS files are included in the "other js files"

File Size (JiT) Size (AoT)

main.bundle.js 6.4 KB 23.9 KB

vendor.bundle.js 255 KB 158 KB

other js files 48.7 KB 49.6 KB

Total Download 306 KB 231.5 KB

AoT goals (from the ):docs

Faster rendering => Components already compiledFewer async request => Inline external HTML and CSSSmaller bundle size => No compiler shippedDetect template errors => Because they canBetter security => Prevents script injection attack

Boot Time Comparison

Event Time (JiT) Time (AoT)

DOM Content Loaded 5.44 s 3.25 s

Load 5.46 s 3.27 s

FMP 5.49 s 3.30 s

DOM Content Loaded:

The browser has finished parsing the DOMjQuery nostalgia => $(document).ready()

Load: All the assets has been downloaded

First Meaningful Paint (FMP):

When the user is able to see the app "live" for the first time

(Show browser profile for both modes)

Lazy Loading

What is Lazy Loading?

Ability to load modules on demand => Useful to reduce the app startup time

(Compare branches no-lazy-loading vs normal-lazy-loading )

Bundle Sizes Comparison (Prod + AoT)

File Size (No LL) Size (LL)

main.bundle.js 23.9 KB 17.4 KB

vendor.bundle.js 158 KB 158 KB

other js files 49.6 KB 49.6 KB

Initial Download 231.5 KB 225 KB

0.chunk.js - 9.1 KB

Total Download 231.5 KB 234.1 KB

Webpack creates a "chunk" for every lazy loaded moduleThe file 0.chunk.js is loaded when the user navigates to adminThe initial download size is smaller with LLThe total size over time is bigger with LL because of Webpack async loadingThe effect of LL start to be noticeable when the app grows

Boot Time Comparison (Prod + AoT)

Event Time (No LL) Time (LL)

DOM Content Loaded 3.25 s 3.11 s

Load 3.27 s 3.25 s

FMP 3.30 s 3.16 s

Not much difference for an small appJust one lazy loaded module with a couple of componentsThe impact is noticeable for big apps

How to Enable Lazy Loading? (1/4)

Step 1: Organize your code into modules

$ tree src/app -L 1 src/app├── admin/ ├── app-routing.module.ts ├── app.component.ts ├── app.module.ts ├── core/ ├── public/ └── shared/

CoreModule provides all the services and the Redux storeSharedModule provides all the reusable components, directives or pipesPublicModule provides the components and routing of the public section of the appAdminModule provides the components and routing of the private section of the appAppModule root module

(Show modules in the IDE)

How to Enable Lazy Loading (2/4)

Step 2: Create a routing module for lazy loaded module

const routes: Routes = [{ path: '', component: AdminComponent, children: [ { path: '', redirectTo: 'list', pathMatch: 'full' }, { path: 'list', component: WorkshopListComponent }, { path: 'new', component: WorkshopEditorComponent }, { path: 'edit/:id', component: WorkshopEditorComponent }, ] }]; @NgModule({ imports: [ RouterModule.forChild(routes) ], exports: [ RouterModule ], }) export class AdminRoutingModule {}

Always use the method forChild when importing the RouterModuleAvoids duplication of services in the child injector

How to Enable Lazy Loading (3/4)

Step 3: Define a child <router-outlet> to render child routes

@Component({ selector: 'rio-admin', template: ` <nav> <a routerLink="./list">List Workshops</a> <a routerLink="./new">Create Workshop</a> </nav> <router-outlet></router-outlet> `, }) export class AdminComponent {}

How to Enable Lazy Loading (4/4)

Step 4: Use the property loadChildren to lazy load the module

export const routes: Routes = [{ path: 'admin', loadChildren: 'app/admin/admin.module#AdminModule', canActivate: [ AuthGuardService ], }]; @NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [ RouterModule ], }) export class AppRoutingModule {}

Only in the root module use the forRoot method of RouterModuleNever import a lazy loaded in this file, use a string as referenceThe path of loadChildren is not relative to the file, but to the index.html file

(Show routes in the IDE and URL structure of the app)

Preloading

(Compare branches normal-lazy-loading vs master )

Enable Preloading

Define the property preloadingStrategy in the root module routing

import { PreloadAllModules } from '@angular/router'; export const routes: Routes = [ ... ]; @NgModule({ imports: [ RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) ], exports: [ RouterModule ], }) export class AppRoutingModule {}

Change Detection

What's Change Detection (CD)?

It's a mechanism to keep our "models" in sync with our "views"

Change detection is fired when...

The user interacts with the app (click, submit, etc.)An async event is completed (setTimeout, promise, observable)

When CD is fired, Angular will check every component starting from the top once.

Change Detection Strategy: OnPush

Angular offers 2 strategies:

Default: Check the entire component when CD is firedOnPush: Check only relevant subtrees when CD is fired

OnPush Requirements:

Component inputs ( @Input ) need to be immutable objects

@Component({ selector: 'rio-workshop', templateUrl: './workshop.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) export class WorkshopComponent { @Input() workshop: Workshop; @Input() isSummary = false; }

View Example

Example: The Component Tree

Default Change Detection

OnPush Change Detection

Summary

What to do?

Apply the OnPush change detection on every component*Never mutate an object or array, always create a a new reference ( )blog

// Don't let addPerson = (person: Person): void => { people.push(person); }; // Do let addPerson = (people: Person[], person: Person): Person[] => { return [ ...people, person ]; };

Benefits:

Fewer checks of your components during Change DetectionImproved overall app performance

Memory Leaks

What's Memory Leak?

The increase of memory usage over time

What Causes Memory Leaks in Angular?

Main Source => Subscriptions to observables never closed

@Injectable() export class WorkshopService { getAll(): Observable<Workshop[]> { ... } }

@Component({ selector: 'rio-workshop-list', template: ` <div *ngFor="let workshop of workshops"> {{ workshop.title }} </div>` }) export class WorkshopListComponent implements OnInit { ... ngOnInit() { this.service.getAll().subscribe(workshops => this.workshops = workshops); } }

Manually Closing Connections

Before the element is destroyed, close the connection

@Component({ selector: 'rio-workshop-list', template: ` <div *ngFor="let workshop of workshops"> {{ workshop.title }} </div>` }) export class WorkshopListComponent implements OnInit, OnDestroy { ... ngOnInit() { this.subscription = this.service.getAll() .subscribe(workshops => this.workshops = workshops); } ngOnDestroy() { this.subscription.unsubscribe(); } }

The async Pipe

It closes the connection automatically when the component is destroyed

@Component({ selector: 'rio-workshop-list', template: ` <div *ngFor="let workshop of workshops$ | async"> {{ workshop.title }} </div>` }) export class WorkshopListComponent implements OnInit { ngOnInit() { this.workshops$ = this.service.getAll(); } }

This is the recommended way of dealing with Observables in your template!

The Http Service

Every method of the http services ( get , post , etc.) returns an observableThose observables emit only one value and the connection is closed automaticallyThey won't cause memory leak issues

@Component({ selector: 'rio-workshop-list', template: ` <div *ngFor="let workshop of workshops"> {{ workshop.title }} </div>` }) export class WorkshopListComponent implements OnInit { ... ngOnInit() { this.http.get('some-url') .map(data => data.json()) .subscribe(workshops => this.workshops = workshops); } }

Emit a Limited Number of Values

RxJs provides operators to close the connection automaticallyExamples: first() and take(n)

This won't cause memory leak issues even if getAll emits multiple values

@Component({ selector: 'rio-workshop-list', template: ` <div *ngFor="let workshop of workshops"> {{ workshop.title }} </div>` }) export class WorkshopListComponent implements OnInit { ngOnInit() { this.service.getAll().first() .subscribe(workshops => this.workshops = workshops); } }

Server Side Rendering

Angular Universal

Provides the ability to pre-render your application on the server

Much faster time to first paint

Enables better SEO

Enables content preview on social networks

Fallback support for older browsers

Use the as the base of your applicationuniversal-starter

What's Included

Suite of polyfills for the server

Server rendering layer

Preboot - replays your user's interactions after Angular has bootstrapped

State Rehydration - Don't lose your place when the application loads

Boot Time Comparison (Client vs Server)

Both environments include previous AoT and Lazy Loading enhancements

Event Time (Client) Time (Server)

DOM Content Loaded 3.11 s 411 ms

Load 3.25 s 2.88 s

FMP 3.16 s ~440 ms

*Times are on mobile over 3G

Universal Caveats

Cannot directly access the DOM

constructor(element: ElementRef, renderer: Renderer) { renderer.setElementStyle(element.nativeElement, ‘font-size’, ‘x-large’); }

Current solutions only cover Express and ASP.NET servers

Project will be migrated into the core Angular repo for v4

Summary

Performance Changes

Event JiT AoT Lazy Loading SSR

DOM Content Loaded 5.44 s 3.25 s 3.11 s 411 ms

Load 5.46 s 3.27 s 3.25 s 2.88 s

FMP 5.46 s 3.30 s 3.16 s ~440 ms

% Improvement (FMP) 39.6% 4.3% 86.1%

*Times are on mobile over 3G

Slides

https://github.com/rangle/angular-performance-meetup