How to Lazy Load in Ionic Angular
Nothing strikes fear into the hearts of developers quite like being told their app is slow. Because of this, great pains are taken to optimize the loading and startup performance in our apps. The techniques we use have changed over the years, but the good news is that a lot of the heavy lifting is now done for us by our frameworks and build systems.
In this post, we will take a look at how lazy loading can be used to help speed up the load times of your Ionic Angular apps. Also, it doesn’t matter if your app is packaged and downloaded from the store, or a progressive web app (PWA) running off a server, lazy loading can help increase your startup times in both situations.
Why Optimize Loading Performance?
Performance is about letting users do what they need to do with your app as quickly and efficiently as possible. If your app is slow to load or unresponsive, then your customers will quickly leave and find an app that works better.
Over the past several years, web sites have started to become more app-like by offering better experiences and more functionality. As apps have become more complex, the amount of JavaScript needed to be downloaded has increased as well. JavaScript is the most expensive asset to download, so this dramatic increase in size now posed a new dilemma for our web apps.
To counter these larger bundle sizes, there are now a few additional techniques we can use to help our apps stay fast. One of these techniques is lazy loading, which breaks larger JavaScript bundles up into smaller chunks and delivers them to the browser as needed. Let’s see how we can begin to optimize our loading performance in an Ionic Angular app.
Note, it is known that premature optimization is the root of all evil. What we will go over are particular techniques you can use to see if it helps your apps, but these aren’t one-size-fits-all rules to throw at every project. Take the time to build out your app and then go over these options to see if they can point you in the right direction and are a good fit for your goals.
Lazy Loading in Ionic Angular
The idea behind lazy loading is that we only download the HTML, CSS, and JavaScript that our application needs to render its first route, and then load additional pieces of our application as needed. The great news is that a new Ionic Angular 4.0 app has lazy loading configured by default. Lazy loading is expressed through how the Angular routes are setup:
const routes: Routes = [
{
path: '',
loadChildren: './tabs/tabs.module#TabsPageModule'
}
];
This is the initial route that is created for you when starting a new Ionic app using the tabs starter template. By specifying a loadChildren
string (instead of passing a page class to component
), the Angular router will load this file dynamically when the user navigates to the route. This JavaScript also gets split off from the rest of the app into its own bundle.
Below, we have the routes setup in the tabs routing module:
const routes: Routes = [
{
path: 'tabs',
component: TabsPage,
children: [
{
path: 'tab1',
children: [
{
path: '',
loadChildren: '../tab1/tab1.module#Tab1PageModule'
}
]
},
/// tab 2, tab3...
}
]
Each tab in this configuration loads its children lazily as well, meaning all the files for tab2 are not loaded until the user navigates to the tab2 page.
By breaking apart our app into separate lazily loaded chunks, we make it so that the browser doesn’t need to download, parse, and compile our entire app before the user even interacts with the first page. If our app was of significant size, this would greatly increase the initial load time of the app.
Using lazy loading can help your app load fast, however, if a user navigates to a new page, the assets will still need to be downloaded before the user views the page. There can be a small delay while this download happens, and it could be several seconds if on a slow network, giving your app a sluggish feel.
But lazy loading was supposed to make our app fast! Well, it did, at least for the initial page load. Now, let’s take a look at how to pre-load additional routes so they are available when the user wants them.
Optimizing Lazy Loading
When importing the Router module in the main app module, you can specify a pre-loading strategy to use. There are two that come out of the box with Angular:
- NoPreloading: Does not perform any preloading of lazily loaded modules. This is the default behavior if no strategy is specified.
- PreloadAllModules: After your app loads the initial module, this strategy will preload all the rest of the modules when the network becomes idle. In the Ionic starter templates, we set this option for you automatically.
To specify which strategy to use, set the preloadingStrategy
parameter on the options object when setting up the router in app-routing.module.ts:
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
The preloadAllModules
strategy essentially loads the rest of the app in memory after the first route loads. Navigating between pages will now be quick, as all the routes and modules are loaded and ready to go.
While preloadAllModules
is a sensible default for most apps, if your app is large enough, loading the entire thing might not be the best option. You might waste user’s bandwidth (if your app is a PWA) by downloading screens they might not visit. Your initial load might also be slower using preloadAllModules
if your app is so large that it takes considerable time to parse and compile the JavaScript.
Fortunately, we can also define custom pre-loading strategies in Angular. Let’s take a look at how to set this up next.
A Simple Pre-Loading Strategy
We will start off by creating a simple strategy that we can use to specify which routes we want to pre-load up front. Using this strategy, we can ensure the most important parts of our app are primed and ready to be navigated to after the first screen loads.
To create this strategy, Angular provides an abstract class called PreloadingStrategy
that you can implement. This class has one method you need to override, preload
, which takes in the configured lazy route, and a parameterless function you can call to programmatically load the lazy route. It returns an observable of type any.
Note that only routes that are lazy (have the loadChildren property) are passed into Pre-loading strategies for processing.
To provide some determination on if we should pre-load a route or not, we can use the data property on the route definition to inform our strategy:
{
path: '',
loadChildren: '../tab2/tab2.module#Tab2PageModule',
data: {
preload: true
},
}
Now, let’s create a SimpleLoadingStrategy
class to pre-load all lazy routes that have this data attribute.
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
export class SimpleLoadingStrategy implements PreloadingStrategy {
preload(route: Route, load: Function): Observable<any> {
if (route.data && route.data.preload) {
return load();
}
return of(null);
}
}
To use this strategy, go into app-routing.module.ts and pass in the SimpleLoadingStrategy
in the options. Also, since the custom strategy is a service, pass it into the providers
collection as well:
@NgModule({
providers: [SimpleLoadingStrategy],
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: SimpleLoadingStrategy })
],
exports: [RouterModule]
})
Now, any routes with preload set to true will be pre-loaded and ready to use after the app initially loads.
This strategy was purposefully simple to show the basics of creating a custom pre-loading scheme. And while this basic strategy might work for some apps, it’s still not practical for a larger app with many routes and screens.
Now that we know how to create a pre-loading strategy, let’s take it one step further and create a more sophisticated one to meet a larger app’s needs.
A Selective Pre-Loading Strategy
An ideal pre-loading strategy might try to stay one step ahead of the user and pre-load any routes that the user might try to go to next. On a tabs interface, you could pre-load each of the tabs so they are fast and responsive when the users click between them. When a user visits a list screen, you could pre-load the details view so it’s ready to go.
With the above strategy in mind, we will create one that we can use to programmatically load routes from within our Ionic pages. This will give you the power to pre-load the appropriate pages you think the users will navigate to in the near term. We will also build this strategy in a way to keep our code clean and concise using TypeScript decorators. There is some setup to do, but the end result will be worth it.
First, let’s see how our route definitions will be set up. Instead of using a preload property on the route, we will simply give the route a name so we can reference it later. Setup each of your lazy loaded routes that you would like to pre-load like the following:
{
path: ':id',
loadChildren:
'../item-detail/item-detail.module#ItemDetailPageModule',
data: {
name: 'ItemDetail'
}
}
Our new pre-loading strategy will keep track of these routes, but instead of calling the load function immediately, we will store it in a dictionary so we can access it later:
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
export class SelectiveLoadingStrategy implements PreloadingStrategy {
routes: { [name: string]: { route: Route; load: Function } } = {};
preload(route: Route, load: Function): Observable<any> {
if (route.data && route.data.name) {
this.routes[route.data.name] = {
route,
load
};
}
return of(null);
}
preLoadRoute(name: string) {
const route = this.routes[name];
if (route) {
route.load();
}
}
}
The preLoadRoute
method takes in the name of the route, and (if it’s found in the dictionary) calls that route’s load method. This method is what will be called directly from our Ionic pages. PreloadingStrategies are just services, so they can be injected into our components like any other service:
constructor(private loader: SelectiveLoadingStrategy) {}
ngOnInit() {
this.loader.preLoadRoute('ItemDetail');
}
When the above list screen loads, it will start pre-loading the ItemDetail
screen.
We could end it there, but adding a bunch of loading code into our components complicates their logic and adds the loader dependency that isn’t very cohesive with the rest of the component. So, let’s clean up this logic by abstracting the loading code into a TypeScript decorator. Then we can add that decorator to each of the pages we want to use pre-loading on.
Create the PreLoad
decorator:
import { AppModule } from '../app.module';
import { SelectiveLoadingStrategy } from '../util/SelectiveLoadingStrategy';
export function PreLoad(page: string): ClassDecorator {
return function(constructor: any) {
const loader = AppModule.injector.get(SelectiveLoadingStrategy);
const ngOnInit = constructor.prototype.ngOnInit;
constructor.prototype.ngOnInit = function(...args) {
loader.preLoadRoute(page);
if (ngOnInit) {
ngOnInit.apply(this, args);
}
};
};
}
This is a class-based decorator that takes in the name of the route you want to load (as defined in the route config). The loader property calls into Angular’s injector service (which we grab from the main app module) to get an instance of SelectiveLoadingStrategy
. Then, we override the ngOnInit
method. In our new ngOnInit
, we load our route by calling loader.preLoadRoute
, and then call the original ngOnInit
method (if it exists).
In order to get access to the Injector service, we will expose that as a static property from app.module.ts:
export class AppModule {
static injector: Injector;
constructor(private injector: Injector) {
AppModule.injector = injector;
}
}
Now, all that’s needed to pre-load a route from another page is to include the PreLoad decorator like so:
@Component({
selector: 'app-item-list',
templateUrl: 'item-list.page.html',
styleUrls: ['item-list.page.scss']
})
@PreLoad('ItemDetail')
export class Tab2Page {
}
And, boom! No more loading code in our actual component classes.
Conclusion
As the web platform has matured over the past few years, many techniques have come along to help with performance. With more mobile devices being used today than ever before, it’s important to make sure your app is fast and responsive. When you start with a mobile-first mindset, you not only make sure your app performs well on these devices, but that it will perform even better on faster devices with better network connections.
In this post, we went over how to set up lazy loading in an Ionic Angular app, and how to use eager pre-loading to load additional views before the user needs them. Hope it was helpful!
While these techniques will take you a long way, there is still more that can be done to optimize your apps user experience. Interested in learning more? Head over to the docs or hit us up in the comments below or on twitter with your questions or suggestions.
Happy Coding!