shlomiassaf/webpack-dll-bundles-plugin

Lazy loading chunks not emitted when using DllBundlesPlugin

Opened this issue · 7 comments

When using this plugin, it will not emit lazy loaded modules as chunks. When I disable this plugin in the webpack config plugins: [] array, then immediately my lazy loaded modules are emitted as chunks. When I re-enable this plugin, the chunks do not get emitted.

plugin config

 new DllBundlesPlugin({
         bundles: {
            polyfills: [
               'core-js',
               'ts-helpers',
               'zone.js'
            ],
            vendors: [
               '@angular/platform-browser',
               '@angular/platform-browser-dynamic',
               '@angular/core',
               '@angular/common',
               '@angular/forms',
               '@angular/http',
               '@angular/router',
               '@angularclass/hmr',
               'rxjs',
               'jwt-decode'
            ]
         },
         dllDir: helpers.root(outputDir + '/dlls'),
         // bundleExtension: '.dll.bundle.js',
         sourceMapFilename: '/maps/[name].map',
         chunkFilename: '/chunks/[name].chunk.js',

         webpackConfig: webpackMergeDll(commonConfig, {
            devtool: 'cheap-module-source-map',
            plugins: [ // These plugins are for the DLL build only, will not be used in dev or production builds.

               new OptimizeJsPlugin({
                  sourceMap: false // prod
               }),

               new UglifyJsPlugin({
                  // DLLS is build in "Production" mode always, since it is not code I will be debugging (that's why its in the DLL bundle anyway)

                  // "Production" mode options
                  beautify: false,
                  comments: false,
                  compress: { //prod
                     screw_ie8: true,
                     warnings: false,
                     conditionals: true,
                     unused: true,
                     comparisons: true,
                     sequences: true,
                     dead_code: true,
                     evaluate: true,
                     if_return: true,
                     join_vars: true,
                     negate_iife: false, // we need this for lazy v8
                     keep_fnames: false
                  },
                  mangle: { //prod
                     screw_ie8: true,
                     keep_fnames: false
                  },
               }),

               new ExtractTextPlugin({
                  filename: '/css/[name].style.css?[hash]',
                  disable: false,
                  allChunks: true
               }),

               // new webpack.DllPlugin({
               //    // The path to the manifest file which maps between
               //    // modules included in a bundle and the internal IDs
               //    // within that bundle
               //    path: helpers.root(outputDir + '/dlls/[name]-manifest.json'),
               //
               //    // The name of the global variable which the library's
               //    // require function has been assigned to. This must match the
               //    // output.library option above
               //    name: '[name]_lib'
               //
               // }),

               new webpack.optimize.CommonsChunkPlugin({
                  name: ['polyfills', 'vendors'].reverse()
               }),

               new CompressionPlugin({
                  asset: "[path].gz",
                  test: /\.(css|html|js|json|map)(\?{0}(?=\?|$))/,
                  threshold: 2 * 1024,
                  algorithm: "gzip",
                  minRatio: 0.8
               })
            ]
         })
      }),

App routes

export const ROUTES: Routes = [{
   path: '',
   redirectTo: 'sub',
   pathMatch: 'full',
}, {
   path: 'sub',
   loadChildren: '../+sub/sub.module#SubModule',
   // canActivate: [RouteProtection]
},
{
   path: 'login',
   component: LoginComponent
}, 
];

What do you mean by not emitted as chunks?

They are part of the bundle?

FYI this plugin is not to be used in a production build.

They are neither admitted as a "0.chunk.js" nor are they a part of the bundle so it just doesnt work at all. But it appears that the issue is due to @ngtools/webpack plugin though rather than your plugin. See this issue and this repo to reproduce

How come this plugin shouldn't be used in a production build, and what do you recommend to use for production DLLs build instead?

This plugin creates a webpack bundle that represent the output after all loaders/plugins did their thing.

Webpack then consumes the bundle and every require/import that is known to exists in the bundle is taken from there without running it through the loader/plugin chain.

So it's like a cache thing, creating pre-built bundle that can be used by webpack.

In theory you can use this in production but it does not make sense, in production you have other webpack tools built for that purpose, e.g. CommonChunksPlugin and more...

Remember that webpack treats the DLL as a fully built bundle so it will not run minification on it and other stuff when doing prod build.

@shlomiassaf The way I use DLLS in production is for Angular and RxJS. Since I have no benefit to recompile them every time i load webpack dev server or do a production build, I pre-build all angular and rxjs using DLLS and I do so with uglifyjs, so basically the dlls are always in "production" mode even when used for Dev, QA, Production etc.

I don't see in which way CommonChunksPlugin would help with this use case? basically to save time on rebuilding.

It make a huge difference.

If we take RXJS, the plugin will include the whole package. Regardless of the operators you use.
With a webpack prod build it will only take those you import.

Furthermore if we take angular, the plugin will create a bundle with the compiler code included, a prod build will omit it via the CommonChunksPlugin and UglifyJS plugin since that code is not being used...

@IAMtheIAM you were using AOT huh? I just figured out, through great sacrifice, that the @ngtools/webpack AOT plugin(which is now the standard way of doing AOT) is not compatible with the DLL plugin, which this plugin wraps.

Rather than throw some error or a clean behavior, angular just fails to emit the lazy bundles. This is not mentioned in any Angular docs. angular/angular-cli#4431

Yea, recently though, I just stopped using DLL bundles plugin, because it really doesn't make sense now that I compared. It does not allow for minification or code shaking and @shlomiassaf said. It is much better to use CommonsChunkPlugin and UglifyJsPlugin properly. Use with ts-loader for webpack dev server mode and hot module replacement, and with @ngtools/webpack for production. It allows AOT compilation and very small bundle, fast build, and overall no problems.

Doing this I reduced my angular total bundles size from 7MB down to 2MB. Major improvement.

Here is my loader for TS files, example:

{
            test   : /\.ts$/, // exclude condition is for karma test runner
            exclude: [ENV === 'test'
               ? '//'
               : /\.(spec|e2e)\.ts$/],
            use    : AOT

               /**
                * AOT BUILD
                */

               ? [{
               // If any of the npm packages or loader configutation changes, it should automatically invalidate/delete the .cache folder, but you may need to do it manually
                  loader : 'cache-loader',
                  options: cacheLoaderOptions

               }, {
                  loader: '@angular-devkit/build-optimizer/webpack-loader'
               }, {
                  loader: '@ngtools/webpack'
               }, {
                  loader : 'thread-loader',
                  options: {
                     // there should be 1 cpu for the fork-ts-checker-webpack-plugin
                     // workers: require('os').cpus().length - 1,
                     workers: isDevServer
                        ? 3
                        : require('os').cpus().length - 1 // fastest build time for devServer: 3 threads; for production: 7 threads (os cpus minus 1)
                  }
               }]


               /**
                * JIT DEV BUILD
                */

               : [{
                  loader : 'cache-loader',
                  options: cacheLoaderOptions
               }, {
                  loader : '@angularclass/hmr-loader',
                  options: {
                     pretty: !isProduction,
                     prod  : isProduction
                  }
               }, {
                  /**
                   * Lazy Module loader
                   *  MAKE SURE TO CHAIN VANILLA JS CODE, I.E. TS COMPILATION OUTPUT.
                   */
                  loader : 'ng-router-loader',
                  options: {
                     loader: 'async-import',
                     aot   : AOT
                  }
               }, {
                  loader : 'thread-loader',
                  options: {
                     // there should be 1 cpu for the fork-ts-checker-webpack-plugin
                     // workers: require('os').cpus().length - 1,
                     workers: isDevServer
                        ? 3
                        : require('os').cpus().length - 1 // fastest build time for devServer: 3 threads; for production: 7 threads (os cpus minus 1)
                  }
               },

                  {
                     loader : 'ts-loader',
                     options: {
                        // disable type checker - we will use it in fork plugin
                        transpileOnly: true,
                        happyPackMode: true
                     }
                  }, {
                     loader: 'angular2-template-loader'
                  }]
         },

plugins

 /**
       * This scans all entry points for common code, but not async lazy bundles,
       * and puts it in commons.bundle.js. An array of names is the same as running
       * the plugin multiple times for each entry point (name)
       */
      new webpack.optimize.CommonsChunkPlugin({
         name     : 'commons',
         minChunks: 2
         /*      ,filename : isDevServer
                  ? '/js/commons.bundle.js'
                  : '/js/commons.bundle.[hash].js'*/
      }),

      /**
       * This only scans lazy bundles, then puts all common code in lazy bundles
       * into one chunk called "commons.lazy-vendor.chunk.js", if it isn't already
       * in commons.bundle.js. If it is already in there, it removes the common code
       * in the lazy bundles and references the modules in commons.bundle.js
       */
      new webpack.optimize.CommonsChunkPlugin({
         async    : 'commons-lazy',
         minChunks: 2 // if it appears in 2 or more chunks, move to commons-lazy bundle
      }),

      /**
       * NOTE: Any name given that isn't in the entry config will be where the
       * webpack runtime code is extracted into, which is what we want. It needs to
       * be the LAST one. Order matters.
       */
      new webpack.optimize.CommonsChunkPlugin({
         name    : 'webpack-runtime',
         filename: isDevServer
            ? 'js/webpack-runtime.js' // in dev mode, it can't have a [hash] or webpack throws an error, for some reason
            : 'js/webpack-runtime.[hash].js'
      }),


      new UglifyJsPlugin({
         // "Production" mode options
         uglifyOptions: {
            output  : {
               beautify: false
            },
            compress: {
               keep_fnames: true,
               negate_iife: false  // we need this for lazy v8
            },
            mangle  : {
               keep_fnames: true
            }
         }
      }),

With thread-loader and cache-loader, build time is very fast. Especially rebuild on HMR mode.