Building shiny lazy-loadable Angular libraries

Esheldoron
Yotpo Engineering
5 min readJun 16, 2022

--

User experience (UX) plays a major role in your website’s success, one key element of UX is loading times.

When writing a big library you should keep in mind that someone will have to load it to their browser, this could impact initial app loading times if not built correctly.

Ideally, you would want your users to only load the necessary things when they enter your site, and not overload them with files/code they won’t use — that means lazy-loading some of your library.

If you’ve ever wondered how to create a lazy-loadable library to boost your site’s performance, you came to the right place!

In Yotpo, UX is a guiding light and is taken into account every step of the way, from design to implementation. In addition, our RnD has been growing rapidly over the last several years, and we had to adjust accordingly.
One of the adjustments included writing shared UI libraries for others to benefit from. Those shared libraries could be simple components, like a button, or a complex component that has several modules.

In this blog post, I will focus on the latter, while explaining how we leveraged lazy-loading using ng-packagr configuration to enhance the UX.

Angular CLI uses a tool called ng-packagr to create packages from your compiled code that can be published to npm.

Library structure and how to break it into independent modules

In this example, I’ll be creating a User Management library.
The structure I’ll be presenting here is part of a monorepo powered by NX.

When creating a library you get the following default structure:

The index.ts file exports both modules to make them available for outside usage:

The index.ts is pointed at from the ng-package.json with the following configuration:

We published the library as an npm package, so it could later be consumed by different apps.

With this structure, the bundling process bundles everything into a single bundle, that looks like this:

This was problematic for us because it meant we couldn’t lazy-load parts of it.

That’s because our modules were declared in the index.ts file, and our bundler (webpack in our case) can’t break them apart. So they’re loaded right away when our library is imported.

So, how could we separate the bundles to achieve the ability to lazy-load the modules? Let me explain…

ng-packagr

By default, when using Angular CLI it uses a tool called ng-packagr (compiles and packages Angular libraries in Angular Package Format).

ng-packagr gives us the ability to split our lib into independent modules that can be lazy-loaded by the app.

Let’s examine the changes we were required to do to achieve this:

  1. Change the global index.ts file to several smaller public_api files
  • Note: public_api is a convention used by Angular component libraries

Each public_api file holds its relevant export statement.

user mgmt’s public_api:

user profile’s public_api:

With user mgmt’s public_api swapping our index.ts file (default entry point of our library — ng-package.json).

2. Create a package.json file for each module

each module contains only:

  • Note: by default ng-packagr will search for “public_api”. You can change that, and supply a different name and path here (which will be demonstrated later).

And now comes the best part, let’s bundle our library and take a closer look at the outcome:

Our library is now made of 2 independent bundles, each could be imported as a stand-alone package (don’t worry, we’ll get to that).

3. One last thing is to define a default route for the main component in your sub-modules.
That is needed to set the module’s default entry component because the consuming application will only import the module without “knowing” the module’s components.

In our example, we defined the following in the user profile’s module:

Library consumption by an app

In order for the library to be consumed by an app, go ahead and install the newly created npm package and setup the routing in the following manner:

Does the job, right?

Partially, it is not lazy-loaded yet!

Now, let’s see how we can leverage lazy-loading from our new node_module

Voila! We’ve now only loaded the user profile module when accessing its relevant page.

What about shared modules?

That could be enough if your modules are completely independent, but what happens when you need some shared service/component?

For example, if we want a shared Authentication service. We wouldn’t want each module to implement it, because that would create code duplications and make it harder to maintain.

So going back to our library, what happens when we have a component or service (or anything else) that we want to put into use in both modules (user-mgmt and user-profile)?

Sounds trivial right?

Let’s create another module and name it “shared” (shocking, I know).

So we get the following structure:

Now, let’s import it!

Going back to our example, we want the user profile module to import the shared module, we can achieve this by doing the following:

Simple right? However, when bundling the library we ran into the following error:

  • rootDir Default: The longest common path of all non-declaration input files

Thus, we couldn’t reference anything that was not under our “rootDir” or nodeModules.

Which actually meant we couldn’t reference the shared module because it wasn’t under the same common path.

Our solution was to extract all the public_api files to the library’s root level, and change the package.json for each — pointing to its relevant API, as such:

As you recall, the user-mgmt page is our library’s entry-point defined in the ng-package.json, so let’s change it accordingly:

Therefore, having a package.json file in the user-mgmt folder is unnecessary.

The other package.json files look like this (with their relative entry files):

After doing so, we were able to reuse anything we wanted and keep loading modules only when we actually needed them.

Wrapping up

There are lots of ways to improve the UX of your website. What I’ve demonstrated here is only a fraction of the huge epic which is UX.

Apart from lazy-loading, you could also utilize Preloading strategies to enhance UX, which I did not cover but you could find useful too.

--

--

Esheldoron
Yotpo Engineering

I’m a Full Stack developer, passionate about coding, and specialized in microservices. Father of 2, geek, and a sports addict