DEV Community

Cover image for You might not be lazy loading properly in Angular. Pitfall of barrel files.
Justin
Justin

Posted on • Originally published at Medium

You might not be lazy loading properly in Angular. Pitfall of barrel files.

A common mistake in many codebases is the assumption that modules are being lazy loaded when, in fact, they are not. The culprit? Barrel files.

Barrel files are widely used, particularly in codebases that utilize NX. But what exactly are barrel files? There is a good chance you have already worked with them.

A barrel file re-exports modules from a folder, and it is conventionally named index.ts. For example:

// index.ts

export { TransactionsComponent } from './lib/transactions.component';
export { CardsComponent } from './lib/cards.component';
Enter fullscreen mode Exit fullscreen mode

In this case, we export TransactionsComponent and CardsComponent from the library into a single file. This creates a public API that hides all the implementation details and exposes the modules that consumers can use.

This setup seems convenient, but what is the issue with it? Let’s explore this further by creating a banking website (called demo-app) that includes a transaction, card, and account page. The account settings page is shared between different apps, not just only demo-app.

You would likely organize the transactions and cards pages in one library and the account settings in another library, structured like this:

├── apps
   ├── demo-app
      ├── app stuff
   ├── other-app
      ├── other app stuff
├── libs
   ├── demo-app
      ├── feature
         ├── card.component.ts
         ├── transactions.component.ts
         ├── index.ts (barrel file)
   ├── other-app
   ├── shared
      ├── feature
         ├── account.component.ts
         ├── index.ts (barrel file)
└── root stuff
Enter fullscreen mode Exit fullscreen mode

When creating a new library with NX, a barrel file is automatically added, and the tsconfig.base.json is updated with the appropriate path. This allows the modules to be imported from the public API.

// tsconfig.base.json

"paths": {
  "@bundleperf/demo-app/feature": ["libs/demo-app/feature/src/index.ts"],
  "@bundleperf/shared/feature": ["libs/shared/feature/src/index.ts"]
}
Enter fullscreen mode Exit fullscreen mode

I created a basic application that displays links to the three different pages. The loadComponent function is used to lazy load these pages (standalone components).

// app.routes.ts

export const appRoutes: Route[] = [
  {
    path: 'transactions',
    loadComponent: () =>
      import('@bundleperf/demo-app/feature').then(
        (c) => c.TransactionsComponent
      ),
  },
  {
    path: 'cards',
    loadComponent: () =>
      import('@bundleperf/demo-app/feature').then((c) => c.CardsComponent),
  },
  {
    path: 'account',
    loadComponent: () =>
      import('@bundleperf/shared/feature').then((c) => c.AccountComponent),
  },
];
Enter fullscreen mode Exit fullscreen mode

Home page of the demo-app with three links to transactions, cards and account.

When clicking on the Transactions link, Angular lazy loads the Transactions component.

Transactions page of the demo-app with three links to transactions, cards and account. It has a string with “All bank transactions here..”

Network activity with a chunk loading in.

As you can see in the network tab, the chunk containing the TransactionsComponent is lazy-loaded. Now, let's navigate to the cards page by clicking on the Cards link.

The Cards page of the demo-app with three links to transactions, cards and account. It has a string with “All bank cards here..”

The page changes, but the network activity remains the same. No new chunk is being loaded. But didn’t we lazy load it? It turns out that the chunk initially loaded includes both TransactionsComponent and CardsComponent.

Output of the chunk.

If we closely examine the chunk, it becomes clear that the AccountComponent is not included. Only TransactionsComponent and CardsComponent are present. Which happens to be both exported in the same barrel file.

To further investigate, we can add the current date to the CardsComponent using the Luxon date library.

Cards component with a date property that gets the current date with the Luxon library.

Refresh the page and revisit the transaction page. Let’s examine the network activity.

Network activity with a chunk and Luxon library loading in.

Suddenly, we are also loading the Luxon library, which is only used within the CardsComponent. Why? When you import an individual API, all the files in that barrel file must be fetched and transformed because they may contain the imported API and potential side effects that run on initialization. Resulting in loading more files than necessary.

Note that with the introduction of the defer feature in Angular 17, lazy loading will not occur either.

Solution

To address this issue, one approach is to avoid using barrel files and instead import modules directly. However, this means sacrificing the ability to hide the implementation details.

A more effective solution is to create libraries with a more granular structure. This involves carefully deciding what components belong together and splitting those that do not. By adopting this approach, you will reap the benefits of improved lazy loading, as well as optimization for NX. With more granular libraries, NX can more precisely determine which code should be linted, tested, built, etc., thereby speeding up local development and pipeline processes.

In a follow-up article, I’ll delve into how you can properly split your code to maximize the benefits of lazy loading, deferred loading, and pipeline tasks. Once it’s complete, I’ll provide a link here or you can follow me for updates.

Top comments (1)

Collapse
 
sherrydays profile image
Sherry Day

Great post! I'm curious, how would you recommend handling barrel files for large-scale applications?