Skip to main content

Speed and Performance

In Angular, by default, NgModules are eagerly loaded, which means that as soon as the application loads, so do all the NgModules, whether or not they are immediately necessary.

Although this is not problematic for small applications, it can lead to performance issues for large applications.

Fortunately, Angular provides us with several tools to avoid such cases.

Lazy loading feature modules

Lazy load is a design pattern that loads NgModules on-demand. Thus, it helps keep initial bundle sizes smaller, which in turn helps decrease load times.

To help users, easily, lazy load specific modules of an SBA application, all libraries have been updated to support this pattern.

For example, if you want to lazy load the preview component, you should start creating an angular module with all its dependencies:

@NgModule({
  declarations: [PreviewComponent],
  imports: [
    CommonModule,
... // other modules dependencies
    PreviewRoutingModule
  ],
  exports: [PreviewComponent]
})
export class PreviewModule { }

Then, in the PreviewModule's routing module, add a route for this component:

const routes: Routes = [
  { path: '', component: PreviewComponent },
];
@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class PreviewRoutingModule { }

The final step is to use loadChildren (instead of component) in your AppRoutingModule routes configuration as follows:

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

With this in place, you should notice the Lazy chunk files, while compiling your application. Those are simply referring to our lazy loaded module

Lazy loading preview

Module Federation

Historically, build tools (Webpack,…) assume that the entire program code is available when compiling. Lazy loading is possible, but only from areas that were split off during compilation.

This was the main motivation behind the creation of Module Federation as a new feature of Webpack 5. Hence, it allows loading separately compiled and deployed code (also called micro front-ends or plugins) into an application.

To simplify the concept, a so-called host references a remote using a configured name. This reference is only resolved at runtime by loading a so-called remote entry point.

⚠️ NOTE: This concept is environment-independent (Angular, React, Vue, ASP.NET, etc.) ⚠️

Activating Module Federation for Angular Projects

To do so, you need to tell the CLI to use Module Federation when building. However, as the CLI shields webpack from us, you need a custom builder.

Fortunately, the package @angular-architects/module-federation provides such a custom builder.

For the following example, let's assume the following naming : the host application as shell and the remote application as mfe (stands for Micro front-end).

To get started, you can just "ng add" the package to your projects:

ng add @angular-architects/module-federation --project shell --port 5000

ng add @angular-architects/module-federation --project mfe --port 3000

These commands activate module federation, assign a port for ng serve, and generate the skeleton of a module federation configuration (webpack.config.js).

⚠️ NOTE: The webpack.config.js is only a partial webpack configuration. It only contains stuff to control module federation. The rest is generated by the CLI as usual ⚠️

Now let's jump onto the project mfe and edit the generated configuration in .../projects/mfe/webpack.config.js

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
[...]
module.exports = {
[...],
plugins: [
new ModuleFederationPlugin({

name: "mfe",
filename: "remoteEntry.js",
exposes: {
// The update
'./Module': './projects/mfe/src/app/toto/toto.module.ts',
},
shared: {
"@angular/core": { singleton: true, strictVersion: true },
"@angular/common": { singleton: true, strictVersion: true },
"@angular/router": { singleton: true, strictVersion: true },
[...]
}
}),
[...]
],
};

This exposes the TotoModule under the name ./Module. Hence, the shell can use this path to load it.

To do so, you need to adjust the shell's generated configuration as follows :

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
[...]
module.exports = {
[...],
plugins: [
new ModuleFederationPlugin({

// Make sure to use port 3000 defined on the first step while adding the package to the projects
remotes: {
'mfe': "mfe@http://localhost:3000/remoteEntry.js"
},
shared: {
"@angular/core": { singleton: true, strictVersion: true },
"@angular/common": { singleton: true, strictVersion: true },
"@angular/router": { singleton: true, strictVersion: true },
[...]
}
}),
[...]
],
};

This references the separately compiled and deployed mfe project.

Now, all remains is to update the routing module of the shell

const routes: Routes = [
... // other routes
{
path: 'toto',
loadChildren: () => import('mfe/Module').then(m => m.TotoModule)
}
];

However, the path mfe/Module which is imported here, does not exist within the shell. It is just a virtual path pointing to another project.

To ease the TypeScript compiler, you need a typing for it by adding the following line to the file .../projects/shell/src/decl.d.ts :

declare module 'mfe/Module';

Once starting both applications side by side (ng serve shell and ng serve mfe), you should notice, at runtime, that shell loads the mfe from its own URL

Loading remotes

Dynamic federation

In the previous section, we assumed that micro front-ends, used in the shell, are already known by the developer.

However, there might be situations where you don’t even know the list of micro front-ends upfront. For example, an application with different dynamic views based on user privileges. This information can be hold by an external system and fetched at runtime via HTTP request.

To dynamically load a micro-frontend at runtime, you need to remove the registration of the micro front-end upfront within shell configuration :

remotes: {
// "mfe": "mfe@http://localhost:3000/remoteEntry.js",
},

Instead, you should use the helper function loadRemoteModule provided by the @angular-architects/module-federation :

const routes: Routes = [
... // other routes
{
path: 'toto',
loadChildren: () =>
loadRemoteModule({
remoteEntry: 'http://localhost:3000/remoteEntry.js',
remoteName: 'mfe',
exposedModule: './Module'
})
.then(m => m.TotoModule)
}
];

To showcase the use of external systems, remotes information can be provided at runtime via a lookup service :

@Injectable({ providedIn: 'root' })
export class LookupService {
lookup(): Promise<Microfrontend[]> {
[...]
}
}

Then, after receiving the Microfrontend array from the LookupService, you can build your dynamic routes as follows :

export function buildRoutes(options: Microfrontend[]): Routes {

const lazyRoutes: Routes = options.map(o => ({
path: o.routePath,
loadChildren: () => loadRemoteModule(o).then(m => m[o.moduleNameProperty])
}));

return [...APP_ROUTES, ...lazyRoutes];
}

More Details on Module Federation

Please have a look at Webpack federation feature and Angular module federation package