Webpack 4 chunking different runtime behaviour compared to Webpack 3

Do you want to request a feature or report a bug?

Bug / Change in behaviour compared to Webpack 3

What is the current behavior?

When telling Webpack which chunk the runtime should be in (via runtimeChunk.name), it doesn’t appear to change the order of execution – see below example.

If the current behavior is a bug, please provide the steps to reproduce.

vendor.js

import $ from 'jquery';
console.log('vendor run.');
$.fn.sayHi = function() {
    alert('Hi');
};

main.js

import $ from 'jquery';
console.log('main run.');
$.fn.sayHi();

index.htm

<html>
<body>
<script src="dist/vendor.js"></script>
<script src="dist/main.js"></script>
</body>
</html>

Webpack 3 config

var webpack = require('webpack');
var path = require('path');

module.exports = {

    mode: 'production',

    entry: {
        main: './main.js',
        vendor: './vendor.js'
    },

    output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].js',
    },

    optimization: {
        minimize: false,
        runtimeChunk: {
            name: 'vendor'
        },
        splitChunks: {
            cacheGroups: {
                default: false,
                commons: {
                    test: /node_modules/,
                    name: "vendor",
                    chunks: "initial",
                    minSize: 1
                }
            }
        }
    }

};

Webpack 4 Result

image

image

As you can see, main run fires before vendor, even though the runtime has been put inside vendor and it’s loaded first.

Webpack 3 Result

image

image

As you can see vendor run fires first, which means plugin is successfully registered and the alert dialog pops up.

Author: Fantashit

10 thoughts on “Webpack 4 chunking different runtime behaviour compared to Webpack 3

  1. You are comparing the CommonsChunkPlugin to the SplitChunksPlugin, they work differently.

    • You should not have a entrypoint for vendor. It’s not an entry.
    • If you want to run two modules in order at an entrypoint, use an array.
    • You should not use the vendor chunk as runtime chunk
    • In general don’t use the runtimeChunk option, except if you know why you need it for long term caching
    var webpack = require('webpack');
    var path = require('path');
    
    module.exports = {
    
        mode: 'production',
    
        entry: {
            main: ['./vendor.js', './main.js']
        },
    
        output: {
            path: path.resolve(__dirname, './dist'),
            filename: '[name].js',
        },
    
        optimization: {
            splitChunks: {
                cacheGroups: {
                    vendor: {
                        test: /node_modules/, // you may add "vendor.js" here if you want to
                        name: "vendor",
                        chunks: "initial",
                        enforce: true
                    }
                }
            }
        }
    };
  2. @sokra Thanks that works nicely. I think I was using my old Webpack 3 config in a slightly weird way so it didn’t translate to Webpack 4 too well. I’m a bit of a Webpack noob still so I’m very thankful for your help.

    Out of interest, would you be able to explain what the enforce option does? Does that just turn off any additional checks/conditions so if it matches the test case it always splits it?

    Also I don’t quite understand the chunks option – for instance, what’s it mean by initial? Is that basically any non async stuff?

  3. Looking at this issue, how to transform:

        entry: {
            vendor: [
                'babel-polyfill',
                'fetch-everywhere',
            ],
            main: [
                path.resolve(__dirname, '../../src/client/index.js'),
            ],
        },

    For it to work with splitChunks ?

    Currently I have to files created main.js and vendor.js which have both at the beginning the webpack bootstrap logic, which is unneeded.

    How to add splitChunks efficiently for fixing this issue?

  4. @GuillaumeCisco

    I think you would just do:

    var webpack = require('webpack');
    var path = require('path');
    
    module.exports = {
    
        mode: 'production',
    
        entry: {
            main: [
                'babel-polyfill',
                'fetch-everywhere',
                path.resolve(__dirname, '../../src/client/index.js'),
            ],
        },
    
        output: {
            path: path.resolve(__dirname, './dist'),
            filename: '[name].js',
        },
    
        optimization: {
            splitChunks: {
                cacheGroups: {
                    vendor: {
                        test: /babel-polyfill|fetch-everywhere/,
                        name: "vendor",
                        chunks: "initial",
                        enforce: true
                    }
                }
            }
        }
    };
  5. I believe hacking in deep configuration, while there is already predefined presets that are recommended by core team and already working for 90%% usecases it’s not a good way to go

    Have you read the discussion on splitChunks and people trying to convert their existing Webpack 3 config over to 4? It’s been a nightmare for some, and it’s exactly because of the “predefined presets” that are in place – they are obscured in the framework and you need to know how they work in order to accurately port your config over.

    especially for newcomers.

    Agreed. Webpack 4 seems targeted towards new people and easily starting up configs. There doesn’t appear to be a clear path for people migrating existing projects over, at least to me.

    This is what starting all the myths about “webpack is hard, I spent the whole day with it’s configs”.
    I clearly see how much time core team is investing in updates that making things accessible, so why avoid it and continue recommend everyone to hack even it’s not needed?

    Well the Webpack 4 documentation is pretty sparse at the moment, I’ve personally spent all day with it’s config trying to convert an existing project over to 4 and it has been much more challenging than it should have been – hence this issue to begin with. I’m also having other problems that’s part of the core webpack-contrib team. For now, I have lived with “hacks” as you say to get things working(ish) compared with my Webpack 3 config, which is very basic and doesn’t have some crazy stuff which goes against the norm.

    Do you think predefined presets are not working for the masses as expected? You can make an issue and I am sure core team would listen to you.

    Predefined configs take a lot of the pain away from configuring stuff manually initially, but I do disagree with some of the fundamental choices that have been made with splitting chunks – for instance, chunks are only split if they are larger than 30KB – this means that if you use various node_modules vendors and let’s say they are all 29KB, you basically don’t get a vendor chunk out the box. I see any node_modules stuff, regardless of size, to be a vendor and should form part of the long term caching. Webpack 4 has chosen not for that to be default and I think that will affect the masses, personally.

    This could end up as “too heated” so I am dropping the mic.

    Amen. I appreciate everything the Webpack 4 team is doing and I’m sure everything will work itself out in the end 👍

  6. Just adding some more examples of using Webpack 4’s optimization.splitChunks:

    I discovered that Webpack 4 easily allowed me to create awesome modularized bundles from my TypeScript-based project. I was wondering if it might help someone else 😄

    Here is the dependency graph of my project:
    module-dependency-graph

    You can see the code at this link.

    Essentially, I have two entry points, each of which depends on one or more “application libraries” (CompanyLib, ProductLib and AnimalLib) and vendor libraries (jquery, typescript-collections):

    1. main.ts, which depends on CompanyLib and ProductLib and jquery.
    2. main2.ts, which depends on AnimalLib.

    Some of these application libraries have external dependencies of their own:

    1. In the CompanyLib/ folder:
      • Organization.ts depends on typescript-collections.
      • etc.
    2. In the ProductLib/ folder:
      • Catalog.ts depends on typescript-collections,
      • TShirt.ts depends on CompanyLib/Organization.ts
      • etc.

    Within an application library, different classes depend on one another freely.


    My goal was to emit a separate bundle for each entry point, application library, and vendor library.

    The reasoning behind this is:

    • As an application library (say, LibA) is developed (features added, bugfixes, performance improvements etc), it might frequently add/remove imports on features of other application and vendor libraries. If we build all such vendor libraries into a single bundle and application libraries into a different bundle, then every import that is added/removed in the code of LibA will cause a corresponding addition/removal of code in these bundle, thus changing its hash and preventing clients from caching it.
      E.g. if LibA depends on lodash, it might add/remove imports for different features of the lodash package frequently. If we only include the features of lodash which are used, we lose all caching ability every time an import is changed.
      The same applies to application bundles: if LibA depends on LibB, and they are both built into the same application bundle, then every time LibA or LibB changes, the client must reload the entire bundle, even if the other does not change. If you have multiple application libraries in the same app.bundle.js bundle, then any time any application library changes, the client’s browser cache is invalidated.
    • So, by making each application library a separate bundle, the client can cache entire application and vendor libraries. This strategy works well so long as you assume your application library does not import several other application or vendor libraries and use only a tiny number of features of each (in general, that is pretty bad coding practice; you want the minimum number of dependencies possible while still maintaining functionality).
    • A more detailed explanation of this reasoning can be found here. I’ve included some calculations as to how such bundling can enable better caching in the long run here.

    I managed to make the bundles I required with the following Webpack 4 config:

    const config = {
        entry: {
            main: "./src/main.ts",
            main2: "./src/main2.ts",
        },
        optimization:{
            splitChunks: {
                cacheGroups: {
                    AnimalLib: {
                        test: new RegExp('AnimalLib' + '\\' + path.sep + '.*.ts'),
                        chunks: "initial",
                        name: "AnimalLib",
                        enforce: true,
                        
                    },
                    CompanyLib: {
                        test: new RegExp('CompanyLib' + '\\' + path.sep + '.*.ts'),
                        chunks: "initial",
                        name: "CompanyLib",
                        enforce: true
                    },
                    ProductLib: {
                        test: new RegExp('ProductLib' + '\\' + path.sep + '.*.ts'),
                        chunks: "initial",
                        name: "ProductLib",
                        enforce: true
                    },
                    jquery: {
                        test: new RegExp('node_modules' + '\\' + path.sep + 'jquery.*'),
                        chunks: "initial",
                        name: "jquery",
                        enforce: true
                    },
                    typescript_collections: {
                        test: new RegExp('node_modules' + '\\' + path.sep + 'typescript-collections.*'),
                        chunks: "initial",
                        name: "typescript_collections",
                        enforce: true
                    }
                }
            }
        },
        module: {
            rules: [
                {
                    resource: {
                        test: /\.ts$/,
                        exclude: /node_modules/,
                    },
                    use: 'awesome-typescript-loader',
                }
            ]
        },
        resolve: {
            extensions: ['.ts', '.js', 'json']
        },
        output: {
            filename: '[name]-[chunkhash:6].bundle.js',
            path: path.resolve(__dirname, 'build')
        }
    };

    Essentially, I added custom keys to optimization.splitChunks.cacheGroups, one for each of the bundles I wanted to output. For each application library, I combined all *.ts files as a separate cacheGroup. Each vendor library also became a separate cacheGroup. Each cacheGroup becomes a separate chunk and then a separate bundle.

    All my bundles are minified together, rather than separately (which is what I would have gotten if I used multiple configs).

    Console output:

    [at-loader] Ok, 0.286 sec.
    Hash: 5875a60095271f85477e
    Version: webpack 4.1.0
    Time: 2191ms
    Built at: 2018-3-13 23:51:48
    Environment (--env): "prod"
                                      Asset      Size  Chunks             Chunk Names
                    jquery-6c5dbc.bundle.js  84.5 KiB       0  [emitted]  jquery
                CompanyLib-6db0e4.bundle.js  1.59 KiB       1  [emitted]  CompanyLib
                 AnimalLib-b9d09f.bundle.js  1.32 KiB       2  [emitted]  AnimalLib
                ProductLib-e46420.bundle.js  4.17 KiB       3  [emitted]  ProductLib
    typescript_collections-ee138a.bundle.js  27.8 KiB       4  [emitted]  typescript_collections
                     main2-abc3b7.bundle.js  1.27 KiB       5  [emitted]  main2
                      main-e5d5f4.bundle.js   2.1 KiB       6  [emitted]  main
    Entrypoint main = typescript_collections-ee138a.bundle.js ProductLib-e46420.bundle.js CompanyLib-6db0e4.bundle.js jquery-6c5dbc.bundle.js main-e5d5f4.bundle.js
    Entrypoint main2 = AnimalLib-b9d09f.bundle.js main2-abc3b7.bundle.js
       33 modules
    

    I have only added the chunkhashes for show.

    Now, I can import different bundles on different pages, and it works perfectly:

    <!-- src/index.html -->
    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8" />
            <title>Hello TypeScript!</title>
        </head>
        <body>
            <p id="employees">EMPTY</p>
            <script src="../build/jquery.bundle.js"></script>
            <script src="../build/typescript_collections.bundle.js"></script>
            <script src="../build/ProductLib.bundle.js"></script>
            <script src="../build/CompanyLib.bundle.js"></script>
            <script src="../build/main.bundle.js"></script>
        </body>
    </html>
    <!-- src/index2.html -->
    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8" />
            <title>Hello TypeScript!</title>
        </head>
        <body>
            <script src="../build/main2.bundle.js"></script>
            <script src="../build/AnimalLib.bundle.js"></script>
        </body>
    </html>

    If I make a small change in CompanyLib/Organization.ts (which is depended on by multiple other files) and then re-compile, I get:

    [at-loader] Ok, 0.273 sec.
    Hash: 787fff911cee68afd06c
    Version: webpack 4.1.0
    Time: 2706ms
    Built at: 2018-3-13 23:52:51
    Environment (--env): "prod"
                                      Asset      Size  Chunks             Chunk Names
                    jquery-6c5dbc.bundle.js  84.5 KiB       0  [emitted]  jquery
                CompanyLib-d97a47.bundle.js  1.65 KiB       1  [emitted]  CompanyLib
                 AnimalLib-b9d09f.bundle.js  1.32 KiB       2  [emitted]  AnimalLib
                ProductLib-e46420.bundle.js  4.17 KiB       3  [emitted]  ProductLib
    typescript_collections-ee138a.bundle.js  27.8 KiB       4  [emitted]  typescript_collections
                     main2-abc3b7.bundle.js  1.27 KiB       5  [emitted]  main2
                      main-e5d5f4.bundle.js   2.1 KiB       6  [emitted]  main
    Entrypoint main = typescript_collections-ee138a.bundle.js ProductLib-e46420.bundle.js CompanyLib-d97a47.bundle.js jquery-6c5dbc.bundle.js main-e5d5f4.bundle.js
    Entrypoint main2 = AnimalLib-b9d09f.bundle.js main2-abc3b7.bundle.js
       33 modules
    

    As you can see, only the CompanyLib bundle has changed. The hashes of all the other bundles remain the same. Thus, the client can cache them.

    Hope this helps!

  7. I stumbled upon such a configuration after trying many many variations. Conceptually, I’m not 100%% sure why it works (e.g., what is a cacheGroup? How does splitChunks work from a beginner’s perspective? What is the initial parameter?)

    Better documentation for Webpack 4 would be an enormous help.

Comments are closed.

Webpack 4 chunking different runtime behaviour compared to Webpack 3

Do you want to request a feature or report a bug?

Bug / Change in behaviour compared to Webpack 3

What is the current behavior?

When telling Webpack which chunk the runtime should be in, it doesn’t appear to change the order of excecution – see below example.

If the current behavior is a bug, please provide the steps to reproduce.

vendor.js

import $ from 'jquery';
console.log('vendor run.');
$.fn.sayHi = function() {
    alert('Hi');
};

main.js

import $ from 'jquery';
console.log('main run.');
$.fn.sayHi();

index.htm

<html>
<body>
<script src="dist/vendor.js"></script>
<script src="dist/main.js"></script>
</body>
</html>

Webpack 3 config

var webpack = require('webpack');
var path = require('path');

module.exports = {

    mode: 'production',

    entry: {
        main: './main.js',
        vendor: './vendor.js'
    },

    output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].js',
    },

    optimization: {
        minimize: false,
        runtimeChunk: {
            name: 'vendor'
        },
        splitChunks: {
            cacheGroups: {
                default: false,
                commons: {
                    test: /node_modules/,
                    name: "vendor",
                    chunks: "initial",
                    minSize: 1
                }
            }
        }
    }

};

Webpack 4 Result

image

image

As you can see, main run fires before vendor, even though the runtime has been put inside vendor and it’s loaded first.

Webpack 3 Result

image

As you can see vendor run fires first, which means plugin is successfully registered and the alert dialog pops up.

~~

Original issue below – fixed by providing custom splitChunks. Above is now the current issue.

~~
Do you want to request a feature or report a bug?

Bug

What is the current behavior?

Webpack 4 will add jQuery to both vendor.js and main.js when you use ProvidePlugin – this has a side effect that if you have a vendor plugin that is registering itself as a jQuery plugin in vendor.js thru $.fn it is only available for use in vendor.js and won’t work in main.js

If the current behavior is a bug, please provide the steps to reproduce.

vendor.js

$.fn.sayHi = function() {
    alert('Hi');
};

main.js

import $ from 'jquery';

$.fn.sayHi();

Webpack 3 config

var webpack = require('webpack');
var path = require('path');

module.exports = {

    entry: {
        main: './main.js',
        vendor: './vendor.js'
    },

    output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].js',
    },

    plugins: [
        new webpack.ProvidePlugin({
           $: 'jquery',
           jQuery: 'jquery',
           'window.jQuery': 'jquery',
       }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        })
    ]

};

The webpack 4 config is exactly the same, except minus the CommonChunkPlugin

This is the output:

Webpack 3 with ProvidePlugin

w3 with provideplugin

Webpack 4 with ProvidePlugin

w4 with provideplugin

As you can see, Webpack 4 has added jQuery to both entries, though Webpack 3 hasn’t.

What is the expected behavior?

To only include one version of jQuery.

If this is a feature request, what is motivation or use case for changing the behavior?

This is likely down to the new “rules” surrounding when to chunk and it also not playing nicely with the ProvidePlugin. This is something that will likely catch a lot of people out, if they use the default chunking algorithm in Webpack 4 along with ProvidePlugin. Maybe there is a way to default to the old behaviour as Webpack 3, but it’s not immediately obvious to me how to do this.

Example Repository

https://github.com/garygreen/webpack4-chunks-prob
~~

Author: Fantashit

4 thoughts on “Webpack 4 chunking different runtime behaviour compared to Webpack 3

  1. Nope. For the life of me I cannot get it working as it did in Webpack 3 – the new splitChunks config settings are cryptic, despite reading https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693 a million times.

    So this is what I’ve got now:

    var webpack = require('webpack');
    var path = require('path');
    
    module.exports = {
    
        mode: 'production',
    
        entry: {
            main: './main.js',
            vendor: './vendor.js'
        },
    
        output: {
            path: path.resolve(__dirname, './dist'),
            filename: '[name].js',
        },
    
        optimization: {
            minimize: false,
            splitChunks: {
                cacheGroups: {
                    default: false,
                    commons: {
                        test: /jquery/,
                        name: "vendor",
                        chunks: "initial",
                        minSize: 1,
                        reuseExistingChunk: true
                    }
                }
            }
        },
    
        plugins: [
            new webpack.ProvidePlugin({
                '$' : './jquery.js',
               jQuery: './jquery.js'
           })
        ]
    
    };

    jquery.js

    ( function( global, factory ) {
    	"use strict";
    	if ( typeof module === "object" && typeof module.exports === "object" ) {
    		module.exports = global.document ?
    			factory( global, true ) :
    			function( w ) {
    				if ( !w.document ) {
    					throw new Error( "jQuery requires a window with a document" );
    				}
    				return factory( w );
    			};
    	} else {
    		factory( global );
    	}
    } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
    
    "use strict";
    
    var
    	version = "3.3.1",
    	jQuery = function( selector, context ) {
    		return new jQuery.fn.init( selector, context );
    	};
    
    jQuery.fn = jQuery.prototype = {
    };
    
    var
    	_jQuery = window.jQuery,
    	_$ = window.$;
    
    if ( !noGlobal ) {
    	window.jQuery = window.$ = jQuery;
    }
    
    return jQuery;
    } );

    (this is a split down version for easier testing)

    This gives me a runtime in main.js and not in vendor.js – however in Webpack 3 it was the other way round, runtime was in vendor.js and not in main.js.

    After some debugging it looks like main.js is executing BEFORE vendor.js which is why the plugins aren’t available, as they haven’t been attached yet because vendor hasn’t run.

    I’ve tried fiddling around with the new optimization.runtimeChunk config option to see if I can get the vendor.js with the runtime instead, but that option doesn’t seem to have any effect whatsoever.

    I know Webpack 4 is in it’s early days – but looking at the comments on that Gist I’m not the only one having trouble migrating.

    @pavelloz you mention you had no troubles with migrating – do you use jQuery and any plugins (and chunk to vendor.js) by any chance? Have you tried just creating a very basic setup where you have a) jQuery, b) some jQuery plugin, c) Use the jQuery plugin in main.js and have jQuery and the plugin go to vendor.js ? That’s essentially the problem I’m experiencing at the moment.

  2. So it seems you can change where the runtime goes by naming the entry:

        optimization: {
            runtimeChunk: {
                name: 'vendor'
            },

    However in my case this doesn’t change what order the files are executed in, it seems main.js is still firing before vendor.js, even though runtime is in vendor.

  3. Continuing off track..

    Multi-Page-Application

    I have a question regarding “controlling the bundle output using webpack.config.js”

    With webpack v3 we used CommonsChunkPlugin, worked good for me.
    With webpack 4 we have to configure this with optimization.splitchunks right?!

    What I want:

    I have a vendor.build.js which imports all vendor libs, output should be: vendor.bundle.js.
    Inside of vendor.build.js I also import all css/scss/less files which want to be outputed as: bundle.css.

    In the webpack.config I use one instance of the ExtractTextPlugin for extracting all css/scss/less into one file.

    I also load modules dynamically in vendor.js and postome.js.

     entry: {
            "vendor": [join(scriptsfolder, "vendor.build")],  // jquery/boostrap/lodash...
            "view.default.index": join(scriptsfolder, "controllers/default/index"),  // page script
            "view.default.posttome": join(scriptsfolder, "posttome")     // page script
        },
    

    How do I have to configure the optimization config part to get the following results in dist folder:

    • vendor.bundle.js
    • bundle.css
    • postome.js
    • view.default.index.js
    • AsynchronouslyLoadedModule1.js
    • AsynchronouslyLoadedModule2.js
  4. I guess, the main problem is, that we still think the CommonsChunk way and the documentation it rather sparse regarding this new approach…

Comments are closed.