Skip to content

⚡⚡ A collection of Angular best practices and tips for building fast, scalable enterprise level applications! Awesome coding! 😎🤘

License

Notifications You must be signed in to change notification settings

akhilben/angular-awesome

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation


Logo

⭐ Collection of best practices and tips for building scalable and performant Angular applications. ⭐
Report Bug · Request Feature

GitHub contributorsGitHubMaintenanceForHelp


🌟 Preface

This repo serves as a reference handbook for both starters and experienced developers to look at from time to time to refresh memory and to develop better 📓.

⚠️ Warning : The contents in this document are just general suggestions taken from the Angular community on how to do stuff. As there can be many solutions to a problem, just take the contents as mere suggestions and make use of it if you think it suits your application.

Do lookout (and don't ignore 👀) for the Tips section to find out how to do things more better. Make sure to expand the collapse of a section; it's there just for a better user experience. Get the summarized points labelled as Takeaway at the end of each section if you don't want to spend your time to read the whole.

Now, step in to find some of the recommended good practices and tips for building awesome Angular applications 🎊 🎉.


📑 Table of Contents


😎 IDE and Plugins

Click to expand There are several powerful free and paid IDE's available in the market today. Choosing the right IDE is very important for development since not all IDE's are great for every language/framework/library. Have added a list of awesome plugins/extensions for IDE's below for a smooth development.
  1. VS Code   Visual Studio Code
    VS Code is a very powerful code editor from Microsoft which is highly recommended for working with Angular. Why? It has a great support for TypeScript out of the box. Moreover, it has syntax highlighting and autocomplete with IntelliSense, which provides smart completions based on variable types, function definitions, and imported modules. Adding up to many other powerfull features, it is highly customizable with tons of extensions, especially for Angular. Below is a list of few vs code extensions that are useful while developing Angular applications.

    • ES Lint:
      A must have extension which marks the code where you have a problem and display a list of warnings & errors on hovering it. It even have an autofix problems functionality. This will help you to adhere to the recommented styleguides and conventions for Angular.
    • Angular Language Service:
      This extension provides a rich editing experience for Angular templates, both inline and external templates including completions lists, AOT diagnostic messages, quick info and go to definition
    • Angular Snippets:
      This extension adds snippets for Angular for TypeScript, HTML and NgRx. This will help you save a lot of time while developing applications. Just type part of a snippet, press enter, and the snippet unfolds! Angular 8 Snippets is also another similar, honorable mention.
    • Angular Schematics:
      This extension allows you to generate Angular schematics with a Graphical User Interface. This extension promote Angular good practices, by improving component generation with the suggestion of different component types. Use this extension to quickly generate component, module, service etc. Angular Files is also another similar, honorable mention.
    • Prettier - Code formatter:
      Prettier is an opinionated code formatter. It enforces a consistent style by parsing your code and re-printing it with its own rules that take the maximum line length into account, wrapping code when necessary.
    • GitLens — Git supercharged:
      GitLens supercharges the Git capabilities built into Visual Studio Code. It helps you to visualize code authorship at a glance via Git blame annotations and code lens, seamlessly navigate and explore Git repositories, gain valuable insights via powerful comparison commands, and so much more.
  2. WebStorm   WebStorm
    One of the smartest and most powerful IDE's for developing Javascript applications available out there. WebStorm by IntelliJ is a highly recommended pick for developing Angular applications with built-in support for TypeScript out of the box. WebStorm comes with intelligent code completion, on-the-fly error detection, powerful navigation and refactoring for Typescript and stylesheet languages. WebStorm is fully packed with a variety of built-in developer tools and various other features and thus saves your time juggling multiple plugins for seamless development. Below is a list of few WebStorm plugins that are useful while developing Angular applications.

    • Angular and AngularJS:
      This all-in-one framework integration plugin packs tons of features such as: code completion for components, built-in and custom directives, and methods in both templates and ts files; navigation from the component, custom directives and event handlers to their definition; code snippets and Angular CLI integration.
    • Prettier:
      This plugin adds support for Prettier, an opinionated code formatter. It enforces a consistent style by parsing your code and re-printing it with its own rules that take the maximum line length into account, wrapping code when necessary.
    • GitToolBox:
      This plugin extends Git Integration with additional features such as status display, auto fetch, inline blame annotation, commit dialog completion, behind notifications and more.

Do check out the Angular IDE by Codemix, which is a dedicated, powerful IDE for Angular.


❤️ Takeaway : Use your favorite IDE (Visual Studio Code or WebStorm recommended) along with awesome plugins for a smooth development experience.

🔝 Back to Contents


🎉 Using Starter Kits

Click to expand

There is no doubt that the Angular CLI's ng new command generates a decent base app to kick-start your project. But sometimes, we want more. Angular starter kits/boilerplates will heavily reduce the development time for initial setups - from basic recommended folder structure to interceptors and guards, these seeds have many features readily available. Below are some of the most used and well maintained Angular starter kits:

  1. Angular, NgRx and Angular Material Starter : As the name suggests, the stack includes Angular, NgRx, Angular Material and Bootstrap 4. This starter has a strong application structure that is easily scalable and suitable for big projects. It also packs basic interceptors, error-handlers, auth-guards, ngrx files, Travis CLI etc. used along with a TODO application example.

  2. ngX Starter Kit : Generated using ngx-Rocket, this starter kit includes modern tools and workflow based on angular-cli, best practices from the community, a scalable base template and a good learning base. This starter kit comes pre-equipped with Bootstrat 4, Font Awesome, RxJS, ng-bootstrap, ngx-translate and Lodash. The starter also includes a basic **login screen, interceptors, guards etc.

  3. ngx-admin : One of the most widely used Angular admin dashboard template based on Angular 9+, Bootstrap 4+ and Nebular. This template packs all the features and more that you will need for an admin dashboard template.


❤️ Takeaway : Use starters/boilerplates to save initial setup time and for a smooth start.

🔝 Back to Contents


❄️ Commit Guidelines

Click to expand

Usually developers tend to add some random commit messages which doesn't actually add any value to the project. By using some precise rules over how the commit messages are formatted can lead to more readable messages that are easy to follow when looking through the project history. We can even generate changelogs from such commit messages 😲! It's recommended to follow the commit guidelines from the official Angular repo.

  • Each commit message consists of a header, a body, and a footer (optional). The header has a special format that includes a type, and a subject:

    <type>: <subject>
    <BLANK LINE>
    <body>
    <BLANK LINE>
    <footer>
    
  • The type can be any of the following:

    • docs: Documentation only changes
    • feat: A new feature
    • fix: A bug fix
    • perf: A code change that improves performance
    • refactor: A code change that neither fixes a bug nor adds a feature
    • test: Adding missing tests or correcting existing tests
  • The footer should contain a closing reference issue if any.


Eg:

fix: no password validations

length and pattern validations for password

PR Close #11721

💡 Tip : Use Conventional Changelog or it’s standard version to generate changelogs and release notes from project's commit messages and metadata with this commit guideline.


❤️ Takeaway : It's recommended to follow the commit guidelines of the official Angular repo; consisting of a header, a body, and a footer (optional). Use these commits for generating changelogs.

🔝 Back to Contents


👷 Configuring Your Project

Click to expand

There is no doubt that Angular CLI has covered most of the recommended configurations out of the box for us. But we can still make it better 😻!

TSLint (⚠️ Deprecated from Angular 11)

Angular CLI generates a basic set of tslint rules for us for static code analysis using codelyzer by Minko Gechev. Below is the recommended configuration:

{
// The rules component-selector and directive-selector have the following arguments:
// [ENABLED, "attribute" | "element", "prefix" | ["listOfPrefixes"], "camelCase" | "kebab-case"]
"component-selector": [true, "element", ["cmp-prefix1", "cmp-prefix2"], "kebab-case"],
"directive-selector": [true, "attribute", ["dir-prefix1", "dir-prefix2"], "camelCase"],

"component-max-inline-declarations": true,
"contextual-lifecycle": true,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"no-queries-metadata-property": true,
"prefer-inline-decorator": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true,

// The rules component-class-suffix and directive-class-suffix have the following arguments:
// [ENABLED, "suffix" | ["listOfSuffixes"]]
// Where "suffix" is/are your custom(s) suffix(es), for instance "Page" for Ionic components.
"component-class-suffix": [true, "Component"],
"directive-class-suffix": [true, "Directive"]
}

💡 Tips : Want to add more rules on top of the Angular CLI configuration? It's highly recommended to use the Angular TSLint Preset by Minko Gechev.
It's highly recommended to use Husky 🐶 to check for lint issues on a git commit hook and avoid bad commit/push.


Prettier

The opinionated code formatter, prettifies our code to look even more beautiful 😍. First step is to install the Prettier plugin in your favorite IDE (go to Choosing IDE) or npm install prettier to make your team members reference the same configuration file regardless of the IDE. Don't forget to set the format on save option in your IDE.

// For VS Code
“editor.formatOnSave”: true

Great 👏! But, how will the prettier and tslint work together? It’s simple, we can leave the code-quality rules for TSLint to handle, and we can have Prettier take care of formatting rules by removing formatting rules from tslint.json.


🎁 Resources : Check out Setting up Prettier in an Angular CLI Project by Victor Mejia.


💡 Tip : Use tslint-config-prettier to use TSLint and Prettier without conflicts.


Aliasing Folders

You might have come across some import statements in codes which is way too long and dirty, like this import { RandomService } from '../../../../core/services/random.service' 😵. To avaoid such situations, we can use aliases for folder paths making the imports cleaner, readable and consistent. So, how will we do this? Head to the tsconfig.json in your project root and add aliases to the paths property:

"paths": {
     "@app/*": ["./src/app/*"],
     "@shared/*": ["./src/app/shared/*"],
     "@core/*": ["./src/app/core/*"],
     "@env/*": ["environments/*"]
   }

Awesome 😎! Now we can import with more cleaner statements like :

import { RandomService } from '@core/services/random.service'

Ok, but hold on, what about imports for style processors like scss? Angular have a solution for that as well 🍻. Head on to angular.json and add a property to the options property (the property inside which you add additional scripts/styles).

"stylePreprocessorOptions": {
   	"includePaths": [
   		"src/theme/"
   	]
   }

(:page_with_curl: Note that we have included the path in assumption that scss files are inside theme folder) Cool ⛄! Now we can import like :

@import "variables";

🎁 Resources :
1. Check out 6 Best Practices & Pro Tips when using Angular CLI by @tomastrajan.
2. Check out Angular - Shortcut to Importing Styles Files in Components on scotch.io


❤️ Takeaway : Setup tslint for code quality rules and Prettier for code formatting rules and use pre-commit hooks to check for lint issues. Add aliases for folders to remove relative paths in import statements.

🔝 Back to Contents


📚 Folder Structure

Click to expand

Finding a scalable and clean folder structure architecture is always hard. Without a proper architecture, you will end up having a really clumsy and hard to maintain piece of codes 😟. There are several blogs/repos which specifies a proper architecture for Angular applications, and each one is a bit different. This section is just a suggestion for architecting a proper folder structure; so just take away the main points and choose a suitable one for yourself (blogs and repos attached in resources at the end of this section). A good guideline to follow is to split our application into at least three different modules — Core, Shared and Feature. Okay, let's dive into it one by one 🐬.

Core Module

Ideally, the CoreModule contains files that are singleton, that is, those files which we only need to load at run-time. The module can contain singleton services, core components, guards, interceptors, constants, enums and core models. The core module should be imported only once, which is inside AppModule.

|-- 📁 core
|      |-- 📁 components
|      |      |-- 📁 shells
|      |      |-- 📁 header
|      |      |-- 📁 page-not-found
|      |      ...
|      |-- 📁 guards
|      |      |-- 📄 auth.guard.ts
|      |      ...
|      |-- 📁 interceptors
|      |      |-- 📄 api-prefix.interceptor.ts
|      |      |-- 📄 error-handler.interceptor.ts
|      |      ...
|      |-- 📁 services
|      |      |-- 📄 utility.service.ts
|      |      |-- 📄 authentication.service.ts
|      |      ...
|      |-- 📁 enums
|      |-- 📁 models
|      |-- 📁 constants
|      |-- 📄 core.module.ts
|      |-- 📄 ensureModuleLoadedOnceGuard.ts
|      |-- 📄 logger.service.ts

💡 Tips : Add a check in the CoreModule constructor and throw an error if already loaded or add a guard for the same to avoid accidental imports (refer here) .
Add a logger system in logger.service.ts file (refer here).


Shared Module

SharedModule is the module where all our shared or dumb components, directives and pipes go. We can also add any third-party components/directives/pipes (like Angular Material components) which will be needed throughout our application, to the imports and exports of the module. This module can be then imported to each feature module.

|-- 📁 shared
|      |-- 📁 components
|      |      |-- 📁 loader
|      |      |-- 📁 confirmation-dialog
|      |      ...
|      |-- 📁 directives
|      |      |-- 📄 drag-drop.directive.ts
|      |      |-- 📄 scroll-spy.directive.ts
|      |      ...
|      |-- 📁 pipes
|      |      |-- 📄 custom-date.pipe.ts
|      |      |-- 📄 safe.pipe.ts
|      |      ...
|      |-- 📁 models
|      |      |-- 📄 user.ts
|      |      |-- 📄 api-response.ts
|      |-- 📄 shared.module.ts

📃 Note : Don't forget to add the components, directives and pipes to the exports inside shared.module.ts.


Feature Modules

It's recommended to create feature modules for every independent feature of our application. Ideally, feature modules shouldn't be dependant on other modules other than the services provided by CoreModule and features exported by SharedModule. A feature module directory can contain components, pages, services etc. that is specific to the corresponding feature.

The pages folder contains higher level components or even lazy loaded modules. We can refer to a page as the parent component which contains several other child components which ultimately makes up a page. The components folder contains components which are consumed by various pages of the corresponding feature.

|-- 📁 modules
|      |-- 📁 home
|      |      |-- 📁 components
|      |      |      |-- 📁 table
|      |      |      ...
|      |      |-- 📁 pages
|      |      |      |-- 📁 home
|      |      |      |-- 📁 details
|      |      |      ...
|      |      ...
|      |      |-- 📄 home-routing.module.ts
|      |      |-- 📄 home.module.ts
|      |-- 📁 users
|      |      |-- 📁 components
|      |      |-- 📁 pages
|      |      ...
|      |      |-- 📄 users-routing.module.ts
|      |      |-- 📄 users.module.ts

Styling

It's preferred to add a theme folder inside src to include custom themes and scss variables.

|-- 📁 theme
|      |-- 📁 partials
|      |-- 📄 variables.scss
|      |      ...
|      |-- 📄 theme.scss

Now we can import the theme.scss in our main styles.scss where we add global styles and import other style files.


🎁 Resources :
1. Check out Choosing The Right File Structure for Angular in 2020 and Beyond 📕! by Mathis Garberg.
2. Check out How to architect epic Angular app in less than 10 minutes! ⏱️😅 by Tomas Trajan.
3. Check out Angular Folder Structure by Tom Cowley.
4. Check out the excellent folder structure in these starter kits.


❤️ Takeaway : Split our application into at least three different modules — Core, Shared and Feature; below is the bigger picture 🖼️ :
src/
|-- 📁 app
|      |-- 📁 core            	    core module (singleton services, single-use components, interceptors, guards etc.)
|      |-- 📁 shared          	    shared module  (common components, directives and pipes)
|      |-- 📁 modules         	    feature modules (each containing pages, components, services etc.)
|      |-- 📄 app.component.*
|      |-- 📄 app.module.ts
|      |-- 📄 app-routing.module.ts
|      +- ...
|-- 📁 assets
|-- 📁 environments
|-- 📁 theme                  	    app global scss variables, partials and theme
|-- 📁 translations/                translations files
|-- 📄 index.html
+- ...
|-- 📄 main.ts

🔝 Back to Contents


⚡ Performance Cheatsheet

Click to expand

It's a known fact that Angular is a highly performant framework. But we can make it better by following some best practices. Let's jump in to the list of such best practices 🤓 :

1. Avoid function calls in templates.

Click to expand

It is not a good practice to write function calls for computing values inside the templates. And if it's a complex function, a big NO 💀.

<tr *ngFor="let book of books">
  {{calculatePrice(book)}}
</tr>

Why?

Angular will run your function in each of it's change detection cycle (which is quite frequent) and if the function is a complex one, this will impose a serious effect on the performance.

Solution:

There may be some cases where this is unavoidable, but for most cases this can be avoided by:

  1. Creating a property in the ts file and setting the value to it once.

    this.books = this.books.map(book => ( { ...books, price: calculatePrice(book)  } );
  2. Using pure pipes : A pure pipe is a pipe that will always always return the same output for an input. Angular executes a pure pipe only when it detects a pure change to the input value because it already knows that the pipe will return the same value for the same input.

    @Pipe({
    	name: 'dummy',
    	pure: true
    })
    export class somePipe implements PipeTransform {
    	transform(value: any, args?: any): any {
    		 // logic
    	}
    }

    🎁 Resources : Check out The essential difference between pure and impure pipes in Angular and why that matters by Max Koretskyi

2. Use trackBy with ngFor

Click to expand

When using *ngFor to loop over an array which might change over time, it is recommended to use trackBy to track array items with unique identifier.

Why?

When an array changes (eg: when we push a new item to array), Angular will remove all the DOM elements associated with that array and create all of it again. This is because Angular has no knowledge of which items have been removed or added.

Solution:

Use trackBy. If we provide a trackBy function, Angular can track which items have been added or removed in the array according to the unique identifier. It then has to create or destroy the DOM elements for only those items that have changed. Cool 💎! Now, this is how we dot it:

<tr *ngFor="let book of books; trackBy: trackByFn"">{{book.name}}</tr>

and then in your component.ts:

trackByFn(index: number, book: Book) {
    return item.id;
}

🎁 Resources : Check out Angular — Improve Performance with trackBy by Netanel Basal

3. Use tree-shakable providers

Click to expand

Make your services tree-shakable by using the providedIn attribute of the @Injectable() decorator instead of explicitely specifying in the providers attribute of a module/component 🌴.

Why?

If your service is tree-shakable, Angular can exclude the code in the final build bundle provided that it's not used anywhere. This can help reduce the overall bundle size 🎉.

Solution:

By default, value for providedIn is root. This will provide your service at the root injector level and can be injected anywhere in your application. Angular 9+ comes with a couple of other options as well:

  1. platform - The use case comes when you are running multiple angular elements (web components) in a single page. Your service will now become a global singleton at the platform-level, and is shared between all of the Angular applications on your page.
  2. any - Angular will provide a unique instance of your service for every module that injects it.
import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root", // or 'any' or 'platform'
})
export class SomeService {}

🎁 Resources : Check out Dependency injection with Angular 9 by Alain Chautard

4. Unsubscribe observables

Click to expand

When you subscribe to observables, make sure to unsubscribe them in the ngOnDestroy method 👀.

Why?

If you fail to unsubscribe your observables, chances are there that memory leaks might happen since your observable stream is left open even after that component is destroyed. This will lead to uexpected behaviours and serious performance issues 😱.

Solution:

There are various ways to unsubscribe observables:

  1. We can make use of takeUntil to listen to the changes until another observable emits a value.

    private _destroy$ = new Subject();
    
    public ngOnInit(): void {
      sampleObservable$
      .pipe(
        // listen to the observable until this._destroy$ emits a value
        takeUntil(this._destroy$)
      )
      .subscribe(item => // logic);
    }
    
    public ngOnDestroy(): void {
      this._destroyed$.next();
      this._destroyed$.complete();
    }
  2. If you need to subscribe to an observable only once, use take(1). 📄 Note that you will need to use takeUntil to avoid any memory leaks caused when the subscription hasn’t received a value before the component got destroyed.

    sampleObservable$
    .pipe(
      take(1),
      takeUntil(this._destroy$)
    )
    .subscribe(item => // logic);
  3. Using unsubscribe() method of Subscription. Using this method, you have to add() your observables (if you have multiple 🚋) to the Subscription and destroy on ngOnDestroy with the unsubscribe() method.

    private _subscriptions$ = new Subscription();
    
    public ngOnInit (): void {
      // we can add multiple observables to _subscriptions$ with add()
      this._subscriptions$.add(
        sampleObservable1$
        .subscribe(item => // logic)
      );
    }
    
    public ngOnDestroy (): void {
      this._subscriptions$.unsubscribe();
    }

💡 Tip : Make use of lint rules to detect any unsubscribed observables. Check out rxjs-tslint for TSLint rules targeting RxJS.


🎁 Resources : Check out 6 Ways to Unsubscribe from Observables in Angular by Chidume Nnamdi

5. Use async pipe

Click to expand

In the previous section, we learnt about unsubscribing observables. But there is a much more efficient way for the same problem, the magical async pipe 💫. It's recommended to avoid subscribing to observables from components and instead subscribe to the observables from the template.

Why?

Because subscriptions can lead to memory leaks if not properly unsubscribed and it's an additional overhead to do so 😩.

Solution:

Fear not! Angular have a magic potion up the sleeves just for automatically managing subscriptions - async pipe 🔮. Yes, it magically handles the subscriptions for us; no more memory leaks by forgetting to unsubscribe observables 😏. Use it whenever it's possible to.

<p>{{ someObservable$ | async }}</p>

<p>{{ (someOtherObservable$ | async).value }}</p>

6. Lazy load modules

Click to expand

Lazy loading is said to be one of the most powerfull feature of Angular. It's recommended to lazy load your feature modules whenever it's possible to.

Why?

By default, webpack (the default bundler of Angular), bundles all your code into one large bundle. This will largely increase the initial rendering time since the browser has to downlaod that one single large file initially itself, leading to a bad user experience 😡.

Solution:

Angular offers a powefull solution to that problem. Split your code to separate bundles and load them on demand 💪. And we do that via 'lazy loading'. This will reduce the initial rendering time since the browser has to download only a small file that contains the code for just your initial page. The browser will be fed the other files on-demand, i.e, when the user needs it (by navigating to a certain page).

// Note that the below syntax is valid for Angular 8 and above.
{ path: 'home',  loadChildren: () => import('./home.module').then(module => module.HomeModule) }

📄 Note : Do not lazy load the default route, as the browser will have to download an extra lazy loaded chunk after downloading the main chunk and parse it. This will slow down the initial rendering time.

7. Use preloading strategy

Click to expand

Preloading strategy is a concept which can be used along with lazy loading to make our application much more faster. You can use Angular's default PreloadAllModules strategy, or you can even write your own cutom strategies depending upon the requirements 🚚.

Why

When we lazy load a module, there is a possible latency since the browser loads the lazy loaded chunk and parse it only when we navigate to that module. This will lead to a bad user experience since the user might be required to wait for sometime until the page gets loaded 🚧.

Solution:

Use preloading strategy to load lazy loaded modules in the background after all the eager loaded modules are ready. This eliminates the possible latency when navigating to a lazy loaded module, but still has the benefit of faster initial loading of the app because the initial module(s) get loaded first. Such a cool feature ❄️! We can do that in various ways:

  1. PreloadAllModules : This is the default preloading strategy of Angular. This will load all the lazy loaded modules in the background once all the eager loaded modules are ready.

    imports: [
      ...
      RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
    ],
  2. Custom strategy : We may not always want to preload all the modules. Some modules might be a less used module which we don't wan't to preload. Or you may want to preload the modules based on some conditions, say, by checking user's bandwidth. We can write a custom preloading strategy for that 😻.

    import { Observable } from "rxjs/Observable";
    import { PreloadingStrategy, Route } from "@angular/router";
    
    export class CustomPreloadStrategy implements PreloadingStrategy {
      preload(route: Route, preload: Function): Observable<any> {
        if (someCondition) {
          return preload();
        }
        return Observable.of(null);
      }
    }

    And in the AppModule:

    imports: [
      ...
      RouterModule.forRoot(routes, { preloadingStrategy: CustomPreloadStrategy })
    ],

💡 Tips : It's highly recommended to use ngx-quicklink by Minko Gechev which automatically downloads the lazy-loaded modules associated with all the visible links on the screen using Intersection Observer. It also checks if the user isn't on a slow connection before preloading 🔥.
Or, make use of machine learning by predictively preloading modules with Guess.js 🔮.


🎁 Resources :
1. Check out Preloading in Angular on alligator.io.
2. Check out Route preloading strategies in Angular by Minko Gechev.

8. Lazy load components

📄 Note : This feature is only supported in Angular 9+.

Click to expand

Yes, it's also possible to lazy load components or modules without a route with Angular's Ivy engine 🔥. We can use this awesome feature to reduce the load/rendering time again!

Why?

Sometimes, you might have some components that are rarely used, say, an info popup that the users use very rarely. Pre loading the code for such components doesn't make any sense. If we can remove such components and load only those components that are necessary for the initial rendering, there will be a considerable gain in the rendering speed ⚡.

Solution:

Let's check out how to lazy load components through the below steps :

  1. First step is to create a container element for the component to be rendered in and get hold of that container in component using @ViewChild.

    <ng-container #someContainer></ng-container>

    In your component:

    @ViewChild('someContainer', {read: ViewContainerRef}) someContainer: ViewContainerRef;
  2. Use ComponentFactoryResolver and Injector to create an instance of the component.

    constructor(
      private injector: Injector,
      private cfr: ComponentFactoryResolver
    ) {}
  3. Use async-await to lazy load the component.

    async lazyLoadComponent() {
      const { LazyComponent } = await import('./lazy.component');
      const componentFactory = this.cfr.resolveComponentFactory(LazyComponent);
      const { instance } = this.someContainer.createComponent(componentFactory, null, this.injector);
    
      // Use the instance to pass down values to LazyComponent like this:
      instance.someProperty = 'dummy property';
    
      // Get hold of emitted values from LazyComponent:
      instance.someEmitter.pipe(
        takeUntil(instance.destroy$)
      ).subscribe(() => // Do something);
    }
  4. Sometimes you will need to import other modules (like SharedModule) to include some features in your component. We can create a dedicated module for the lazy loaded component and add it to the lazy.component.ts file.

    @NgModule({
      declarations: [LazyComponent],
      imports: [CommonModule, SharedModule],
    })
    
    // Remove export statement to avoid accidental imports in other files.
    class LazyCompModule {}

🎁 Resources :
1. Check out Angular 9: Lazy Loading Components by John Papa.
2. Check out Lazy load Angular components by Kevin Kreuzer.

9. Use ChangeDetectionStrategy.OnPush

Click to expand

Angular gives us an option to choose the ChangeDetectionStrategy of a component. By default, the value is Default. It's recommended to change that to OnPush strategy to maximise the performance 👌.

Why?

By default, Angular run its change detection cycle on all the components whenever there occurs some changes, like a simple click event or when we recieve data from ajax calls. Running change detection cycle on every such events are costly and may affect the performance when the app grows.

Solution:

We can minimize these checks by setting our component's changeDetection to ChangeDetectionStrategy.OnPush. This will tell Angular to run change detection cycle only when:

  1. The Input reference changes.
  2. Some event occurs in the component or any of the children.
@Component({
  selector: 'app-selector',
  ...
  changeDetection: ChangeDetectionStrategy.OnPush
})

📄 Note : Make use of detectChanges() or markForCheck() functions of ChangeDetectorRef to explicitely run the change detection cycle if required.


🎁 Resources : Check out 🚀 A Comprehensive Guide to Angular onPush Change Detection Strategy by Netanel Basal.

10. Disable change detection

Click to expand

It is recommended to detach and reattach the change detector whenever required, for components where data changes happen frequently 🔧.

Why?

Running change detection cycle frequently when data changes occur constantly is very costly and can lead to performance issues 🔻.

Solution:

Use the detach() method of ChangeDetectorRef to detach the change detector so that Angular won't run any change detection cycle unless you explicitely says to, by calling reattach() or detectChanges(). The reattach() method can be used to re attacch the change detector when some frequent complex calculations have been finished. The detectChanges() can be used to detect the changes once, whenever required.

constructor(private cdRef: ChangeDetectorRef) {
  cdRef.detach();

  // to check and update view every 5sec for a frequently changing component
  // or use this.cdRef.reattach() when frequent complex calculations have been finished
  setInterval(() => {
    this.cdRef.detectChanges();
  }, 5000);
}

🎁 Resources : Check out Everything you need to know about change detection in Angular by Max Koretskyi.

11. Run outside angular

Click to expand

Angular make use of NgZone, which is a wrapper for Zone APIs, to detect when to run change detection cycle. Zones wrap asynchronous browser APIs, and notifies when an asynchronous task has started or ended. Angular takes advantage of these APIs to get notified when any asynchronous task like xhr calls or any user events is done to run it's change detection mechanism 👼. So it is recommended to use the runOutsideAngular method of the NgZone instance to minimize running change detection cycles.

Why?

For the same reason in the above section - running change detection cycle frequently when data changes occur constantly is very costly and can lead to performance issues 🔻.

Solution:

We can run complex functions that don't necessarily require a change detection cycle to run (inorder to reflect something in the view) inside the callback of runOutsideAngular().

constructor(private zone: NgZone) {
}

someComplexFunction() {
  this.zone.runOutsideAngular(() => {
    // your complex logic
  });
}

📄 Note : Make use of run() method to explicitely run the change detection cycle if required.


🎁 Resources : Check out Using Zones in Angular for better performance by Pascal Precht.

Other performance optimisation techniques

Click to expand

1. Add API caching mechanisms

While most of the time it is, not all the API responses are dynamic. Sometimes some of the responses do not change often. We can store those values from the API by adding a caching mechanism and return that when subsequent calls to that APIs are made. In this way we can reduce the ajax calls and significantly improve the speed of our application as we don't have to wait for the network.


💡 Tip : Use cashew 🐿, an awesome, flexible library that caches HTTP requests in Angular. Caching is nut a problem anymore 😉!


2. Use service workers

Make use of service workers to cache our static assets(images, icons and fonts) and build artifacts(JS and CSS bundles). This can significantly improve the rendering time and make our application lightning fast ⚡ by reducing the number of network requests needed. You can even make your application work when offline serving from the saved cache 🍻!


We can make our application a Progressive Web App (PWA), by adding @angular/pwa package. Play with the generated ngsw-config.json to to define what and how to cache. With this feature, we can add our web application to our mobile phone's home screen and make our web app to look and feel like a native mobile application by adding application icons, splash screens etc. We can even release the application in Google Playstore just like any mobile application with Trusted Web Activity 😱 😱!


🎁 Resources :
1. Check out Getting started with service workers in the official Angular docs.
2. Check out Angular Service Worker - Step-By-Step Guide for turning your Application into a PWA on Angular University.
3. Check out How to build Progressive Web Apps with Angular. by Eniola Lucas.


3. App Shell

We can improve the user experience by quickly launching a static rendered page (a skeleton common to all pages) while the browser downloads the full client version and switches to it automatically after the code loads. This will significantly reduce the time for the first paint since the browser just need to render HTML and CSS without the need for initialize any Javascript. Such a cool feature 😯!


We can make use of the Angular CLI to automatically generate an app shell for us with the command ng generate app-shell. Or thinking ahead, we can make use of Angular Universal to pre-render a static app shell 🌌. Do check out the resources to know more about it.


🎁 Resources :
1. Check out App shell in the official Angular docs.
2. Check out Angular App Shell - Boosting Application Startup Performance on Angular University.


4. Web Workers

By default, our code usually runs in a single thread. This leaves behind a problem of unresponsive ui if some computationally intensive tasks are being performed. But thanks to Angular's support for web workers, we can run such tasks in another thread in the background without blocking our main execution thread 🌞. In huge and complex applications, we can even go to an extend where we run our entire application (including change detection) in a Web Worker and leave the main UI thread responsible only for rendering.


🎁 Resources : Check out Web Workers in Angular in the official Angular docs.


5. Web Assembly

⚠️ Warning : Although most of the modern browsers support web assembly, it's still in a premature state and might be unstable. So use at your own risk 😁!

If someone wants to go the extra mile for performance, web assembly is for you! So what exactly is web assembly 😕? In a nutshell, it is a low-level assembly-like language with a size- and load-time-efficient binary format which can run in near native speed. Okay, so what has it got to do with Javascript 😶?

WebAssembly is designed to run alongside JavaScript — using the WebAssembly JavaScript APIs, you can load WebAssembly modules into a JavaScript app and share functionality between the two. Meaning, we can write our code in C/C++, Rust etc., compile it into a web assembly module and use it with our Angular application 💥. In short, we can run our computationally intensive tasks in web assembly and make full use of it's lightning speed ✨!


🎁 Resources :
1. Check out Web Assembly in MDN.
2. Check out Examples of how to use WebAssembly within Angular by Boyan Mihaylov.
3. Check out Using Web Assembly to speed up your Angular Application by Lukas Marx.


🎁 Resources :
1. Do check out this awesome repo : angular-performance-checklist by Minko Gechev and find out more pro performance tips 😍.
2. Check out Best practices for a clean and performant Angular application by Vamsi Vempati.
3. Check out Optimizing the Performance of Your Angular Application by Netanel Basal. 4. Check out 15 Angular Performance Tips & Tricks on The Angular Guru.


❤️ Takeaway :
1. Make use of pure pipes to avoid function calls in templates
2. Use trackBy with *ngFor to minimize DOM manipulations by tracking array items with a unique identifier.
3. Use providedIn attribute to make your services tree shakable.
4. Unsubscribe your observables to avoid memory leaks and use async pipe whenever possible to make Angular handle the subscriptions.
5. Lazy load modules and components to separate build bundles and load them on demand. Make use of preloading strategies to avoid possible latency while loading the lazy loaded chunk.
6. Use a combination of ChangeDetectionStrategy.OnPush, ChangeDetectorRef and NgZone methods to minimize the number of change detection cycles.
7. Use service workers, app shell and api caching to further improve the performance.

🔝 Back to Contents


Roadmap

See the open issues for a list of proposed features (and known issues).


Contributing

Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated.

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b docs/AmazingDoc)
  3. Commit your Changes (git commit -m 'Add some AmazingDoc')
  4. Push to the Branch (git push origin docs/AmazingDoc)
  5. Open a Pull Request

License

Distributed under the MIT License. See LICENSE for more information.