Speed Up Angular App
Compression
compression reduces file sizes by a decent margin, so it will reduce the take required to load the wep app: first you need to create a script that will automatically compress all the files in the dist folder and its subfolders as well this script will use gzip and brotli algorithms: compress.ts
import {readFileSync, writeFileSync} from 'fs';
import {compress} from 'brotli';
import {gzip} from 'compressing';
import {resolve} from 'path';
const {readdir} = require('fs').promises;
const distFolder = './dist/browser';
const brotliSettings = {
extension: 'br',
skipLarger: true,
mode: 1, // 0 = generic, 1 = text, 2 = font (WOFF2)
quality: 11, // 0 - 11,
lgwin: 12, // default
threshold: 10240
};
async function* getFiles(dir: string) {
const dirents = await readdir(dir, {withFileTypes: true});
for (const dirent of dirents) {
const res = resolve(dir, dirent.name);
if (dirent.isDirectory()) {
yield* getFiles(res);
} else {
yield res;
}
}
}
function brotliCompression(input: string, output: string, settings: any) {
const result = compress(readFileSync(input), settings);
if (result) writeFileSync(output, result);
}
function gzipCompression(input: string, output: string) {
return gzip.compressFile(input, output);
}
async function AllCompression(file: string, settings: any) {
brotliCompression(file, file + '.br', settings);
await gzipCompression(file, file + '.gz');
}
async function compressFolderFiles(distFolder: string) {
for await (const file of getFiles(distFolder)) {
if (
file.endsWith('.ico') ||
file.endsWith('.jpg') ||
file.endsWith('.jpeg') ||
file.endsWith('.png')
)
brotliSettings.mode = 0;
else if (
file.endsWith('.js') ||
file.endsWith('.css') ||
file.endsWith('.html') ||
file.endsWith('.svg')
)
brotliSettings.mode = 1;
else if (
file.endsWith('.ttf') ||
file.endsWith('.eot') ||
file.endsWith('.woff') ||
file.endsWith('.woff2')
)
brotliSettings.mode = 2;
else continue;
console.log('compressing: ', file);
await AllCompression(file, brotliSettings);
}
}
compressFolderFiles(distFolder);
then add this to your package.json:
"compress": "tsc compress.ts && node compress.js",
Lazy Loaded Modules
split your web app to separate modules and load them on demand using the import syntax like following
const routes: Routes = [
{
path: 'home',
loadChildren: () =>
import('./home.module').then(
m => m.HomeModule
)
},
]
Preload Strategy
preload strategy allow you to load modules, before the user requested them so the pages will load faster, I use ngx-quicklink
package to accomplish this in app.module.ts
import { QuicklinkModule } from 'ngx-quicklink';
@NgModule({
imports: [
QuicklinkModule,
]
})
in `app-routing.module.ts
import { QuicklinkStrategy } from 'ngx-quicklink';
@NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy: QuicklinkStrategy })],
})
SSR
ssr enables your page to be rendered on the server, which speed up the load time, and first contentfull paint, also improves seo angular does everything automatically, just run the following cmd
ng add @nguniversal/express-engine
to test it out run:
npm run dev:ssr
OnPush change detection
on push strategy improve performance by preventing angular from constantly checking for changes, instead it checks for change only on few cases, and if you requested it programatically.
import {ChangeDetectionStrategy, Component} from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./home.component.scss']
})
you can fire change detection manually using ChangeDetectorRef
import {ChangeDetectorRef} from '@angular/core';
private user: User;
constructor(
private ref: ChangeDetectorRef
) {}
updateUser() {
user.name = 'trump';
this.ref.markForCheck();
}
Resources Lazy Loading
load css files only if they are needed create a server called LazyLoadResoureces
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
interface Resource<T> {
tag: string
attr: string
value: string
attr2: string
value2: string | boolean
parentTag: string
}
@Injectable({
providedIn: 'root'
})
export class LazyLoadResourcesService {
private _bootstrapJs: Resource<HTMLScriptElement> = { tag: 'script', attr: 'src', value: "bootstrapjs.min.js", attr2: "async", value2: true, parentTag: 'body' }
private _jqueryJs: Resource<HTMLScriptElement> = { tag: 'script', attr: 'src', value: "jquery.min.js", attr2: "async", value2: null, parentTag: 'body' }
private _gtagJs: Resource<HTMLScriptElement> = { tag: 'script', attr: 'src', value: "https://www.googletagmanager.com/gtag/js", attr2: "defer", value2: true, parentTag: 'body' }
private _bootstrapCss: Resource<HTMLLinkElement> = { tag: 'link', attr: 'href', value: "bootstrap.min.css", attr2: "rel", value2: "stylesheet", parentTag: 'head' }
private _stylesCss: Resource<HTMLLinkElement> = { tag: 'link', attr: 'href', value: "styles.min.css", attr2: "rel", value2: "stylesheet", parentTag: 'head' }
private _fontAwesomeCss: Resource<HTMLLinkElement> = { tag: 'link', attr: 'href', value: "font-awesome.min.css", attr2: "rel", value2: "stylesheet", parentTag: 'head' }
public loadAuthModuleResources(){
!this._ResourceExists(this._gtagJs) && this._loadResource(this._gtagJs)
!this._ResourceExists(this._bootstrapCss) && this._loadResource(this._bootstrapCss)
!this._ResourceExists(this._stylesCss) && this._loadResource(this._stylesCss)
!this._ResourceExists(this._fontAwesomeCss) && this._loadResource(this._fontAwesomeCss)
}
public loadDashboardModuleResources(){
!this._ResourceExists(this._gtagJs) && this._loadResource(this._gtagJs)
!this._ResourceExists(this._stylesCss) && this._loadResource(this._stylesCss)
!this._ResourceExists(this._bootstrapCss) && this._loadResource(this._bootstrapCss)
!this._ResourceExists(this._fontAwesomeCss) && this._loadResource(this._fontAwesomeCss)
!this._ResourceExists(this._jqueryJs) && this._loadResource(this._jqueryJs)
!this._ResourceExists(this._bootstrapJs) && this._loadResource(this._bootstrapJs)
}
private _loadResource<T>(res: Resource<T>): void{
const element = window.document.createElement(res.tag) as unknown as T
element[res.attr] = res.value
element[res.attr2] = res.value2
window.document[res.parentTag].appendChild(element)
}
private _ResourceExists<T>(res: Resource<T>): boolean{
return !!window.document.querySelector(`${res.tag}[${res.attr}="${res.value}"][${res.attr2}="${res.value2}"]`)
}
}
then use it in your components:
constructor(
private _lazyLoadResourcesService: LazyLoadResourcesService,
) { }
ngOnInit(): void {
this._lazyLoadResourcesService.loadAuthModuleResources()
}
Use Async Pipe
whenever you loaded data from external apis, just to display them on the view, you should use async pipe.
<ul>
<li *ngFor="let product of productsList$ | async">
<!-- { { product.name } } -->
</li>
</ul>
Misc
- implement resource budgeting
- reduct usage of third party libs
- analyse js bundles with
source-map-explorer
- improve web vitals matrix
- audit the app with lighthouse
- defer css/js load as much as possible
- implement service worker