Back to Blog Home
← all posts

Optimizing app loading time with Angular Lazy Loading

December 2, 2016 — by Stanimira Vlaeva

When you develop mobile applications you should always be alert on the performance and always optimize. In this blog post I will walk you through a very effective pattern that describes optimizing  the loading time of an application when using Angular. Meet "Angular Lazy Loading".

You may now watch the full Angular/Vue.js code sharing webinar from June 2018 on YouTube!
Developing mobile apps with Angular may result in an application file that is too large and affects the startup time. Luckily, the Angular router provides a great feature called lazy loading to help us decrease the initial loading time.

What is lazy loading?

With lazy loading we can split our application to feature modules and load them on-demand. The main benefit is that initially we can only load what the user expects to see at the start screen. The rest of the modules are only loaded when the user navigates to their routes.

Check out this Victor Savkin's blog post for more in-depth information about lazy loading.

Using lazy loading with NativeScript

The Angular built-in module loader uses SystemJS. However, when developing NativeScript apps we don't have to set up SystemJS to create lazy loaded modules. We can use the NativeScript module loader, distributed with NativeScript Angular (the recommended way), write our own module loader or not use a module loader for lazy loading at all.

In the following sections we will use the simple lazyNinjas app which has two modules - the HomeModule (not lazy loaded) and the NinjasModule (lazy loaded). We'll cover the different methods for enabling lazy loading in the next sections. Take a minute to make yourself familiar with the ninjas - download the app from github and run it.

Providing the NativeScript module loader

We can replace the default application module loader by providing another in our root NgModule. For our NativeScript applications, we recommend using the NSModuleFactoryLoader:

Now, let's take a look at our router configuration:

The `routes` array is our app's actual router configuration.
First, we are adding the HomeModule routes using the ES2015 spread operator ('...').

...homeRoutes,

Then, we register the lazy loaded module. When the user navigates to '/ninjas', Angular will load NinjasModule located in './ninjas/ninjas.module'.

 
   {
        path: "ninjas",
        loadChildren: "./ninjas/ninjas.module#NinjasModule",
   }

In the file "./ninjas/ninjas.module.ts", we have the class definition for our NinjasModule, along with its routes. We import the routes using the `forChild` method of the `NativeScriptRouterModule`. Here's how it looks like:

That's all! We recommend using this method for lazy loading when working with NativeScript applications.
If we want to have nested lazy modules, we should provide the module loader in all NgModules that have lazy loadable routes. For reference, check out the nested-lazy-modules branch in the lazyNinjas repository.
In the following sections, we'll go over two other ways that can be used in more complicated scenarios.

Providing a callback to the `loadChildren` property

Remember the loadChildren method from our router configuration? No? Here's it again:

 
const routes = [
        ...homeRoutes,
        {
              path: "ninjas",
              loadChildren: "./ninjas/ninjas.module#NinjasModule"
        }
];

We use it to specify the path to the NinjasModule. Internally, the module loader takes that string and uses it to load module from the file. However, we can pass a callback instead of a string. For example:

loadChildren: () => require("./ninjas/ninjas.module.js")["NinjasModule"]

Let's see what happens step by step.

First of all, in the "./ninjas/ninjas.module.ts" file we exported our NgModule. This is transpiled to the following JavaScript code:

That allows us to require the module object (exports) as we do in the callback, passed to the `loadChildren` property, and query for the "NinjasModule".

loadChildren: () => require("./ninjas/ninjas.module.js")["NinjasModule"]

We can also omit the extension of the file, and write the above as:

loadChildren: () => require("./ninjas/ninjas.module")["NinjasModule"]

You can find that version of the app in the callback-loading branch.

Providing a custom module loader

Instead of passing a callback to every `loadChildren` property, we can extract the loading logic to a module loader. That's pretty much why we recommend using NativeScript module loader. But we can also create our own loader and use it instead.

Let's take a look at the NinjaModuleLoader used in our simple app:

The loader implements the `NgModuleFactoryLoader`, which has a single method - `load` with one parameter - the path, which we'll pass to the `loadChildren` property. We'll adopt the same syntax that the built-in module loader uses. As stated in the Angular documentation, "lazy loaded module location is a string, not a type... the string identifies both the module file and the module class, the latter separated from the former by a #."

We should change all `loadChildren` properties in our router configuration. Luckily, we have only one:

loadChildren: "./ninjas/ninjas.module#NinjasModule"

The private `splitPath` method of the NinjasModuleLoader is where we extract the module file location and the exported module name. Then we can simply require the module in the same way as in our callback function:

let loadedModule = require(modulePath)[exportName];

Extracting the loading logic allows us to check if the module exists and throw a meaningful error message if it doesn't.

if (!loadedModule) {
   throw new Error(`Cannot find "${exportName}" in "${modulePath}"`);
}
If the module was successfully required, we compile asynchronously it with the Angular compiler:
return this.compiler.compileModuleAsync(loadedModule);

Finally, we need to register our module loader in the root module*.

You can find the final version of the app in the custom-module-loader branch.

* Note that you can have a different module loader for every NgModule!

Benefits from lazy loading

For a real-life NativeScript app with lazy loaded modules, you can check out our SDK Examples. It has more than 100 different components, each with its own route, and ~15 feature modules. The following table presents the startup times with and without lazy loading.

Platform

Device

No lazy loading

With lazy loading

Android

Nexus 5

~13s

~4s

iOS

iPhone 5s

~12s

~3s


For our 2.5 release we are working to enable Ahead of time compilation and Webpack 2 with treeshaking and obfuscation, which will also improve the loading time significantly. Expect an update on this in the following weeks.