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)
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
or <10ms + browser replaint
SEO
First screen
Social friendly preview
Â
Static Site Generator
Router Plugins
Render Plugins
$ 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
rendering
is done with help of a real browser - Puppeteer
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/*"
benchmarks
speed
benchmarks
file size
ng add @angular/pwa
{
"$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)"
]
}
}
]
}
{
"$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"]
}
}
...
]
}
{
...
"assetGroups": [ ... ],
"dataGroups": [
{
...
},
{
...
}
]
}
export interface DataGroup {
name: string;
urls: string[];
version?: number;
cacheConfig: {
maxSize: number;
maxAge: string;
timeout?: string;
strategy?: 'freshness' | 'performance';
};
cacheQueryOptions?: {
ignoreSearch?: boolean;
};
}
strategy: 'freshness' | 'performance';
performance(default) - cache first
freshness - network first
"dataGroups": [
{
"name": "random.org",
"urls": ["https://api.random.org/**"],
"cacheConfig": {
"maxSize": 20,
"maxAge": "7d",
"strategy": "freshness"
}
}
]
// zone.js simplified
//--------------------------------
setTimeout(_ => {
console.log('some action');
}, 3000);
// zone.js simplified
const oldSetTimeout = setTimeout;
setTimeout = (handler, timer) => {
console.log('START');
oldSetTimeout(_ => {
handler();
console.log('FINISH');
}, timer);
}
//--------------------------------
setTimeout(_ => {
console.log('some action');
}, 3000);
// 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);
}
}
// 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();
}
}
<div (click)="parent()">
<button (click)="child()">Submit</button>
</div>
* from Angular9
platformBrowserDynamic()
.bootstrapModule(AppModule, { ngZoneEventCoalescing: true })
constructor(ngZone: NgZone) {
ngZone.runOutsideAngular(() => {
this._increaseProgress(() => {
// reenter the Angular zone and display done
ngZone.run(() => {
console.log('Outside Done!');
});
});
});
}
platformBrowserDynamic()
.bootstrapModule(AppModule, { ngZone: 'noop' })
.catch(err => console.error(err));
class AppComponent {
constructor(app: ApplicationRef) {
setInterval(_ => app.tick(), 100);
}
}
<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>
most part of illustrations for this topic were taking from @PascalPrecht slides
Each component has its own Change Detector
@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
@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
@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
@Component({
...
})
class UserCardComponent {
contructor(cd: ChangeDetectorRef) {
cd.detach();
// start of a heavy operation
// for which we want to skip CD
// ...
// ...
cd.reattach();
}
}
@Pipe({
name: 'mypipe',
pure: false/true <----- here (default is `true`)
})
export class MyPipe {
}
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
npm i memo-decorator --save
Minko Gechev
import memo from 'memo-decorator';
...
@memo()
transform(value: string): string {
...
}
The image was taken from zangxx66/vue-virtual-scroll-list
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');
Performance Monitor in Chrome DevTools
Performance Monitor in Chrome DevTools