Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at TechCrunch Disrupt (New York, NY) with: Danielle Morrill
Ben Nadel at TechCrunch Disrupt (New York, NY) with: Danielle Morrill@DanielleMORRILL )

Experimenting With Lazy Loaded Modules, Ahead Of Time (AoT) Compiling, And Webpack 4 In Angular 6.1.7

By Ben Nadel on

Now that I've finally upgraded my Angular research and development (R&D) workflow to use Ahead of Time (AoT) compiling, I figured it was time to tackle another one of my white whales: lazy loading modules in a route-based Angular application. For a long time, other developers have told me that lazy loading modules "just works." Of course, the devil is always in the details; and, it took me a number of days - and compromises - to get to something that actually compiles in Webpack / TypeScript and runs in the browser. As such, I wanted to document my approach so that I could refer back to it in the future.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

When moving to a lazy loaded module scenario in Angular 6.1.7, we have to change the way that we define our route configuration. Instead of simply telling Angular which routes map to which components:

  • {
  • path: "my-feature",
  • component: MyFeatureComponent
  • }

... we have to tell Angular which routes map to which module locations:

  • {
  • path: "my-feature",
  • loadChildren: "./my-feature/my-feature.module#MyFeatureModule"
  • }

Then, when the user navigates to this route, Angular loads the MyFeatureModule code file asynchronously and renders the first matching component under the "my-feature" segment.

One thing to be aware of is that the route definitions in a lazy-loaded module have to take into account that they are "under" the "my-feature" segment. Meaning, a "root" component in a feature has to match against the "empty string", not the "my-feature" segment since the "my-feature" segment was already provided as the hook for the lazy-loading:

  • RouterModule.forChild([
  • {
  • // NOTE: Since this module is being lazy-loaded, the root segment has
  • // already been defined (as part of the lazy-load configuration). As
  • // such, the root segment here in the lazy-loaded module is empty.
  • path: "",
  • component: MyFeatureComponent
  • }
  • ])

The path that we provide to the "loadChildren" property of the route configuration can either be relative to the current module; or, it can be relative to the application:

  • Module Relative: ./my-feature/my-feature.module#MyFeatureModule
  • App Relative: app/my-feature/my-feature.module#MyFeatureModule

Notice that if it is module-relative, the path has to start with "./". Without the leading dot, the path is assumed to be relative to the app module.

Ok, so to get started, I put together a small "Hello World" type demo for the lazy-loading of Angular modules. I replaced one statically loaded module with one behind a "loadChildren" path. The code compiled in Webpack 4 and TypeScript; but, when I went to run it in the browser, the asynchronously loaded module came up as a 404 Not Found.

I posted my confusion about this on Facebook. And, as luck would have it, Emmanuel Kwene swooped in with the solution! He pointed me in the direction of the "publicPath" property in my Webpack 4 configuration. It turns out that the "publicPath" property is specifically used to help locate assets that are asynchronously loaded into a Webpack-based application.

Since my Webpack 4 code gets compiled into a "./build" directory, I updated my Webpack's "output" section to include a "publicPath" of "./build":

  • output: {
  • filename: "[name].[contenthash].js",
  • path: path.join( __dirname, "build" ),
  • publicPath: "./build/"
  • },

Woot! With this "publicPath" property in place, my "loadChildren" module was located and then pulled into the running application successfully.

My next hurdle was when I tried to add a second lazy loaded module. For some reason, with a second lazy loaded module, the Angular application would no longer compile in Webpack. I kept getting a "Cyclic Dependency" error. Googling revealed that this error is related to some bug in the "toposort" module within Webpack's HtmlWebpackPlugin.

To get around this bug, I had to switch the "chunksSortMode" setting from the default value of "dependency" to "none":

  • // I generate the main "index" file and inject Script tags for the files
  • // emitted by the compilation process.
  • new HtmlWebpackPlugin({
  • filename: "../index.htm",
  • template: "./app/main.htm",
  • // CAUTION: I had to switch this to "none" when using Lazy Loading
  • // modules otherwise I was getting a "Cyclic dependency" error in the
  • // Toposort module in this plug-in. As a side-effect of this, I had to
  • // start including the Polyfill file directly in the main.ts (as opposed
  • // to including it as an entry point).
  • // --
  • // Read More: https://github.com/jantimon/html-webpack-plugin/issues/870
  • chunksSortMode: "none"
  • }),

This allowed the Webpack 4 application to compile. But, when I went to run it in the browser, nothing worked. The problem was, once the "chunkSortMode" was removed, my Polyfill entry file was no longer being injected into the HTML file as the first Script tag. As such, the polyfill for Zone.js was no longer available at the time the Angular code executed.

I think maybe I could have fixed this by creating some custom Function-based sort for chunks. But, honestly, all the examples of that went way over my head. Webpack mostly confuses me. After flailing on this problem for a day or so, I finally just decided to say, "screw it"; and, I moved the Polyfill from an external entry file into an explicit "import" in my root TypeScript file, "main.ts":

  • // Import for side effects - we have to import this first so that the polyfills will
  • // be available for the rest of the code.
  • // --
  • // NOTE: I would normally include this as an Entry bundle; but, I couldn't get the
  • // HtmlWebpackPlugin to work properly if I did that (since I don't think it could
  • // implicitly determine the dependency order). In the future, I might be able to make
  • // this more dynamic (ie, use Webpack's import() syntax).
  • import "./main.polyfill";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // Import the core angular services.
  • import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
  •  
  • // Import the root module for bootstrapping.
  • import { AppModule } from "./app.module";
  •  
  • platformBrowserDynamic().bootstrapModule( AppModule );

I believe this ended up compiling it into the "vendors" file that Webpack 4 automatically generates using its default "splitChunks" configuration. And, in the future, I may be able to conditionally execute the import based on Webpack's import() syntax. But, that's a whole other exploration. For now, I'll settle for the fact that the Webpack 4 application is compiling and the Angular 6.1.7 application is loading, complete with lazy loaded modules.

That said, even though things were finally compiling and running, I didn't like the asymmetry of how the routes were configured. As I mentioned in Shai Reznik's routing post on A Better Pattern For Large Scale Apps, a statically defined routing module needs to define the routes and get imported into the parent module; whereas a lazy loaded module simply needs to define the routes (no import into the parent module required).

What I wanted was for the topography of the parent module to be the same, regardless of whether or not the child routes were being statically loaded or lazy loaded. The sticking point for this was the "module reference." So, I decided that I would try deferring the "module reference" to the child module; very much in the same way that I am deferring the route configuration to the child module.

This approach requires each feature module to export both its routes and its modules:

  • export var MyFeatureView: RoutableView = {
  • modules: [
  • // ....
  • ],
  • routes: [
  • // ....
  • ]
  • };

Now, in a statically loaded module, this export would contain two populated collections:

  • export var MyFeatureView: RoutableView = {
  • modules: [
  • MyFeatureModule // <---- needs to be imported into parent module.
  • ],
  • routes: [
  • {
  • path: "my-feature",
  • component: MyFeatureComponent
  • }
  • ]
  • };

And, in a lazy loaded module, this export would contain only one populated collection:

  • export var MyFeatureView: RoutableView = {
  • modules: [
  • // NOTE: Since this module is being lazy-loaded, the parent module does NOT NEED
  • // to import this module. As such, this collection is empty.
  • ],
  • routes: [
  • {
  • path: "my-feature",
  • loadChildren: "./my-feature/my-feature.module#MyFeatureModule"
  • }
  • ]
  • };

At this point, the parent module becomes unaware of the interplay between the modules and the routes. And, the parent module can simply spread the two collections into its own configuration:

  • @NgModule({
  • imports: [
  • BrowserModule,
  • // --
  • ...MyFeatureView.modules, <--- SPREAD into the current configuration.
  • // --
  • RouterModule.forRoot(
  • [
  • {
  • path: "app",
  • children: [
  • ...MyFeatureView.routes <--- SPREAD into the current configuration.
  • ]
  • },
  • ]
  • )
  • ],
  • // ...
  • })
  • export class AppModule {
  • // ...
  • }

As you can see, it just spreads the ".modules" property into its "imports"; and, the ".routes" property into its ".children". In the case where the child module is being lazy loaded, the ".modules" collection is empty and the spread is, essentially, a no-op. In the case where the child module is being statically loaded, however, the ".modules" collection is populated, allowing the spread to import the child module into the parent module.

The parent module is now completely unaware of how the child module is being loaded. And, what's more, the semantics of the parent module are consistent regardless of how the module is being loaded.

You may think that this inverts the control in an odd way. But, the reality is, routing is a really messy thing. I have yet to see any examples of routing configuration that seem clean and easy to work with. If you try to centralize routes, then you need to import loads of component references. If you try to decentralize routes, you lose sight of the overall app structure. If you want the parent modules to know how the child modules are loaded, you have to spread the "segment" definitions across two modules. If you want to the child to own its own loading strategy, then you have to jump through some hoops (like I've done) in order to get the semantics to be consistent.

Routing is messy! Any approach you choose will always entail a set of trade-offs. In this case, I'm adding verbosity in order to keep the routing semantics consistent and more loosely coupled.

Now that I've laid out the basics of my current approach to lazy loading modules in Angular 6.1.7 with Webpack 4, let's take a closer look at the code. I've already outlined the main.ts bootstrapping file above; so, let's look at the root Angular module.

My demo application has three "primary" modules and one "secondary" module. Of these four feature modules, three are lazy loaded and one is statically loaded:

  • // Import the core angular services.
  • // import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
  • import { BrowserModule } from "@angular/platform-browser";
  • import { NgModule } from "@angular/core";
  • import { RouterModule } from "@angular/router";
  • import { Routes } from "@angular/router";
  •  
  • // Import the application components and services.
  • import { AppViewComponent } from "./views/app-view.component";
  • import { AsideView } from "./views/aside-view/aside-view.module";
  • import { FeatureAView } from "./views/feature-a-view/feature-a-view.module";
  • import { FeatureBView } from "./views/feature-b-view/feature-b-view.module";
  • import { FeatureCView } from "./views/feature-c-view/feature-c-view.module";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // When we included a routable view into the router tree, we have to define the routes
  • // and, SOMETIMES, import the view modules (when statically loaded). In order to keep
  • // the routing semantics consistent across our views, I'm pushing both the ROUTE and
  • // MODULE definitions into the subview. This way, the parent context always SPREADS both
  • // the modules (into the imports) and the routes (into the RouterModule) into its own
  • // definition. This allows a module to switch from statically loaded to lazy loaded
  • // without the parent context having to know about it.
  • export interface RoutableView {
  • modules: any[],
  • routes: Routes
  • }
  •  
  • @NgModule({
  • imports: [
  • BrowserModule,
  • // NOTE: When a routing module is statically included, then the routing module
  • // needs to be explicitly imported. In order to not worry about this divergence,
  • // let's let the child module define the importable modules (which may or may
  • // not be an EMPTY ARRAY - empty if lazy-loaded).
  • // --
  • ...AsideView.modules, // <--- empty array.
  • ...FeatureAView.modules, // <--- empty array.
  • ...FeatureBView.modules, // <--- empty array.
  • ...FeatureCView.modules,
  • // --
  • RouterModule.forRoot(
  • [
  • {
  • path: "app",
  • children: [
  • // CAUTION: These routes define LAZY LOADED modules.
  • ...FeatureAView.routes, // <--- using "loadChildren"
  • ...FeatureBView.routes, // <--- using "loadChildren"
  • ...AsideView.routes, // <--- using "loadChildren"
  •  
  • // CAUTION: These routes define STATICALLY LOADED modules.
  • ...FeatureCView.routes
  • ]
  • },
  • // Handle root redirect to app.
  • {
  • path: "",
  • pathMatch: "full",
  • redirectTo: "app"
  • },
  • // Handle root not-found redirect.
  • {
  • path: "**",
  • redirectTo: "/app"
  • }
  • ],
  • {
  • // Tell the router to use the HashLocationStrategy.
  • useHash: true,
  • enableTracing: false
  • }
  • )
  • ],
  • providers: [
  • // CAUTION: We don't need to specify the LocationStrategy because we are setting
  • // the "useHash" property in the Router module above (which will be setting the
  • // strategy for us).
  • // --
  • // {
  • // provide: LocationStrategy,
  • // useClass: HashLocationStrategy
  • // }
  • ],
  • declarations: [
  • AppViewComponent
  • ],
  • bootstrap: [
  • AppViewComponent
  • ]
  • })
  • export class AppModule {
  • // ...
  • }

As I mentioned above, three of the four feature modules are lazy-loaded. However, as you can see from the App module, there is no semantic difference in how we are integrating the feature modules. Each feature module exports its own modules and routes. These modules and routes are then consistently spread into the parent module (which is the App module in this case).

The differences between the lazy loaded modules and the statically loaded modules are fully encapsulated within the target module. For example, here is "Feature-B", a lazy loaded module that also imports a child module:

  • // Import the core angular services.
  • import { CommonModule } from "@angular/common";
  • import { NgModule } from "@angular/core";
  • import { RouterModule } from "@angular/router";
  • import { Routes } from "@angular/router";
  •  
  • // Import the application components and services.
  • import { FeatureBViewComponent } from "./feature-b-view.component";
  • import { MyWidgetComponent } from "./my-widget.component";
  • import { RoutableView } from "~/app/app.module";
  • import { SubBView } from "./sub-b-view/sub-b-view.module";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @NgModule({
  • imports: [
  • CommonModule,
  • // NOTE: When a routing module is statically included, then the routing module
  • // needs to be explicitly imported. In order to not worry about this divergence,
  • // let's let the child module define the importable modules (which may or may
  • // not be an EMPTY ARRAY - empty if lazy-loaded).
  • // --
  • ...SubBView.modules, // <--- empty array.
  • // --
  • RouterModule.forChild([
  • {
  • // NOTE: Since this module is being lazy-loaded, the root segment has
  • // already been defined (as part of the lazy-load configuration). As
  • // such, the root segment here is empty.
  • path: "",
  • component: FeatureBViewComponent,
  • children: [
  • // CAUTION: These routes define LAZY LOADED modules.
  • ...SubBView.routes
  • ]
  • }
  • ])
  • ],
  • declarations: [
  • FeatureBViewComponent,
  • MyWidgetComponent
  • ]
  • })
  • export class FeatureBViewModule {
  • // ...
  • }
  •  
  • export var FeatureBView: RoutableView = {
  • modules: [
  • // NOTE: Since this module is being lazy-loaded, the parent module does NOT NEED
  • // to import this module. As such, this collection is empty.
  • ],
  • routes: [
  • {
  • path: "feature-b",
  • loadChildren: "./views/feature-b-view/feature-b-view.module#FeatureBViewModule"
  • // NOTE: I could have also defined this path relative to the root of the
  • // application. Though, by making it relative to the parent module, I had
  • // to use "./" in the path prefix.
  • // --
  • // loadChildren: "app/views/feature-b-view/feature-b-view.module#FeatureBViewModule"
  • }
  • ]
  • };

Because this module is lazy loaded, it exports an empty "modules" collection. And, in its "routes" collection, it includes only a single route that indicates a "lazy load". The rest of the routes for the feature are defined within the RouterModule.forChild() configuration.

Let's now contrast this with the "Feature-C" module that is statically loaded:

  • // Import the core angular services.
  • import { CommonModule } from "@angular/common";
  • import { NgModule } from "@angular/core";
  • import { RouterModule } from "@angular/router";
  • import { Routes } from "@angular/router";
  •  
  • // Import the application components and services.
  • import { FeatureCViewComponent } from "./feature-c-view.component";
  • import { RoutableView } from "~/app/app.module";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @NgModule({
  • imports: [
  • CommonModule
  • // NOTE: Since this module is being statically loaded in the app, we don't need
  • // to define a .forChild() router definition - these routes are being included
  • // directly into the parent module's route definition.
  • // --
  • // RouterModule // <--- Include if you need router-outlet or routerLink.
  • ],
  • declarations: [
  • FeatureCViewComponent
  • ]
  • })
  • export class FeatureCViewModule {
  • // ...
  • }
  •  
  • export var FeatureCView: RoutableView = {
  • modules: [
  • // NOTE: Since this module's routes are being included directly in the parent
  • // module's router definition, we need to tell the parent module to import this
  • // module. Otherwise, the application won't know about the declared components
  • // and services.
  • FeatureCViewModule
  • ],
  • routes: [
  • {
  • path: "feature-c",
  • component: FeatureCViewComponent
  • }
  • ]
  • };

This time, since the "Feature-C" module is statically loaded, its "modules" collection contains the Feature Module reference that needs to be imported into the parent context. And, its "routes" collection points directly to Angular components (indicating that no lazy load is needed).

Notice that in both cases - lazy loaded or statically loaded - the "root segment" of the feature is holey defined within the feature module itself. This helps keep the modules easier to understand since you don't have to flip back and forth between two different modules to get a sense of where, in the URL, a module is located.

There are two other features in this lazy-loaded Angular application. But, they are more or less copies of the previously-discussed modules. As such, they aren't worth showing here - you can look at my GitHub project if you want to see more.

NOTE: The "Aside" feature module is loaded into a secondary router-outlet. It works exactly like the other routes; except for that it defines an "outlet" property in the subroot of its router configuration tree.

The one final piece of the puzzle is the Webpack 4 configuration. In my Webpack 4 configuration file, the things to note are the single entry file, the "publicPath" output property, and "chunksSortMode" in the HtmlWebpackPlugin:

  • // Load the core node modules.
  • var AngularCompilerPlugin = require( "@ngtools/webpack" ).AngularCompilerPlugin;
  • var CleanWebpackPlugin = require( "clean-webpack-plugin" );
  • var HtmlWebpackPlugin = require( "html-webpack-plugin" );
  • var path = require( "path" );
  • var webpack = require( "webpack" );
  •  
  • // We are exporting a Function instead of a configuration object so that we can
  • // dynamically define the configuration object based on the execution mode.
  • module.exports = ( env, argv ) => {
  •  
  • var isDevelopmentMode = ( argv.mode === "development" );
  •  
  • // Locally, we want robust source-maps. However, in production, we want something
  • // that can help with debugging without giving away all of the source-code. This
  • // production setting will give us proper file-names and line-numbers for debugging;
  • // but, without actually providing any code content.
  • var devtool = isDevelopmentMode
  • ? "eval-source-map"
  • : "nosources-source-map"
  • ;
  •  
  • // By default, each module is identified based on Webpack's internal ordering. This
  • // can cause issues for cache-busting and long-term browser caching as a localized
  • // change can create a rippling effect on module identifiers. As such, we want to
  • // identify modules based on a name that is order-independent. Both of the following
  • // plugins do roughly the same thing; only, the one in development provides a longer
  • // and more clear ID.
  • var moduleIdentifierPlugin = isDevelopmentMode
  • ? new webpack.NamedModulesPlugin()
  • : new webpack.HashedModuleIdsPlugin()
  • ;
  •  
  • return({
  • // I define the base-bundles that will be generated.
  • // --
  • // NOTE: There is no explicit "vendor" bundle. With Webpack 4, that level of
  • // separation is handled by default. You just include your entry bundle and
  • // Webpack's splitChunks optimization DEFAULTS will automatically separate out
  • // modules that are in the "node_modules" folder.
  • // --
  • // CAUTION: The ORDER OF THESE KEYS is meaningful "by coincidence." Technically,
  • // the order of keys in a JavaScript object shouldn't make a difference because,
  • // TECHNICALLY, the JavaScript language makes to guarantees around key ordering.
  • // However, from a practical standpoint, JavaScript keys are iterated over in the
  • // same order in which they were defined (especially in V8). By putting the
  • // POLYFILL bundle first in the object definition, it will cause the polyfill
  • // bundle to be injected into the generated HTML file first. If you don't want to
  • // rely on this ordering - or, if it breaks for you anyway - you can use the
  • // HtmlWebpackPlugin (see: chunksSortMode) to explicitly order chunks.
  • entry: {
  • main: "./app/main.ts"
  • // NOTE: I'm currently including the polyfill directly in the main.ts file.
  • // If I have it as an Entry, I get a "cyclic dependency" error since I had to
  • // ALSO change my "chunksSortMode" to "none" in order to get Lazy Loading
  • // modules to work.
  • // --
  • // polyfill: "./app/main.polyfill.ts",
  • },
  • // I define the bundle file-name scheme.
  • output: {
  • filename: "[name].[contenthash].js",
  • path: path.join( __dirname, "build" ),
  • publicPath: "./build/"
  • },
  • devtool: devtool,
  • resolve: {
  • extensions: [ ".ts", ".js" ],
  • alias: {
  • "~/app": path.resolve( __dirname, "app" )
  • }
  • },
  • module: {
  • rules: [
  • // I provide a TypeScript compiler that performs Ahead of Time (AoT)
  • // compilation for the Angular application and TypeScript code.
  • {
  • test: /(\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
  • loader: "@ngtools/webpack"
  • },
  • // When the @ngtools webpack loader runs, it will replace the @Component()
  • // "templateUrl" and "styleUrls" with inline "require()" calls. As such, we
  • // need the raw-loader so that require() will know how to load .htm and .css
  • // files as plain-text.
  • {
  • test: /\.(htm|css)$/,
  • loader: "raw-loader"
  • },
  • // If our components link to .less files instead of .css files, then the
  • // less-loader will parse the LESS CSS file on-the-fly during the require()
  • // call that is generated by the @ngtools webpack loader.
  • {
  • test: /\.less$/,
  • loaders: [
  • "raw-loader",
  • "less-loader"
  • ]
  • }
  • ]
  • },
  • plugins: [
  • // I clean the build directory before each build.
  • new CleanWebpackPlugin([
  • path.join( __dirname, "build/*.js" ),
  • path.join( __dirname, "build/*.js.map" )
  • ]),
  •  
  • // I work with the @ngtools webpack loader to configure the Angular compiler.
  • new AngularCompilerPlugin({
  • tsConfigPath: path.join( __dirname, "tsconfig.json" ),
  • mainPath: path.join( __dirname, "app/main" ),
  • entryModule: path.join( __dirname, "app/app.module#AppModule" ),
  • // Webpack will generate source-maps independent of this setting. But,
  • // this setting uses the original source code in the source-map, rather
  • // than the generated / compiled code.
  • sourceMap: true
  • }),
  •  
  • // I generate the main "index" file and inject Script tags for the files
  • // emitted by the compilation process.
  • new HtmlWebpackPlugin({
  • // Notice that we are saving the index UP ONE DIRECTORY, so that it is
  • // output in the root of the demo.
  • filename: "../index.htm",
  • template: "./app/main.htm",
  • // CAUTION: I had to switch this to "none" when using Lazy Loading
  • // modules otherwise I was getting a "Cyclic dependency" error in the
  • // Toposort module in this plug-in. As a side-effect of this, I had to
  • // start including the Polyfill file directly in the main.ts (as opposed
  • // to including it as an entry point).
  • // --
  • // Read More: https://github.com/jantimon/html-webpack-plugin/issues/870
  • chunksSortMode: "none"
  • }),
  •  
  • // I facilitate better caching for generated bundles.
  • moduleIdentifierPlugin
  • ],
  • optimization: {
  • splitChunks: {
  • // Apply optimizations to all chunks, even initial ones (not just the
  • // ones that are lazy-loaded).
  • chunks: "all"
  • },
  • // I pull the Webpack runtime out into its own bundle file so that the
  • // contentHash of each subsequent bundle will remain the same as long as the
  • // source code of said bundles remain the same.
  • runtimeChunk: "single"
  • }
  • });
  •  
  • };

Bringing this all together gives me an Angular 6.1.7 application with Ahead of Time (AoT) compiling and lazy loading routes. The lazy loading can be seen as additional HTTP requests that get made during the life-cycle of the Angular application:


 
 
 

 
 Lazy loading modules in Angular 6 makes additional HTTP requests to load the module code. 
 
 
 

As you can see, as we navigate to each of the lazy loaded modules, Angular makes a subsequent HTTP request back to the server to load the module bundle. It then incorporates the bundle into the running application and renders the relevant components.

And, of course, when we navigate to "Feature C", there is no additional HTTP request because that feature module is being statically loaded in the main application bundle.

This is the approach to lazy loading modules in Angular 6.1.7 that I could finally get working with Webpack 4 and Ahead of Time (AoT) compiling. It is in no way intended to represent any "best practices". For all I know, my approach is hella sloppy. That said, there are things about it that I really like, such as the symmetric loading semantics and the encapsulation of the router segment definitions within a feature module.

If anyone has any suggestions about improving this design, I'd really love to hear it!



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

@Rhrn,

I could swear that I tried that approach with no luck. I will try again to see if maybe I made a typo somewhere.

Reply to this Comment

@Rhrn,

So, I was able to get something to build based on the generated file (and then updating the chunks to match):

chunks: [ "runtime", "polyfill", "vendors~polyfill", "vendors~main", "main" ],
chunksSortMode: "manual"

The problem with this is that I can't really explain why that works. I think it is only a coincidence that the names I am choosing match the chunk names that the default splitter is using. In order to feel more comfortable with the manual sort option, I think I would have to explicitly define the chunking algorithm, which is way over my head.

By moving the polyfill.ts into the main.ts file, then I lose some parallel request-ability; but, at the benefit of feeling like the magic is "fully encapsulated" and not a leaky-abstraction that I have to know about.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.