DEV Community

Cover image for How to make your app indefinitely lazy – Part 1: Why lazy loading is important
Aleksandrovich Dmitrii
Aleksandrovich Dmitrii

Posted on

How to make your app indefinitely lazy – Part 1: Why lazy loading is important

Lazy loading is a principle which should be known by most front-end developers. However, it is a more complicated topic than it is usually perceived to be. Therefore, even if you do use lazy loading, you might still have some gaps in your knowledge. And if you feel you know it all, it might be a good idea to freshen your grasp on this topic.

⏱️ Reading time: ~12-15 minutes
🎓 Level: Beginner+

Series Contents:

  1. How to make your app indefinitely lazy – Part 1: Why lazy loading is important
  2. How to make your app indefinitely lazy – Part 2: Dependency Graphs
  3. How to make your app indefinitely lazy – Part 3: Vendors and Cache
  4. How to make your app indefinitely lazy – Part 4: Preload in Advance

In this article, we'll cover the fundamentals of lazy loading:

  • What is it and why is it important;
  • How can we utilize lazy loading in our projects;
  • As well as what parts of the code should be loaded lazily.

This article covers only the basics, but starting from part 2 we will cover more complicated topics:

  • How bundlers (i.e. Webpack) analyze source code files, build dependency graphs and generate files for the assembly.
  • How we can decrease the size and the number of downloaded files by correctly setting up the structure of files and correctly using static imports.
  • How we should download vendor files and set up Webpack's cache groups.
    • As well as how lazy loading affects caching and how to make our projects as cacheable as possible.
  • How we can utilize prefetching/preloading strategies, including:
    • What Webpack magic comments can we use;
    • What is speculative or manual prefetching and how to use it;
    • And what third-party or our own solutions can be used for that.
  • And how can we request data from server without waiting for our static files to be downloaded.

Brace yourself, because you are about to become a real pro. But make sure you read all the articles 😅


Why Lazy Loading Matters

Let's commence. What is lazy loading and why is it important at all?

Imagine a large web application: the bigger it gets, the longer it takes for a browser to download all the static files. And the longer your app is being downloaded, the more annoyed your users may be. As front-end developers, we are responsible for making our users as little annoyed as possible. At least when it comes to performance.

"Lazy loading" could be the most basic strategy we can apply to make our application load faster if we consider FE-only mechanisms. The core idea of it is simple: if you don't need to use a piece of code immediately, you don't need to load it. And I really like this definition, but we will revisit it later. For now, let's take a look at the simplest example of applying lazy loading in an application.

Load your pages lazily

Consider, we have a simple application with 3 pages. The entire app is compiled into a single JavaScript file. Here's the code:

import { HashRouter, Routes, Route, Link } from 'react-router-dom';
import React, { Suspense } from 'react';

import Chapter1 from './pages/chapter-1/Chapter1';
import Chapter2 from './pages/chapter-2/Chapter2';
import Title from './pages/title/Title';

export const App = () => (
  <HashRouter>
    <nav className="navigation">
      <ul>
        <li><Link to="/">Title</Link></li>
        <li><Link to="/chapter-1">Chapter 1</Link></li>
        <li><Link to="/chapter-2">Chapter 2</Link></li>
      </ul>
    </nav>

    <Suspense fallback="Loading...">
      <div className="book-grid">
        <Routes>
          <Route path="/" element={<Title />} />
          <Route path="/chapter-1" element={<Chapter1 />} />
          <Route path="/chapter-2" element={<Chapter2 />} />
        </Routes>
      </div>
    </Suspense>
  </HashRouter>
);
Enter fullscreen mode Exit fullscreen mode

When we open such a website, it can take about 580ms to download the only JavaScript file our application has.

.

We'd like to improve the loading time, but how can we achieve it? We can start loading pages lazily. Meaning that unless users want to see a certain page, they will not download its code. And this can be achieved quite easily. Instead of importing pages "Chapter1" and "Chapter2" statically, we should import them using dynamic imports. Plus, since we use React, we also must wrap those imports into React.lazy HOC:

import { HashRouter, Routes, Route, Link } from 'react-router-dom';
import React, { Suspense } from 'react';
import Title from './pages/title/Title';

const Chapter2 = React.lazy(
  () => import(/* webpackChunkName: "Chapter2" */ './pages/chapter-2/Chapter2')
);

const Chapter1 = React.lazy(
  () => import(/* webpackChunkName: "Chapter1" */ './pages/chapter-1/Chapter1')
);

export const App = () => (
  <HashRouter>
    <nav className="navigation">
      <ul>
        <li><Link to="/">Title</Link></li>
        <li><Link to="/chapter-1">Chapter 1</Link></li>
        <li><Link to="/chapter-2">Chapter 2</Link></li>
      </ul>
    </nav>

    <Suspense fallback="Loading main...">
      <div className="book-grid">
        <Routes>
          <Route path="/" element={<Title />} />
          <Route path="/chapter-1" element={<Chapter1 />} />
          <Route path="/chapter-2" element={<Chapter2 />} />
        </Routes>
      </div>
    </Suspense>
  </HashRouter>
);
Enter fullscreen mode Exit fullscreen mode

And, voilà - now all of a sudden, it only takes 270ms to download the app initially. We managed to halve the time.

.

The sole purpose of lazy loading is postponing the load of our files only when they are needed by requesting them only when they are actually needed. In the case above, the code of pages Chapter 1 and Chapter 2 will be downloaded only when these pages are open.

And one of the main benefits of using lazy loading is that it allows us to decrease the initial loading time of our application. And there are a couple of reasons on why we should care about "initial" loading time a lot:

  1. It is the most important delay in the user's perspective. Until we download initial files, users are unable to interact with our website. And if we are talking about client-rendered applications, users won't even be able to see any content until we download initial files.
  2. Initially loaded files lose cache more often than other files and therefore caching optimizations are not as efficient for them as for other files. We will cover the root cause of this problem in Part 3

However, this mechanism helps with overall loading time as well. If we want to open a certain page from the very beginning, let's say Chapter 1, it still takes less time (~470ms) than it was before applying lazy loading (~580ms).

.

Therefore, to simplify, we could say that lazy loading is a win-win optimization approach that can optimize loading time and improve UX of a web application.

To give you a perspective of how useful lazy loading can be in real applications, I'll tell you about my real project I currently work on. My application is designed to display a lot of diverse data: it has tables, charts, graphs, video players, and even code editors. Therefore, it uses quite a few UI and other NPM libraries. If I sum the size of all the generated files of the assembly, the total size of my project will exceed 22 MB. Without any lazy loading, users would need to download all 22 MB initially. But if we start loading pages lazily, users will need to download 0.5-6 MB of files only to display a single page. In my case, loading pages lazily led to 77-97% improvement of how fast static files are downloaded.

And that's it. The concept is indeed quite simple, but the devil is in the details. During this series we will dive much deeper. But for now we are ready to form the first rule on our journey to make an app indefinitely lazy:

📌 In most cases, loading your pages lazily must be the default approach. Regardless of how many pages there are in a project, or how large these pages are.


Lazy loading is not only about pages

However, if we are only using lazy loading for pages, like in the example above, we're not getting the full benefits of this mechanism. We certainly can do better. Let's repeat the definition once again.

📝 If you don't need to use a piece of code immediately, you don't need to load it.

A page is indeed a good example of a React component, which isn't required to be displayed immediately. But what else can be loaded lazily? The answer is simple: almost anything!

Other components also can be loaded lazily. Modal windows, side sheets, tab sections, or pop-ups. We don't need to display them straight away in most cases. And what do we do when we don't need to use the code immediately? We don't download it. The concept of the ideal lazy loading implementation is straightforward: unless a component is tiny and trivial, it should be loaded lazily.

There are also several reasons to use lazy loading beyond just pages:

  • Each of these components may have unique logic. A modal window or a side sheet may have dozens of unique form field validations. A tab section might be as contentful as a full-fledged page. And they all might contain built-in data-URI or SVG images, their own styles, their own utility methods, and sometimes they may even have unique NPM dependencies.
  • Another problem is that there can be a lot of such components: dozens and sometimes hundreds. And sometimes they might be nested:
    • a page may have tab sections, and one of them may contain its own tab sections, and so on.
    • a modal window can be a wizard, or simply may be able to open another modal window, that can also open another modal window, and so on.

You get the idea. Of course, not all application has that level of component nesting. But the number of such components is also a problem. Sure, a single modal window may not affect performance that much. But dozens of non-lazy components can quickly add up.

To give you a perspective of a real application, I tell about my 2 last projects:

  1. In my previous company, there wasn't much nesting, but about 1/5 of size of initial files was dedicated to displaying modal windows. And there wasn't any reason to do so from a business perspective. By starting to load them lazily, we managed to get a 20% improvement for the initial loading time.
  2. However, in my current project, the level of nesting is crazy. Literally dozens of possibly lazy components can be concealed on a single page. One of the most visited pages of this app was 3.5 MB in size. And when I started to load all such components lazily, this page reduced its size to 1.1 MB, which is a 68% improvement.

But again, we can do even better. The good part about lazy loading is that we are not limited with loading lazily components only. We can also load lazily utility methods, classes, and even NPM packages.

  • For example, in my project, if a user has the admin role, they are able to execute some logic from Dev Tools' Console to simplify testing. And this logic is downloaded only if the role is active.
  • Or, if only a certain button requires to use a function from an NPM library, I download this library only when the button is clicked.

So, if you ever thought that lazy loading was just about page loading, you could explore other options to improve your performance. And now, the rule:

📌 Lazy loading is not only about page loading. You can lazily load modal windows, side sheets, pop-ups, tab sections, and even utility methods and NPM libraries.


Conclusion

That's it for today. Thank you for joining me on our journey to make our web applications indefinitely lazy. If you have any questions feel free to ask them in comments.

And to summarize this article, let's list the rules we learned today:

  • 📌 In most cases, loading your pages lazily must be the default approach. Regardless of how many pages there are in a project, or how large these pages are.
  • 📌 However, lazy loading is not only about page loading. You can lazily load modal windows, side sheets, pop-ups, tab sections, and even utility methods and NPM libraries.

But it was just the beginning. You can read the next article to dive even deeper:
How to make your app indefinitely lazy – Part 2: Dependency Graphs

Here are my social links: LinkedIn Telegram GitHub. See you ✌️

Top comments (0)