Angular Performance Tuning
Stepan Suvorov
-
CTO @ Studytube
-
Google Developer Expert at Angular
-
Angular Kharkiv(Ukraine) Meetup organizer
-
Dutch Angular Group co-organizer
-
Angular Workshops on javascript.info
-
Angular Pro Screencast (https://angular.ninja)
Agenda
- Performance
- User perception of loading time
- How to measure?
- Network performance
- Hosting Optimization
- Bundle Size Optimization
- SSR: Scully vs Universal
- ServiceWorkes
- Runtime Optimizations
- Zones
- Change Detection
- Pure Pipes
- Big List Optimization
- WebWorkers
- Memory Leaks
User perception of
loading time
Measure performance with RAIL
User perception
0 to 16 ms |
the best
16 to 100 ms |
users feel like the result is immediate
100 to 1000 ms |
accepted delay for complete task change view
>1000 ms |
user loses the focus
>10000 ms |
users are frustrated and are likely to abandon tasks
process events in <50ms
60 FPS (<16ms)
or <10ms + browser replaint
- use idle time to complete deferred work
- < 50 ms
- priority for the main thread
- < 5s
- mid-range mobile device
- 3G
How To Measure
Network performance
Hosting Optimization
What to do?
- Use CDN
- Use CDN on Global Cloud Provider
- Use good compression (gzip, Brotli)
- Combine small files
- Split big files
- Optimize your bundle
Bundle Size Optimization
Identify the problem
What to do?
- Split into smaller chunks
- Load asynchronously (lazy modules)
- with preloading strategies
- Use light package alternatives
- Do not include source maps
- Target recent browsers
- Monitor your bundle size( "budgets" section in ng-cli config)
- With Tree Shaking in mind
- Tree-shakable providers (@Injectable(provideIn))
- provideIn: 'platform'
Server Side Prerendering
Why to SSR?
-
SEO
-
First screen
-
Social friendly preview
Scully
vs
Universal
What is Scully?
Static Site Generator
- Provide all Jamstack goals
- Dot it for 100% of Angular projects
- Require ~no effort to integrate
- Support Markdown
- Plugin system/ecosystem
- ...
How Scully works ?
- Find all your routes
- Generate all the pages
Router Plugins
Render Plugins
Angular Universal
$ ng add @nguniversal/express-engine
$ ng add @scullyio/init
$ npm run dev:ssr
$ ng build
$ npm run scully
$ npm run scully:serve
init
dev env
prod env
$ npm run build:ssr
$ npm run serve:ssr
$ ng build --prod
$ npm run scully
you need nodejs server!
you can just upload your files to CDN
Angular Universal
rendering
is done with help of a real browser - Puppeteer
- document/window/navigator
- jQuery old plugins
with help of ngExpressEngine (without real browser)
JIT
is built for it
was not made to compile pages on fly (regeneration takes too much time)
partial
you can regenerate any specific page
--routeFilter "*/pages/*"
Angular Universal
benchmarks
speed
Angular Universal
benchmarks
file size
Service Workers
What are Service Workers?
- can't access the DOM directly
- control how network requests from your page
- It's terminated when not in use (no own state)
- extensive use of promises
- HTTPS required
Install with AngularCli
ng add @angular/pwa
- @angular/service-worker
- enables ng-cli support
- import and register SW in the app module
- updates index.html with a link to the manifest file
- ngsw-manifest.json
Why to use SW
- Offline out of box
- Application Updates (SwUpdate)
- Push Notifications (SwPush)
- Cashing resources
What's being cached?
- index.html
- favicon.ico
- build artifacts (js and css bundles)
- assets
ngsw-config.json
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
]
}
ngsw-config
- installMode
- updateMode
- prefetch
- lazy
Caching External Resources
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"index": "/index.html",
"assetGroups": [
...
{
"name": "externals",
"installMode": "lazy",
"updateMode": "lazy",
"resources": {
"urls": ["https://angular.io/assets/images/logos/angular/angular.svg"]
}
}
...
]
}
Caching HTTP Requests
{
...
"assetGroups": [ ... ],
"dataGroups": [
{
...
},
{
...
}
]
}
dataGroups
export interface DataGroup {
name: string;
urls: string[];
version?: number;
cacheConfig: {
maxSize: number;
maxAge: string;
timeout?: string;
strategy?: 'freshness' | 'performance';
};
cacheQueryOptions?: {
ignoreSearch?: boolean;
};
}
dataGroups
strategy: 'freshness' | 'performance';
-
performance(default) - cache first
-
freshness - network first
dataGroups
"dataGroups": [
{
"name": "random.org",
"urls": ["https://api.random.org/**"],
"cacheConfig": {
"maxSize": 20,
"maxAge": "7d",
"strategy": "freshness"
}
}
]
Runtime Optimizations
Zones
zone.js
// zone.js simplified
//--------------------------------
setTimeout(_ => {
console.log('some action');
}, 3000);
zone.js
// zone.js simplified
const oldSetTimeout = setTimeout;
setTimeout = (handler, timer) => {
console.log('START');
oldSetTimeout(_ => {
handler();
console.log('FINISH');
}, timer);
}
//--------------------------------
setTimeout(_ => {
console.log('some action');
}, 3000);
NgZone
// packages/core/src/zone/ng_zone.ts#L304
// ngzone simplified
function onEnter() {
_nesting++;
}
function onLeave() {
_nesting--;
checkStable();
}
function checkStable() {
if (_nesting == 0 && !hasPendingMicrotasks) {
onMicrotaskEmpty.emit(null);
}
}
onMicrotaskEmpty
// packages/core/src/application_ref.ts#L601
// ApplicationRef simplified
this._zone.onMicrotaskEmpty.subscribe({
next: () => {
this._zone.run(() => {
this.tick();
});
}
});
tick() {
for (let view of this._views) {
view.detectChanges();
}
}
ngZoneEventCoalescing
<div (click)="parent()">
<button (click)="child()">Submit</button>
</div>
* from Angular9
platformBrowserDynamic()
.bootstrapModule(AppModule, { ngZoneEventCoalescing: true })
Run code outside Zones
constructor(ngZone: NgZone) {
ngZone.runOutsideAngular(() => {
this._increaseProgress(() => {
// reenter the Angular zone and display done
ngZone.run(() => {
console.log('Outside Done!');
});
});
});
}
CD without Zones
platformBrowserDynamic()
.bootstrapModule(AppModule, { ngZone: 'noop' })
.catch(err => console.error(err));
class AppComponent {
constructor(app: ApplicationRef) {
setInterval(_ => app.tick(), 100);
}
}
Zone patch
<script>
__Zone_disable_timers = true; // setTimeout/setInterval/setImmediate
__Zone_disable_XHR = true; // XMLHttpRequest
__Zone_disable_Error = true; // Error
__Zone_disable_on_property = true; // onProperty such as button.onclick
__Zone_disable_geolocation = true; // geolocation API
__Zone_disable_toString = true; // Function.prototype.toString
__Zone_disable_blocking = true; // alert/prompt/confirm
</script>
Change Detection
most part of illustrations for this topic were taking from @PascalPrecht slides
Each component has its own Change Detector
On Push
@Component({
template: '<user-card [data]="data"></user-card>'
})
class MyApp {
constructor() {
this.data = {
name: 'Jack',
email: 'jack@mail.com'
}
}
changeData() {
this.data.name = 'John';
}
}
MyApp
UserCard
<user-card [data]="data"></user-card>
MyApp
UserCard
<user-card [data]="data"></user-card>
data = { name: "John", email: "jack@mail.com" }
MyApp
UserCard
<user-card [data]="data"></user-card>
data = { name: "John", email: "jack@mail.com" }
oldData === newData
MyApp
UserCard
<user-card [data]="data"></user-card>
data = { name: "John", email: "jack@mail.com" }
oldData === newData
Reference is the same, but the property could've changed (mutable), so we need to check
Angular is conservative by default and checks every component every single time
IMMUTABLE OBJECTS
@Component({
template: '<user-card [data]="data"></user-card>'
})
class MyApp {
constructor() {
this.data = {
name: 'Jack',
email: 'jack@mail.com'
}
}
changeData() {
this.data = {...this.data, name: John};
}
}
@Component({
template: `<h1>{{data.name}}</h1>
<span>{{data.email}}</span>`
})
class UserCardComponent {
@Input() data;
}
@Component({
template: `<h1>{{data.name}}</h1>
<span>{{data.email}}</span>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class UserCardComponent {
@Input() data;
}
MyApp
UserCard
<user-card [data]="data"></user-card>
data = { name : "Jack", email: "jack@mail.com" }
oldData === newData
What if there was no Input() change but we still need to run CD?
@Component({
template: `<h1>{{data.name}}</h1>
<span>{{data.email}}</span>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class UserCardComponent {
@Input() data;
contructor(dataService: DataService) {
dataService.onChange.subscribe(data => {
this.data = data;
});
}
}
Input() did not change, CD propagation stops
@Component({
template: `<h1>{{data.name}}</h1>
<span>{{data.email}}</span>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class UserCardComponent {
@Input() data;
contructor(dataService: DataService,
cd: ChangeDetectorRef) {
dataService.onChange.subscribe(data => {
this.data = data;
cd.markForCheck();
});
}
}
Mark path until root for check
Perform change detection as usual
Restore original state
cd.markForCheck()
vs
cd.detectChanges()
cd.detach()
/cd.reattach()
@Component({
...
})
class UserCardComponent {
contructor(cd: ChangeDetectorRef) {
cd.detach();
// start of a heavy operation
// for which we want to skip CD
// ...
// ...
cd.reattach();
}
}
Pure Pipes
Pure Pipes
@Pipe({
name: 'mypipe',
pure: false/true <----- here (default is `true`)
})
export class MyPipe {
}
Pure Function
function sum(a, b) {
return a + b;
};
sum(1, 1); // 2
sum(1, 1); // 2
sum(1, 1); // 2
let state = 0;
function changeState(v) {
return state += v;
}
changeState(1); // 1
changeState(1); // 2
changeState(1); // 3
How is it related to performance?
Pure Pipes on steroids
npm i memo-decorator --save
Minko Gechev
import memo from 'memo-decorator';
...
@memo()
transform(value: string): string {
...
}
Big Lists Optimisation
Virtual Scroll
The image was taken from zangxx66/vue-virtual-scroll-list
Virtual Scroll in Angular
Angular CDK
Virtual Scroll
Web Workers
Web Workers
ng generate webWorker my-worker
const worker = new Worker(`./my-worker.worker`,
{ type: `module` });
worker.onmessage = ({ data }) => {
console.log(`page got message: ${data}`);
};
worker.postMessage('hello');
Memory leaks
What is memory leak?
What is memory leak?
What is memory leak?
How to detect memory leak?
Performance Monitor in Chrome DevTools
How to detect memory leak?
Performance Monitor in Chrome DevTools
What's the cause?
Unhandled Subscription
What to do with a Subscription?
- unsubscribe always!
- ngOnDestroy()
- decorator
- takeUntil() (takeWhile(), first(), last(), take())
- Async Pipe
Thank you for your attention.
Questions?
Grazie per l'attenzione
Angular Performance Tuning
By Stepan Suvorov
Angular Performance Tuning
- 997