How to use AngularJS lazy loading to improve your user experience

Posted by Andy Armstrong on 9/5/2017

Intro

Routing is a key component of any modern single page application and clearly is a key component of any AngularJS application. Routing gives some semantic separation of concerns for the user via the browser location bar and also determines the modules and components that are displayed to the user.

The downside of single page apps is that we must provide all the code used in the application on the first page load, which can be quite a heavy download.

Enter lazy loading, or only loading the modules and components we need when the user navigates to a route.

In this post we will explain why you want to use lazy loading, some of the basics and some more advanced concepts, and finally a code example to get you started.

We you should use lazy loading?

One of the downsides of SPA’s is the fact that the user “only loads the page once” and eschews reloading the page to provide an “Application-like” experience. The downside of this improved UX is that we must load all modules and components on the first load and this can take some time.

With lazy loading we only load the required modules on the first load to improve the user experience. Sending less code means the user sees the application sooner, which means happier users.

Lazy loading basics

Lazy loading is performed by allowing the router config to provide the module to load on route execution. This of course relies on you splitting your application into modules. Of course, feel free to check out our previous post on creating exportable feature modules in AngularJS 2.

To utilize lazy loading you must split your code into modules (you should be doing this already anyway).

Let’s take a look at the code base tree below as an example:


src/app/
├── app.module.ts
├── app.component.ts
├── app-routing
│   └── app-routing.module.ts
├── about
│   ├── about.module.ts
│   ├── form
│   │   └── form.component.ts
│   └── routing
│       └── routing.module.ts
├── contact
│   ├── contact.module.ts
│   ├── form
│   │   └── form.component.ts
│   └── routing
│       └── routing.module.ts
└── home
    ├── home.component.html
    └── home.component.ts

This example is probably very familiar; we have a simple application module with a basic routing module & route config. Simple and one-to-one with our components and routes.

To enable lazy loading, we need to update the routing config to indicate that some modules will be loaded on route execution:


import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent }    from '../home/home.component'

const routes: Routes = [
    {
        path: '',
        pathMatch: 'full',
        component: HomeComponent,
    },
    ...
    {
        path: 'about',
        loadChildren: 'app/about/about.module#AboutModule'
    }
];

export const AppRouting: ModuleWithProviders = RouterModule.forRoot(routes);

In the example above we see that the routing config has some additional settings.

As you can see, enabling lazy loading is trivial.

Pre-Loading Modules

Pre-loading modules allows us to load some of our lazy-loaded modules as the user is using the application. This results in a better user experience in which the modules are available ‘instantly’ to the user and gives us all the advantages of lazy loading.


@NgModule({
  imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })], // Define Preloading Strategies
  exports: [RouterModule],
  providers: [AppCustomPreloader]
})
export class AppRoutingModule { }

pre-loading of modules can be done after the user has loaded the application, but while they click around. This can greatly improve the UX, but you still have some logical separation of the code into modules… and they are loaded after the initial module has been loaded.

Here we will show a very simple example where we use the provided PreloadAllModules class from Angular:


import { PreloadAllModules } from '@angular/router';

...

export const AppRouting: ModuleWithProviders = RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules });

If you are interested in creating a custom loader, for example one that only pre-loads ContactModule, check out this post on custom pre-loaders from Victor Savkin.

Example

As an example, we have an angular application with four routes: Home, List, About and it’s child Contact. Home is our default route, and will be loaded automatically. List will be lazy loaded when the user requests this link. About will have a child route, Contact, that is pre-loaded when the user goes to “about”.

Example App Module


...
import { AppRouting }           from './app-routing/app-routing.module';

import { AppComponent }         from './app.component';

// Import the home component as it exists in this module
import { HomeComponent }        from './home/home.component';

@NgModule({
  declarations: [
    HomeComponent
  ],
  imports: [
    ...
    AppRouting
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Example App Routing Module


// Import home component from this module
import { HomeComponent }    from '../home/home.component'

// Route defintions
const routes: Routes = [
    {
        path: '',
        pathMatch: 'full',
        component: HomeComponent,
    },

    // Lazy load the Contact module
    {
        path: 'contact',
        loadChildren: 'app/contact/contact.module#ContactModule'
    },

    // Lazy load the About Module
    {
        path: 'about',
        loadChildren: 'app/about/about.module#AboutModule'
    }
];

// Export this module, for the root module
export const AppRouting: ModuleWithProviders = RouterModule.forRoot(routes);

About Module


// Import the form component from this module
import { FormComponent }    from './form/form.component';

// Import the routing module from *this* module
import { Routing }          from './routing/routing.module';

@NgModule({
  imports: [
    CommonModule,
    Routing
  ],
  declarations: [FormComponent]
})
export class AboutModule { }

About Module Routing


// Import the routing module from *this* module
import { FormComponent }        from '../form/form.component';

// Define the routes for this module
const routes: Routes = [
    {
        path: '',
        component: FormComponent,
    }
];

export const Routing: ModuleWithProviders = RouterModule.forChild(routes);

Here is a link to the example code from this post if you would like to take a look yourself.

Lazy Loading and AOT

A lot of AngularJS end users also use Webpack and AOT compiling to create a compressed and trimmed down application, and you can even split your application into some module chunks to further decrease the initial size of your application.

This excellent blog post by Damien Bod will give you some more details on how to make webpack aware of your modules, and even provides a full example on how to update your local code.

Conclusion

Lazy loading is a great way to make your SPA lean and mean with faster load times with on a few small changes. These better user experiences will make all the difference for your end users!