Understanding Lazy-Loading in Popular Frontend Frameworks

This post is 4 years old. (Or older!) Code samples may not work, screenshots may be missing and links could be broken. Although some of the content may be relevant please take it with a pinch of salt.

One of my favourite terms by far is "lazy-loading". Quite frankly, when a few years ago, I heard this term, it made me smile. In this article, we'll take a look at what this term means exactly for the three most used frontend frameworks today: Angular, React and Vue.js.

Eager vs Lazy

The term eager signifies the loading of every component that we have for a give application, therefore posing potential performance bottlenecks. Lazy-loading, on the other hand, makes sure that a given component gets loaded only when it's needed and not a moment before.

Now you could think, oh, this is great, the application would become snappier, it would load faster. However, if you have a module/component in your application that takes considerable time to execute and load, it later will still mean that it'll slow down the application. For this reason, you can utilise the preloading - effectively preloading a component in the background. This technique requires an article on its own; this article is not going to get into the details but merely introduce you to this concept towards the end.

About the sample projects

The sample applications created in all three frameworks are all very similar. Each of them shows the following two things:

How to load lazy-load a component (within a page)

How to lazy-load a component via routing

To effectively visualise lazy-loading, the demo application/component is going to calculate the 42nd Fibonacci number. Mathematical operations are considered to be blocking - meaning that our programme cannot progress further; it needs to return the result of the calculation. This strategy is only being used to mimic what happens if there exists a piece of code that takes a long time to execute, and what impact it has on the overall application usage experience.

Project on GitHub

To access the project please visit the following GitHub repository: https://github.com/tpiros/lazy-loading

Angular

Let's start our discussion with Angular because this frontend framework has a particularity when it comes to lazy-loading components. In fact, in Angular, the smallest logical unit that we can consider for lazy-loading via routing is a module because components always belong to modules.

The Fibonacci component

This is how our component looks like in Angular:

fibonacci: number;

ngOnInit(): void {
const fibonacci = num => {
if (num <= 1) return 1;
return fibonacci(num - 1) + fibonacci(num - 2);
};
this.fibonacci = fibonacci(42);
}

Since this is an individual component and it already belongs to the root Angular component, we can load it on a page.

Loading a component on a page

First let's take a look at how to load the component on a page. To achieve this we have the following added to app.component.html:

<div>
<p>I am the <strong>home</strong> component.</p>
<button (click)="showMe()"></button>
<div *ngIf="showFibonacci">
<app-fibonacci-one></app-fibonacci-one>
</div>
</div>

Along with the following TypeScript code:

export class AppComponent {
showFibonacci: Boolean = false;
showMe() {
this.showFibonacci = !this.showFibonacci;
}
}

What's really interesting in this situation is the fact that the Fibonacci component will only get loaded if showFibonacci is set to true. This means that just using ngIf can be utilised for lazy-loading. This is because Angular doesn't just show/hide the component in the DOM; it adds/removes it based on the specified condition.

Lazy-loading via routing

For discussing lazy-loading via routing in Angular, we have already established that there's a need to create a feature module.

Learn more about Angular feature modules: https://angular.io/guide/feature-modules

Creating a feature module in our app along with the second component can be achieved by using the Angular CLI: ng g m fibonacci && ng g c --module=fibonacci fibonacci.

Once we have the module created, we can assign a component to the module, and then we need to add it to the main routing module (app-routing.module.ts):

const routes: Routes = [
{
path: 'fibonacci',
loadChildren: () =>
import('./fibonacci/fibonacci.module').then((m) => m.FibonacciModule),
},
];

Note that we are using loadChildren() and import the module as part of the route definition. This means that the module will be loaded only when the appropriate route is active.

Compare the above code with the following:

import { FibonacciComponent } from './fibonacci/fibonacci.component';

const routes: Routes = [
{
path: 'fibonacci',
component: FibonacciComponent,
},
];

The above will eagerly load the FibonacciComponent. It will cause a significant delay in displaying the main page of the application. Why block the main page with an operation in a component that we are not even seeing/using?

Read more about lazy-loading in Angular: https://angular.io/guide/lazy-loading-ngmodules

Vue.js

Next up, let's take a look at how to achieve lazy-loading in Vue.js. Let's create a Vue.js app (using the Vue CLI) and add a new component. Take a look at how the component's <script> part would look like:

const fibonacci = (num) => {
if (num <= 1) return 1;
return fibonacci(num - 1) + fibonacci(num - 2);
};
const myFibonacci = fibonacci(42);

export default {
name: 'Fibonacci',
data: () => ({
fibonacci: 0,
}),
created() {
this.fibonacci = myFibonacci;
},
};

Note the reason why we need to do the calculation outside the export default {} block is that otherwise, we couldn't mimic a blocking operation. Naturally, Vue.js has both the mounted and method properties available for components, which would allow the code to be invoked only when the component is created.

Lazy-loading a single component

With Vue.js we can utilise v-if to add/remove an element from the DOM and therefore lazily load a component. However there's more work that we need to do when it comes to Vue.js vs Angular. Take a look at the following code:

<div v-if="showFibonacci">
<Fibonacci />
</div>
<script>
import Fibonacci from './Fibonacci.vue';
export default {
name: 'Home',
data: () => ({
showFibonacci: false,
}),
methods: {
showMe: function () {
this.showFibonacci = !this.showFibonacci;
},
},
components: {
Fibonacci,
},
};
</script>

This may seem like a logical way to do lazy-loading; however, upon opening the page, it becomes evident that the initial load time is really long. This is because the component is eagerly loaded regardless of the v-if condition. In other words, we are telling Vue to load all the components regardless of them being added to the DOM.

The load performance significantly changes if we make the following changes in the <script> element:

// import Fibonacci from './Fibonacci.vue';
export default {
name: 'Home',
data: () => ({
showFibonacci: false,
}),
methods: {
showMe: function () {
this.showFibonacci = !this.showFibonacci;
},
},
components: {
// Fibonacci
Fibonacci: () => import('./Fibonacci.vue'),
},
};

By adding the import statement, inline, as part of the component's property, we are enabling lazy-loading for the fibonacci component. Now refreshing the app will mean that the main page loads really fast.. When the Fibonacci component is displayed on the main page, only then, we have some delay.

Lazy-loading components via routing

Lazy-loading components in Vue.js follows a similar pattern of that we have discussed earlier. Check out the router:

// excerpt
import Home from '../views/Home.vue';
import Fibonacci from '../views/Fibonacci.vue';

Vue.use(VueRouter);

const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/fibonacci',
name: 'Fibonacci',
component: Fibonacci,
},
];

The above router is something that you may have used/seen before in a Vue.js app. Even though it is functional, with a blocking operation at hand operation, you can observe the issue. If we have a blocking operation in the Fibonacci component, why should that block the loading time of the Home component, which is exactly what's happening.

To fix this particular problem, we can resort to the previously seen pattern and import the component at the route definition:

import Home from '../views/Home.vue';
// import Fibonacci from '../views/Fibonacci.vue';

Vue.use(VueRouter);

const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/fibonacci',
name: 'Fibonacci',
component: () => import('../views/Fibonacci.vue'),
},
];

Now loading the main page will not be blocked and the fibonacci component is only loaded when the user selects that route.

Learn more about lazy-loading in Vue.js: https://router.vuejs.org/guide/advanced/lazy-loading.html

React

Last but not least, let's take a look at how to achieve lazy-loading using React. The application was created using the create-react-app CLI and similarly to the previous examples, we have a component with some blocking operation:

import React from 'react';

const fibonacci = (num) => {
if (num <= 1) return 1;
return fibonacci(num - 1) + fibonacci(num - 2);
};

const fib = fibonacci(42);

const Fibonacci = () => (
<>
<p>
Hello, this is the <strong>Fibonacci</strong> component. For fun I
calculated the 42nd Fibonacci number which is: {fib}.
</p>
</>
);

export default Fibonacci;

Lazy-loading a single component

By default, just like in the previous examples using different frameworks, importing components will mean eager loading:

import Fibonacci from './Fibonacci';
const Home = () => {
const [showFibonacci, setShowFibonacci] = useState(false);
const showMe = () => setShowFibonacci(!showFibonacci);
return (
<>
<p>
I am the <strong>home</strong> component.
</p>
<button onClick={showMe}>{showFibonacci ? 'Hide' : 'Show'}</button>

{showFibonacci ? <Fibonacci /> : ''}
</>
);
};

In the above example, even though the Fibonacci component is not being shown, loading the main page of the application still takes a lot of time. To fix this, we need to tell React to lazy-load the component in question. React provides a few helpers such as the Suspense component (for displaying a placeholder while the component is being loaded) and the lazy() method which load component lazily:

import React, { Suspense, useState, lazy } from 'react';
const Fibonacci = lazy(() => import('./Fibonacci'));
// ...
{
showFibonacci ? (
<Suspense fallback={<div>Loading...</div>}>
{' '}
<Fibonacci />
</Suspense>
) : (
''
);
}

Making these changes in the application will mean that the home component will load fast, and the Fibonacci component will only load when the user is asking for it.

Lazy-loading via routing

The same theory applies to lazy-loading a component via routing as well - including the usage of Suspense and lazy():

import Fibonacci from './components/Fibonacci';
<Router>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/fibonacci">Fibonacci</Link>
</li>
</ul>
</nav>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/fibonacci" component={Fibonacci} />
</Switch>
</Suspense>
</Router>;

Given the router above, in combination with the import statement means that the Fibonacci component will be loaded eagerly. Hopefully, by now, it's clear why this is not ideal. To enable lazy-loading for components via routing, we need to change the code to utilise the aforementioned Suspense component and lazy() method:

// import Fibonacci from './components/Fibonacci';
const Fibonacci = lazy(() => import('./components/Fibonacci'));

// ...
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/fibonacci" component={Fibonacci} />
</Switch>
</Suspense>;

Learn more about lazy-loading in React: https://reactjs.org/docs/code-splitting.html

Verify via DevTools

To see what's going on behind the scenes, we can utilise the browser's DevTools panel.

What is being discussed in this section is valid for all the frameworks displayed throughout the article.

Primarily we can verify that when our application is utilising eager loading, all the JavaScript gets loaded and executed by the browser. How is this visible in DevTools? Clicking on the Fibonacci link doesn't download additional JavaScript.

Updating the code to use lazy-loading will mean that less JavaScript is downloaded, to begin with. When the component gets loaded, a new JavaScript request appears - this is the chunk that we just requested.

Take a look at the screenshots below to see a before/after state, but I also recommend that you run these samples on your own and play around with DevTools.

One more thing

Of course lazy-loading a component doesn't solve one problem: the time of execution for the "problematic" module. In our case, the component is exaggerated of course since it's doing some heavy mathematical calculation but regardless of this, users can still visit the route and face performance issues. There are certain strategies to help overcome this issue. With all the frameworks, we can use magic comments via Webpack to dynamically add prefetch (or preload) via <link rel="prefetch" /> to a page. Just place the magic comments before the component's name, inside the import:

const Fibonacci = lazy(() =>
import(
/* webpackMode: "lazy" */
/* webpackPrefetch: true */
/* webpackPreload: true */
/* webpackChunkName: "fibonacci" */ './components/Fibonacci'
)
);

The above will add a <link rel="prefetch" as="script" href="/static/js/fibonacci.chunk.js"> element to the DOM.

Learn more about magic comments and the preload/prefetch options in Webpack here: https://webpack.js.org/guides/code-splitting/#prefetchingpreloading-modules.

Conclusion

Lazy-loading is a concept that allows us to select which component(s) to load later in an application to allow a better performance. This is a strategy that can be chosen over eager loading, where all components are loaded at the same time causing potential performance issues. The article highlighted how to load individual components as well as how to apply lazy-loading for components via routing in three frameworks: Angular, React and Vue.js.