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

Webpack 日常使用与优化 #37

Open
creeperyang opened this Issue Aug 26, 2017 · 20 comments

Comments

Projects
None yet
8 participants
@creeperyang
Owner

creeperyang commented Aug 26, 2017

请注意,webpack >= 3.0

记录下日常使用 webpack 的一些注意点/优化点,记忆和自用更多一点,同学们谨慎阅读。

1. webpack.optimize.CommonsChunkPlugin 比你想的更强大/配置更复杂

实际项目:

  • 入口文件: ['babel-polyfill', './src/client.js']
  • chunks:入口文件及其依赖文件里通过 import() (split point) 分离出 chunk 文件。

webpack 配置:

new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: module => {
    // 所有 node_modules 下的文件
    return module.resource && /node_modules/.test(module.resource)
  }
}),

build 情况:

2017-08-26 11 53 50

可以看到,我们生成了 vendor.hash.js ,里面包括了入口文件使用到的所有 node_modules 下的文件,但是,我们同时看到,同样位于 node_modules 下的 bluebird 并没有被打包进 vendor.hash.js,并且在多个 chunk 文件中重复。

可见,CommonsChunkPlugin 比我们想象的要复杂,我们的配置并没有达到预期。

阅读完文档和资料,算是重新认识下 CommonsChunkPlugin 的配置项。

首先我们理解下 webpack 的 chunk:

  • 入口文件(entry)也是 chunk,即 entry chunk;
  • 入口文件以及它的依赖文件里通过 code split 分离出来/创建的也是 chunk,可以理解这些 chunk 为 children chunk 。
  • CommonsChunkPlugin 创建的文件也是 chunk,即 commons chunk

其次我们要明确:CommonsChunkPlugin 是把共用模块的代码从 bundles 分离出来合并到单独的文件(commons chunk)。

有了这些概念后我们可以重新理解下 CommonsChunkPlugin 的各个选项:

  1. name 可以是已经存在的 chunk 的 name (一般是入口文件),那么共用模块代码会合并到这个已存在的 chunk;否则,创建名字为 namecommons chunk 来合并。

  2. filenames,即这个 commons chunk 的文件名(最终保存到本地的文件)。

  3. chunks 指定 source chunks,即从哪些 chunk 去查找共用模块。省略 chunks 选项时,默认为所有 entry chunks。

  4. minChunks 可以

    1. 设定为数字(大于等于2),指定共用模块被多少个 chunk 使用才能被合并。
    2. 也可以设为函数,接受 (module, count) 两个参数,用法如上。
    3. 特别地,还可以设置为 Infinity ,即创建 commons chunk 但不合并任何共用模块。这时一般搭配 entry 的配置一起用:
      entry: {
        vendor: ["jquery", "other-lib"],
        app: "./entry"
      }
      new webpack.optimize.CommonsChunkPlugin({
        name: "vendor",
    
        // filename: "vendor.js"
        // (Give the chunk a different name)
    
        minChunks: Infinity,
        // (with more entries, this ensures that no other module
        //  goes into the vendor chunk)
      })
  5. children 设为 true 时,指定 source chunkschildren of commons chunk。这里的 children of commons chunk 比较难理解,可以认为是 entry chunks 通过 code split 创建的 children chunks。childrenchunks不可同时设置(它们都是指定 source chunks 的)。

    children 可以用来把 entry chunk 创建的 children chunks 的共用模块合并到自身,但这会导致初始加载时间较长:

    new webpack.optimize.CommonsChunkPlugin({
      // names: ["app", "subPageA"]
      // (choose the chunks, or omit for all chunks)
    
      children: true,
      // (select all children of chosen chunks)
    
      // minChunks: 3,
      // (3 children must share the module before it's moved)
    })
  6. async 即解决children: true时合并到 entry chunks 自身时初始加载时间过长的问题。async 设为 true 时,commons chunk 将不会合并到自身,而是使用一个新的异步的 commons chunk。当这个 commons chunk 被下载时,自动并行下载相应的共用模块。

好了,解读了这么多选项的各种用法,是时候见证实际效果了:

new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: module => {
    return module.resource && /node_modules/.test(module.resource)
  }
}),
new webpack.optimize.CommonsChunkPlugin({
  name: 'client',
  async: 'chunk-vendor',
  children: true,
  minChunks: (module, count) => {
    // 被 3 个及以上 chunk 使用的共用模块提取出来
    return count >= 3
  }
}),

2017-08-27 1 31 19

可以看到,我们把 client 创建的多个 chunk 的共用模块分离到了 chunk-vendor.hash.chunk.js,大大减少了这些 chunk 里的重复代码,总 size 从 1.36MB 减少到 872.3KB


参考

@creeperyang

This comment has been minimized.

Owner

creeperyang commented Aug 27, 2017

2. webpack.optimize.CommonsChunkPlugin 与优化浏览器端缓存

浏览器下载静态文件是非常耗时的,所以为了提高性能,浏览器尽一切可能来使用缓存。通常情况下,

  • 对同名的静态文件,浏览器根据之前的 response header 使用 协商缓存 或 强缓存;
  • 想要资源更新后浏览器可以正确获取新的资源,一般更新文件名。

所以生产中,我们一般在文件名中加入 hash,即文件内容变化,则文件名变化。这样自动利用浏览器缓存且不会妨碍文件更新。

hash vs chunkhash

在 webpack 里,我们可以通过 hash/chunkhash 来自动更新文件名:

output: {
    filename: '[name].[chunkhash:8].js'
},

其中:

  • hash 是 build-specific ,即每次编译都不同——适用于开发阶段。
  • chunkhash 是 chunk-specific,是根据每个 chunk 的内容计算出的 hash——适用于生产。

怎么最大化利用浏览器缓存?(怎么保证 vendor 的有效缓存?)

有了 chunkhash,看起来我们已经可以正常利用浏览器缓存了。但 webpack 有个已知问题:

webpack 自身的 boilerplate 和 manifest 代码可能在每次编译时都会变化。

这导致我们只是在 入口文件 改了一行代码,但编译出的 vendor 和 entry chunk 都变了,因为它们自身都包含这部分代码。

这是不合理的,因为我们的依赖(node_modules)没变,vendor 不应该在我们业务代码变化时发生变化!而 vendor 通常 size 很大,每次发布业务代码导致 vendor 变化无法利用缓存是不可接受的。

很直接的解决方案:为什么不把 webpack 自身的这部分代码分离出来呢?

new webpack.optimize.CommonsChunkPlugin({
      name: "runtime",
      minChunks: Infinity
}),

配置很简单,如上即可。(name 只要不在 entry 里出现过即可,但通常使用 "runtime"/"manifest")。

下面是我改业务代码后的两次编译:

2017-08-28 12 56 56

2017-08-28 12 57 34

可以看到 vendor 的 chunkhash 没有变化。很好,终于可以愉快的利用浏览器缓存了!

@hanxiansen

This comment has been minimized.

hanxiansen commented Dec 20, 2017


问下博主,上面这种图片是通过什么生成的?

@creeperyang

This comment has been minimized.

@qingmingsang

This comment has been minimized.

qingmingsang commented Dec 20, 2017

现在webpack的配置越来越复杂,插件越来越多,越来越累赘了

@creeperyang

This comment has been minimized.

Owner

creeperyang commented Dec 20, 2017

主要是因为需求太多样化了,前端也越来越复杂了。

而插件越来越多,说明生态繁荣,同时能满足的场景也各式各样。

感觉号称0配置的比如 parcel 这些只能满足一部分需求,但如果没有特殊需求的可以一试。

@lz-lee

This comment has been minimized.

lz-lee commented Dec 20, 2017

多页配置webpack,需要将每个页面对应的css/less单独打包成对应的css,为什么设置minChunks >=2 就可以,而设置为 1 就只打包成一个css文件。

@creeperyang

This comment has been minimized.

Owner

creeperyang commented Dec 20, 2017

@lz-lee minChunks 指定 1 相当于命中所有chunks(每个chunk必然出现 >= 1 次),即所有 chunk 打包为一个文件。

建议提问前可以先阅读官方文档

@creeperyang

This comment has been minimized.

Owner

creeperyang commented Jan 12, 2018

3. 命令行里输出详细错误原因

合格的程序员碰到错误的第一反应一定是自己先找原因,而不是去问别人。

0配置打包工具的火爆说明webpack某种程度上比较繁琐,尤其当你碰到错误时。排除错误的前提是可以找到原因,查找原因的前提是错误信息详细输出——善用stats配置。

具体的配置官方文档都有,不再细述,这里主要强调怎么防止配置不起作用:

  1. 使用 webpack-dev-server 时需要把statsdevServer里。

  2. 使用 Node.js API 时stats 不起作用,但可以使用webpack-dev-middleware的配置:

    const devMiddleware = require('webpack-dev-middleware')(compiler, {
        publicPath: webpackConfig.output.publicPath,
        stats: {
            warnings: true,
            reasons: true,
            errors: true,
            errorDetails: true,
            colors: true,
        }
        // quiet: true
    });
    
@kunsun

This comment has been minimized.

kunsun commented Mar 6, 2018

关于chunks的分类,讲的特别好,赞

@kunsun

This comment has been minimized.

kunsun commented Mar 6, 2018

不过webpack4.0.1 好像取消了children chunks的配置,不知道是不是理解有误

@creeperyang

This comment has been minimized.

Owner

creeperyang commented Mar 6, 2018

@kunsun 对。4.0 去除CommonsChunkPlugin,使用 optimization.splitChunks/optimization.runtimeChunk 代替。

可以参考阅读:

@dickenslian

This comment has been minimized.

dickenslian commented Mar 6, 2018

总结得很好,children部分的例子对我很有帮助

@qingmingsang

This comment has been minimized.

qingmingsang commented Mar 7, 2018

new webpack.optimize.CommonsChunkPlugin({
  name: 'client',
  async: 'chunk-vendor',
  children: true,
  minChunks: (module, count) => {
    // 被 3 个及以上 chunk 使用的共用模块提取出来
    return count >= 3
  }
})

请问这个配置里的client是一个单独的entry的name吗,我试着直接用似乎没有效果

@nlffeng

This comment has been minimized.

nlffeng commented Aug 4, 2018

@creeperyang 关于webpack.optimize.CommonsChunkPlugin 与优化浏览器端缓存
在项目中静态导入本地模块时(import),vendor(公共模块)的chunkhash不会发生变化,但当我在项目中动态导入时(dynamic import),vendor的chunkhash会发生变化,不知原因是为何?
假如项目较大,就需要按需加载(dynamic import),vendor的chunkhash每次都发生变化,如何做到缓存呢?

@creeperyang

This comment has been minimized.

Owner

creeperyang commented Aug 6, 2018

@nlffeng 可以给个repo或者其它例子实际查看下吗?

@nlffeng

This comment has been minimized.

nlffeng commented Aug 6, 2018

@creeperyang 首先感谢回答,以下是我的webapck配置
image
1、当我在js文件动态导入2个(模块/js文件),构建打包后如下:
image
2、当我在js文件动态导入1个时,构建打包后,这时vendor的chunkhash发生了变化,如下:
image
vendor是公共库,是没有发生变化的,此处由于动态导入模块,引起了vendor的chunkId发生了变化,即vendor内容中chunkId值发生了变化,导致chunkhash变化,这样,在一个项目中,每次添加一个动态导入时,它的公共库的chunkhash都会发生变化,如何做到缓存呢?

@creeperyang

This comment has been minimized.

Owner

creeperyang commented Aug 7, 2018

@nlffeng 考虑下搭配 NamedModulesPlugin/HashedModuleIdsPlugin 使用。

https://webpack.js.org/guides/caching/#src/components/Sidebar/Sidebar.jsx

@nlffeng

This comment has been minimized.

nlffeng commented Aug 7, 2018

@creeperyang 我的配置中是有HashedModuleIdsPlugin这个,这个插件是可以保持node_modules当中的模块的id不变,但是生成vendor后这个模块的id会和受其它动态导入的模块的影响,才会产生这种chunkhash变化的情况,楼主可以试一试,目前还没找到如何保持这个vendor的moduleId保持不变的方法!

@cqq626

This comment has been minimized.

cqq626 commented Aug 8, 2018

@creeperyang
2018-08-08 11 34 21
我是使用了vue-router的懒加载,这样改完后,异步加载组件里的类库仍没被抽离出来,chunk-vendor文件没有生成,求指点..
webpack版本是3.3.0

@creeperyang

This comment has been minimized.

Owner

creeperyang commented Aug 10, 2018

@cqq626 可以设置stats.chunks/stats.children等,查看webpack实际是怎么为你处理的。

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