Angular App Optimization - aakash14goplani/FullStack GitHub Wiki

Topics Covered
Lazy Loading
Implementing Lazy Loading
Pre-loading lazy-loaded code
Ahead of Time Compilation


Lazy Loading

  • Consider this application, we have three routes - our root route i.e. /, /products and /admin. Now every route is associated with a module, our app module and then our different feature modules where each module of course might have multiple components, multiple directives, pipes and so on that belong to it.

  • Now whenever we visit any page, we load everything! Wouldn't it make more sense to only load products and admin when we actually visit these pages, so that we only load the code that belongs to these areas of our application when we really need it.

  • That's the idea of lazy loading. With lazy loading, we initially only load our root route content, so only the app module code and the code of all the components that are registered there and we don't load the other modules and only when we visit another module, like the admin module, we load the admin module code and the code of all the components that belongs to that module.

  • The advantage of this is that initially, we download a smaller code bundle and we download more code when we need it but initially our app is able to start faster because it has to download and parse less code on the first visit of a certain route and that's the advantage.


Implementing Lazy Loading

  • For lazy loading to work, your feature module needs to bring its own routes i.e. it needs to bring its own route config via forChild().

    const appRoute: Routes = [
       { path: 'shopping-list', component: ShoppingListComponent },
       { path: '**', component: PageNotFoundComponent, data: {message: 'Please double check the URL entered'} }
    ];
    @NgModule({
       imports: [ RouterModule.forChild(appRoute) ],
       exports: [ RouterModule ]
    })
    export class ShoppingListRouting {}
    
  • In Feature routing module, I will change it to an empty path because now we need to add something to our app routing module for lazy loading to work.

    const appRoute: Routes = [
       { path: '', component: ShoppingListComponent },
       { path: '**', component: PageNotFoundComponent, data: {message: 'Please double check the URL entered'} }
    ];
    @NgModule({
       imports: [ RouterModule.forChild(appRoute) ],
       exports: [ RouterModule ]
    })
    export class ShoppingListRouting {}
    
  • Now here, In the app routing module. I add that path again here but now I don't add a component which I want to load but instead, you add loadChildren, that's a special property in a route config which Angular understands as please only load the code content or the module I'm going to point you at when anyone, when the user visits this path here.

    const recipesAppRoute: Routes = [
       { path: '', redirectTo: '/recipes', pathMatch: 'full' },
       {
          path: 'shopping-list',
          loadChildren: () => import('./shopping-list/shopping-list.module').then((mod) => mod.ShoppingListModule)
          /* Alternate Syntax (not recommended): 
          { path: 'shopping-list', loadChildren: './shopping-list/shopping-list.module#ShoppingListModule' } */
       },
       { path: '**', component: PageNotFoundComponent, data: {message: 'Please double check the URL entered'} }
    ];
    
  • The effect of that will be that now the code is split at that point and everything at this path i.e. entire module and everything that module uses, will be put into a separate code bundle which is then downloaded and parsed on demand as soon as a user visits this page but not sooner!

  • That is simply achieved by the Angular CLI essentially analyzing this when we build our app and it will then analyse the feature module and it will look at what we declare in here and so on and will put the code of all these things we declare here into that separate bundle which is now built separately, detached from our main bundle with which the app starts.

  • Important for this to have a real effect and save code is that you have these import statements only in the feature module and you don't have any old import statements too!.

  • What you import here is all bundled together into one code bundle. So in the feature module, in the end the build process will look at these imports which are required for this to work but it will look at these imports and whatever is included here will be included in the same bundle, so if you have another even unused import, then this part would be included into the app module code bundle even though you're not using it there.

  • Now in the feature routing module, it's important that you have an empty path because we have to keep in mind that we now change the route config to include the path here in the app routing module and it will load the feature module whenever we visit /shopping-list and therefore in the recipes module, we now already are at /shopping-list and hence here in the feature routing module, which is only kicking in once we do visit this module, we should start with / because again, we already are at /shopping-list.

  • In the app module, I'm still importing the feature module. Now the feature module is the module we're now trying to load lazily, so we should only have this route where we use loadChildren, we must not import feature module anymore, otherwise we're importing it both eagerly and lazily at the same time and that will cause an error.

    /* BEFORE */
    imports: [
       ...
       ShoppingListModule
    ],
    /* AFTER */
    imports: [
       ...
    ],
    
  • NOTE: Please note, that you need to ensure that in your tsconfig.json file, you use "module": "esnext", instead of "module": "es2015",


Pre-loading lazy-loaded code

  • Right now, we load code whenever we need it, so as soon as we visit /shopping-list, for example after logging in, we bring in the Shopping List module. Now the downside of that of course is that this is downloaded and parsed just when we need it, leading to a very tiny delay in our application.

  • We might not see that here because the module maybe small and the internet connection maybe fast! But the bigger the module and the slower the internet connection, the longer that delay will be and therefore, we can actually tell Angular to pre-load lazy loaded modules to avoid this delay.

  • For this, we simply go to our root router module, so here we reconfigure our root routes and you can pass a second argument to that. This is an object where you can configure that root router and there, you can set up a preloadingStrategy and you can set this equal to PreloadAllModules. The default is noPreloading.

    @NgModule({
       imports: [
          RouterModule.forRoot(recipesAppRoute, { preloadingStrategy: PreloadAllModules })
       ],
       exports: [ RouterModule ]
    })
    export class AppRoutingModule {}
    
  • With PreloadAllModules imported and set here as a preloading strategy, you're telling Angular generally we're using lazy loading, so it will not put all that code into one bundle, it will split it as we saw it but it will preload the bundles as soon as possible, so when we are on the auth page, it will already preload recipes and shopping list so that these code bundles are already available when we need them.

  • The advantage is that the initial download bundles still is kept small because there, that code is not included and therefore the initial loading phase is fast but then when the user is browsing the page and we have some idle time anyways, then we preload these additional code bundles to make sure that subsequent navigation requests are faster!

  • Now you can even build your own preloading strategies here where you could have more refined logic, for example controlling that only certain routes should be preloaded.


Ahead of Time Compilation

  • Ahead of Time Compilation is the optimization technique we should perform before we ship our app to production.

  • In our angular application we're of course writing some code in our templates that includes special angular syntax like ngIf, ngFor etc. which only angular understands.

  • That's not a one to one snapshot that's going to get rendered into the real DOM instead angular will pass our templates and then update the real DOM based on the instructions we placed in our templates so all our components and our directives are in the end translated to commands that will be used to alter the real DOM JavaScript.

  • We make use of compilers to translate our angular code to JavaScript code, one such example is ng serve which is the angular compiler that is included in the angular framework.

  • It's a crucial part of the angular framework and it's actually quite big. The Angular compiler itself is written in JavaScript that reads your templates and actually translates all the logic there into concrete instructions that update the real DOM.

  • This process is called just in time compilation because the angular templates are compiled just in time when the rendering occurs in your browser.

  • The obvious downside of this is that this compilation process takes time. The entire compiler is part of your angular application even though it has no purpose, it has nothing to do with your business logic other than bringing it onto the screen which arguably is important but still a bit annoying that it has to be part of your application.

  • The good thing is it doesn't have to be part of your application since the angular compiler is responsible for converting your template code into your template instructions into JavaScript commands.

  • The browser understands we can do that during development as part of the build process and that process is called ahead of time compilation because we compile the angular templates ahead of the execution time. Then the angler template compiler runs during the build process before the app is deployed and not in the browser now.

  • The process by default uses the Just-in-Time compiler because that's better for debugging and fast updating of the running application and there's nothing wrong with that during development. We want to have a good development experience with rich error messages and so on but as soon as we're preparing ourselves for production we want to optimize our code as much as possible and shrink it to a small of a bunch less possible. And we do that with a command ng build --prod.

  • This command takes your angular application and now does not spin up a development server but it actually builds your entire app into a few files which you can then deploy. It uses ahead of time compilation.

  • You don't need to configure anything special. That's the great thing. It works out of the box but it does it for you. It generates a new dist folder. This folder control contains a folder with your project name and then there you got multiple files which are basically which basically contain the content of your application so your code files, bundled and optimized and decreased in size as much as possible to be as small as possible.

Aot vs JIY