Skip to content

Guides - Code splitting seems to miss a basic example #1333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jakearchibald opened this issue Jun 23, 2017 · 15 comments
Closed

Guides - Code splitting seems to miss a basic example #1333

jakearchibald opened this issue Jun 23, 2017 · 15 comments

Comments

@jakearchibald
Copy link

jakearchibald commented Jun 23, 2017

When reading https://webpack.js.org/guides/code-splitting-libraries/ I was trying to figure out how to create a new file based on the common parts of two entry points.

I guess this is technically what the "vendor" example is doing, but as a first-time reader I didn't realise this, because this is in a page specific to "libraries", whereas I was wanting to avoid duplication in code I'd written.

I don't know if this would help people other than me, but I would have understood the guide better if it was structured something like this:

(the following is just showing the order I think things should be introduced, not final copy)

Splitting out common code

If two of your entry points import/require the same modules, you'll end up with that code duplicated in each output file. CommonsChunkPlugin is a plugin that can extract common modules:

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

module.exports = function(env) {
    return {
        entry: {
            page1: './page1/index.js',
            page2: './page2/index.js'
        },
        output: {
            filename: '[name].[chunkhash].js',
            path: path.resolve(__dirname, 'dist')
        },
        plugins: [
            new webpack.optimize.CommonsChunkPlugin({
                name: 'common' // Specify the common bundle's name.
            })
        ]
    }
};

This will generate:

  • page1.ef0942b9ff95bd25c024.js - the stuff unique to page1.
  • page2.9de799314948aa4d7686.js - the stuff unique to page2.
  • common.a7b0b08b0c83bdccebbd.js - the stuff used in both page1 and page2.

Note: The filename of the chunk will inherit the filename of the output. [name].[chunkhash].js in this case.

page1 won't automatically load common, you must include both on your page, and common must execute first:

<script src="common.a7b0b08b0c83bdccebbd.js" defer></script>
<script src="page1.ef0942b9ff95bd25c024.js" defer></script>

Splitting out webpack's bootstrap

Webpack's bootstap code for dealing with importing modules is usually included with every output. However, CommonsChunkPlugin will see this duplication and move it into one file, common in this case.

However, since the bootstrap contains details about the other modules, such as their names & hashes, updating the code unique to page1 will also update common, since page1's hash will change. Adding a page3 will also update common since it will be adding details of this new module. This means the user is having to download a fresh common even though only a small part of the bootstrap has changed.

To solve this, we can extract the bootstrap into its own file:

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

module.exports = function(env) {
    return {
        entry: {
            page1: './page1/index.js',
            page2: './page2/index.js'
        },
        output: {
            filename: '[name].[chunkhash].js',
            path: path.resolve(__dirname, 'dist')
        },
        plugins: [
            new webpack.optimize.CommonsChunkPlugin({
                name: 'common' // Specify the common bundle's name.
            }),
            new webpack.optimize.CommonsChunkPlugin({
                name: 'manifest'
            })
        ]
    }
};

This will generate:

  • page1.ef0942b9ff95bd25c024.js - the stuff unique to page1.
  • page2.9de799314948aa4d7686.js - the stuff unique to page2.
  • common.a7b0b08b0c83bdccebbd.js - the stuff used in both page1 and page2.
  • manifest.1302ca8eccf18a208f11.js - webpack's bootstrap and module manifest.

Instances of CommonsChunkPlugin are handled in order. Our first instance extracts all the common code to common. The second instance doesn't find any common code (we've already extracted it), but it will generate a file containing the bootstrap, and remove it from the others.

Now manifest must execute first:

<script src="manifest.1302ca8eccf18a208f11.js" defer></script>
<script src="common.a7b0b08b0c83bdccebbd.js" defer></script>
<script src="page1.ef0942b9ff95bd25c024.js" defer></script>

Splitting out rarely-updated modules

We've removed all of the duplication, but you may find that a small part of one of your bundles is updating frequently. It's best to extract that, so the rest of the bundle, which perhaps updates infrequently, can cache for longer.

To do this, create an additional entry point for the parts that frequently update (or the parts that infrequently update – the important thing is they're separate):

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

module.exports = function(env) {
    return {
        entry: {
            page1: './page1/index.js',
            page2: './page2/index.js',
            fequentlyupdates: './page1/frequentlyupdates.js'
        },
        output: {
            filename: '[name].[chunkhash].js',
            path: path.resolve(__dirname, 'dist')
        },
        plugins: [
            new webpack.optimize.CommonsChunkPlugin({
                names: ['common', 'fequentlyupdates']
            }),
            new webpack.optimize.CommonsChunkPlugin({
                name: 'manifest'
            })
        ]
    }
};

CommonsChunkPlugin sees that fequentlyupdates is the name of an entry point, so it removes its code from the other modules.

Note: I expected two instances of CommonsChunkPlugin to work. One for common, then one for fequentlyupdates, but it throws an error. I don't know if this is a bug with webpack or a bug in my mental model.

Splitting out a library

Library modules are typically rarely updated as part of your build. So let's say instead of fequentlyupdates, you wanted to extract moment, the date formatting library:

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

module.exports = function(env) {
    return {
        entry: {
            page1: './page1/index.js',
            page2: './page2/index.js',
            moment: 'moment'
        },
        output: {
            filename: '[name].[chunkhash].js',
            path: path.resolve(__dirname, 'dist')
        },
        plugins: [
            new webpack.optimize.CommonsChunkPlugin({
                names: ['common', 'moment']
            }),
            new webpack.optimize.CommonsChunkPlugin({
                name: 'manifest'
            })
        ]
    }
};

Here we've replaced the path to the fequentlyupdates script with the module name.

Splitting out everything in node_modules

Assuming you update dependencies less-frequently than your application code, you may want to split everything within node_modules:

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

module.exports = function(env) {
    return {
        entry: {
            page1: './page1/index.js',
            page2: './page2/index.js'
        },
        output: {
            filename: '[name].[chunkhash].js',
            path: path.resolve(__dirname, 'dist')
        },
        plugins: [
            new webpack.optimize.CommonsChunkPlugin({
                name: 'common'
            }),
            new webpack.optimize.CommonsChunkPlugin({
                name: 'vendor',
                minChunks: function (module) {
                    return module.context && module.context.indexOf('node_modules') !== -1;
                }
            }),
            new webpack.optimize.CommonsChunkPlugin({
                name: 'manifest'
            })
        ]
    }
};
@prateekbh
Copy link
Contributor

prateekbh commented Jun 23, 2017

Splitting out everything in node_modules

A small comment here, this approach may end up hurting you in certain cases. e.g. if you use some node modules only in a very specific corner case of your application, this will bundle it in your common bundle and you will end up shipping more code than really necessary.

Specifying minChunks will just add the most commonly used node modules in this bundle.

new webpack.optimize.CommonsChunkPlugin({
  name: 'common',
  minChunks: 3, //only put node modules in common bundle, which have been used more than twice
}),

@jakearchibald
Copy link
Author

jakearchibald commented Jun 23, 2017

@prateekbh yeah, I was hoping to find a way to say "put stuff in vendor if it's from node_modules and it's already in common", but I couldn't find a way to do that.

@sokra
Copy link
Member

sokra commented Jun 23, 2017

This looks great. Would you like to convert this issue into a PR?

@jakearchibald
Copy link
Author

Sure! @TheLarkInn said he had some nits, but I'm happy to start on a PR if the rough structure is ok.

My plan would be to replace "Code Splitting - Libraries" with something structured like above, and rename the article to something like "Code Splitting - JS".

@TheLarkInn
Copy link
Member

TheLarkInn commented Jun 23, 2017

Only part I would mention is that:

When you set name property to a string value that doesn't match an entry key, it will split the bootstrap code.

In addition many will see

new webpack.optimize.CommonsChunkPlugin({
   names: ['common', 'applesauce']
})

This is the equivalent of:

new webpack.optimize.CommonsChunkPlugin({
   name: 'common'
}),
new webpack.optimize.CommonsChunkPlugin({
   name: 'applesauce'
})

Which will do the exact same as mentioned but your bootstrap bundle will be called applesauce.[hash].js

I feel like these are important to clarify but not sure how to make it flow.

@jakearchibald
Copy link
Author

I'll try and call out the difference between name & names, but I'm not really sure I understand the difference. For instance, I thought:

new webpack.optimize.CommonsChunkPlugin({
    names: ['common', 'moment']
})

…would behave the same as…

new webpack.optimize.CommonsChunkPlugin({
    name: 'common'
}),
new webpack.optimize.CommonsChunkPlugin({
    name: 'moment'
})

…but the latter throws an error: ERROR in CommonsChunkPlugin: While running in normal mode it's not allowed to use a non-entry chunk (moment).

@TheLarkInn
Copy link
Member

Then I'll have to verify this in the source (thankfully we have fully documented that plugin src for reasons like this, where what we currently have documented may vary).

Here is what our documentation for this plugin states for each property:

{
  name: string, // or
  names: string[],
  // The chunk name of the commons chunk. An existing chunk can be selected by passing a name of an existing chunk.
  // If an array of strings is passed this is equal to invoking the plugin multiple times for each chunk name.
  // If omitted and `options.async` or `options.children` is set all chunks are used,
  // otherwise `options.filename` is used as chunk name.

  filename: string,
  // The filename template for the commons chunk. Can contain the same placeholders as `output.filename`.
  // If omitted the original filename is not modified (usually `output.filename` or `output.chunkFilename`).

  minChunks: number|Infinity|function(module, count) -> boolean,
  // The minimum number of chunks which need to contain a module before it's moved into the commons chunk.
  // The number must be greater than or equal 2 and lower than or equal to the number of chunks.
  // Passing `Infinity` just creates the commons chunk, but moves no modules into it.
  // By providing a `function` you can add custom logic. (Defaults to the number of chunks)

  chunks: string[],
  // Select the source chunks by chunk names. The chunk must be a child of the commons chunk.
  // If omitted all entry chunks are selected.

  children: boolean,
  // If `true` all children of the commons chunk are selected

  async: boolean|string,
  // If `true` a new async commons chunk is created as child of `options.name` and sibling of `options.chunks`.
  // It is loaded in parallel with `options.chunks`. It is possible to change the name of the output file
  // by providing the desired string instead of `true`.

  minSize: number,
  // Minimum size of all common module before a commons chunk is created.
}```

@TheDutchCoder
Copy link
Collaborator

@skipjack is actually in the process of rewriting this part of the guides. I'm sure he will take a look at this as you put in a lot of effort and we're trying to make the guides more accessible to everyone, specifically beginners.

Thanks for all the hard work!

@jakearchibald
Copy link
Author

Ah cool! I'll hold off on the PR then. Happy to help review the new doc as someone who hit the learning curve pretty hard on this.

@TheLarkInn
Copy link
Member

Thank you Jake, we'll make sure we tag you it PR already lands before discussed here.

@skipjack
Copy link
Collaborator

@jakearchibald I'll ping you as soon as I have a PR up so we can discuss further. It would be great to have your help on this and other guides as well.

@mika76
Copy link

mika76 commented Jun 24, 2017

I'm an unknown third party here but I was reading this and trying to understand it until @TheLarkInn said the following...
"When you set name property to a string value that doesn't match an entry key, it will split the bootstrap code."

This seems like a pretty bad way of making an interface. Why not have an explicit parameter to tell whether you're splitting bootstrap or not? Currently it will just cause confusion and weird hard-to-find bugs.

If some sort of parameter is not possible then maybe separate bootstrapChunkPlugin?

@twhid
Copy link
Contributor

twhid commented Jun 24, 2017

FWIW, I wrote this:
https://github.com/1stdibs/webpack-2-long-term-cache/

Covers some of the same ground.

@skipjack
Copy link
Collaborator

skipjack commented Jun 24, 2017

@twhid great thanks. The PR I'll open shortly actually removes most of the caching-related discussion from the code splitting docs and keeps it focused on the various approaches to split code. All the caching discussion should go in the caching guide instead (which does need some updating, see #652).

We're at the point with this site where we've covered most of the missing documentation and now we have to start refining what we have by making the voice/examples a bit more consistent/reader-friendly and replacing any duplicated documentation with well-placed links (e.g. #1258).

@gkatsanos
Copy link

could you guys define whats the unit for minsize? bytes? so, 1000 = 1KB?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants