Skip to content
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 code splitting 解析 #242

Open
yutingzhao1991 opened this issue Feb 28, 2019 · 0 comments
Open

webpack code splitting 解析 #242

yutingzhao1991 opened this issue Feb 28, 2019 · 0 comments
Labels

Comments

@yutingzhao1991
Copy link

什么是 code splitting

为什么要做 code splitting

  • 减少单一资源的大小(大小和数目之间有一个权衡)
  • 对于多页或者动态加载下的多路由情况下减小冗余的内容(同样的模块在不同逻辑可能会被重复包含)
  • 通过不同页面直接模块的复用提高缓存的作用
  • 将长期不会改变的内容打包到一个文件中避免新的发布带来资源的更新,可以提高缓存的命中率

chunk 概述

chunk 是指最终被打包出来的代码块(构建产物每个文件就是一个 chunk),code splitting 则是指如何生成这些代码块。做 code splitting 有如下四个方法:

  • 多 entry,多 entry 不仅可以用来多个独立的应用的配置,还可以用来实现一个应用打包成为多个包。
  • 动态加载(Dynamic Imports)。
  • Prevent Duplication,配置 optimization.splitChunks,使用 SplitChunksPlugin 这个插件。
  • 配置 optimization.runtimeChunk。

其中前面两种是基本的方法,它们将 chunk 分为 initial 和 async 两种类型。

  • initial 是指初始化 chunk,一般就是每个 entry 对应一个。
  • async 是动态加载的部分,也就是项目里面通过 import() 来加载的部分。

后面两个方法是指通过 webpack 的配置,可以按照一定的规范来划分 chunk,也就是从上面两种基本的 chunk 中提取出更多 chunk,达到提高缓存命中率,降低冗余代码等优化,从而实现期望的优化目的。

一句话来说:webpack 会 以 entry 和 import 为切割点划分文件,然后按照 optimization.splitChunks 配置来做公共 chunk 的提取。

解析 SplitChunksPlugin (optimization.splitChunks)

Since webpack v4, the CommonsChunkPlugin was removed in favor of optimization.splitChunks.

webpack 核心通过 entry 还有 import 实现了按照切割点做基本的划分,SplitChunksPlugin 主要是实现基于基础划分之上的一个优化,主要就是通过提取更多通用的 chunk 来做到 Prevent Duplication。

配置说明

官方配置文档:https://webpack.js.org/configuration/optimization/#optimizationsplitchunks

其实就是 SplitChunksPlugin 插件的配置 https://webpack.js.org/plugins/split-chunks-plugin/

umi(该文章写的时候是 2.5.5) 的默认配置是 { chunks: 'async', name: 'vendors' }

  if (!opts.disableDynamicImport && !process.env.__FROM_UMI_TEST) {
    webpackConfig.optimization
      .splitChunks({
        chunks: 'async',
        name: 'vendors',
      })
      .runtimeChunk(false);
  }

image

这里我觉得 umi 的默认配置不是很合理,这样会导致实际上所有提取出来的模块最后都被合并了。

webpack 原生的默认规则是:

  • chunk 是共享的或者是在 node_modules 下面。
  • chunk 的大小大于 30kb(压缩和 gzip 之前)。
  • 每个页面的 chunk 数不大于 5 个。
  • 初始化页面的 chunk 数不大于 3 个。

具体默认配置:

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 提取 chunks 的时候从哪里提取,如果为 all 那么不管是不是 async 的都可能被抽出 chunk,为 initial 则会从非 async 里面提取。
      minSize: 30000, // byte, == 30 kb,越大那么单个文件越大,chunk 数就会变少(针对于提取公共 chunk 的时候,不管再大也不会把动态加载的模块合并到初始化模块中)当这个值很大的时候就不会做公共部分的抽取了
      maxSize: 0, // 文件的最大尺寸,优先级:maxInitialRequest/maxAsyncRequests < maxSize < minSize,需要注意的是这个如果配置了,umi.js 就可能被拆开,最后构建出来的 chunkMap 中可能就找不到 umi.js 了。
      minChunks: 1, // 被提取的一个模块至少需要在几个 chunk 中被引用,这个值越大,抽取出来的文件就越小
      maxAsyncRequests: 5, // 在做一次按需加载的时候最多有多少个异步请求,为 1 的时候就不会抽取公共 chunk 了
      maxInitialRequests: 3, // 针对一个 entry 做初始化模块分隔的时候的最大文件数,优先级高于 cacheGroup,所以为 1 的时候就不会抽取 initial common 了。
      automaticNameDelimiter: '~', // 文件名分隔符
      name: true, // chunk 的名称,如果设置为固定的字符串那么所有的 chunk 都会被合并成一个,这就是为什么 umi 默认只有一个 vendors.async.js。
      cacheGroups: { // 自定义规则,会继承和覆盖上面的配置
        vendors: {
          test: /[\\/]node_modules[\\/]/, // test 符合这个规则的才会加到对应的 group 中
          priority: -10 // 一个模块可能属于多个 chunkGroup,这里是优先级,自定义的 group 是 0
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true // 如果该chunk包含的modules都已经另一个被分割的chunk中存在,那么直接引用已存在的chunk,不会再重新产生一个
        },
		common: { // 这个不是默认的,我自己加的
		  filename: '[name].bundle.js', // chunks 为 initial 时有效。在 manifest 中最后会是 '[name].js': [name].bundle.js。在 umi 中该项默认值是 [name].async.js,webpack 默认值是 [name].js。
		  name: 'common', // 和 filename 的作用类似
          chunks: 'initial',
          minChunks: 1,
		  enforce: true, // 不管 maxInitialRequest maxAsyncRequests maxSize minSize 怎么样都会生成这个 chunk
		}
      }
    }
  }
};

构建产物生成规则

遵循 [type]~[filename] 这样的规则。

其中 filename 对于动态加载分割出来的是 output.chunkFilename,其它的是 output.filename

filename 和 chunkFilename 的格式类似 [name].async.js,其中 [name] 可以指定,也可以使用自动生成的。对于动态加载的模块,可以配置 webpackChunkName 这个 inline directives 来设置 name。

// 示例 webpackChunkName
function getComponent() {
  return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
    var element = document.createElement('div');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    return element;

  }).catch(error => 'An error occurred while loading the component');
}

getComponent().then(component => {
  document.body.appendChild(component);
});

公共的 chunk 的 name 是多个模块的 name 合并在一起(但是有长度限制):

类似:vendors~p__dashboard__routers__App~p__dashboard__routers__Books~p__dashboard__routers__Collaborative~c1537219.async.js

性能优化

优化思路:

  • 总体目的是为了降低(最小可用)资源加载时间。
  • 提高缓存命中率。
  • 降低资源尺寸大小。
  • 预加载。

预加载

  • webpack 4.6.0+ 支持了 prefetching 和 preloading。
import(/* webpackPrefetch: true */ 'LoginModal');

会在 HTML 中添加:

<link rel="prefetch" href="login-modal-chunk.js">

和语雀做的预加载不同,这个预加载是预加载未来可能出现的请求。prefetch 是新的页面中可能需要加载的文件,preload 是当前页面可能需要加载的文件。

splitChunk 配置策略

请求数和大小之间的权衡。HTTP2 普及后倾向于多文件,单个文件更小,提高缓存命中率和总体资源大小。

个人的优化建议:

  • initial 的模块按照项目实际情况手动划分为几个部分,对于单页应用来说通常就是指把 node_modules 里面的内容单独抽取,比如可以把 react 相关抽取出来作为一个 chunk,然后其他工具抽取出来作为一个 chunk。这样固定的模式更容易保持构建产物在多次发布之间不改变。
  • async 的模块设定一个合适的 minSize 即可,使得文件大小和个数最优。个人感觉也不需要单独把 node_modules 中的文件提取出来,这样可能会导致文件太多。
  • 对于多 entry 的应用可以再抽取出公共的方法,通过 minChunks 来设定,比如超过两个引用就抽取到 commons 中。

external

一些特别大的库可以加上 external,比如 data-set,g2 这些。

optimization.runtimeChunk

比起 optimization.splitChunks 来说,optimization.runtimeChunk 更简单。

这个值默认是 false,当 runtimeChunk 为 true 时,会将 webpack 生成的 runtime 作为独立 chunk ,runtime 包含在模块交互时,模块所需的加载和解析逻辑。这部分的大小通常来说很小,在 ant-design-pro 的项目中尝试了一下,设置为 true 之后打包出来的 runtime~umi.js 只有 36.0 KB(未压缩和 gzip 前)。

如果配置了改项,那么你需要在你的页面中提前引入相关的 runtime 的 js。

示例

将多个 entry 共享的代码提取到一个 chunk 里面

config.optimization.splitChunks({
  cacheGroups: {
	commons: {
	  name: 'commons', // 没有 name 的话会按照规则生成多个 chunk。name 相同使得 chunk 都被合并到一起了,不再受到其它规则的约束。一般 chunks 不是 sync 的 cacheGroups 都要指定 name,因为 initial 的 chunk 中抽取出来的模块需要手动引入(当然也可以按照构建信息来自动处理,不过当前 umi 中没有这个逻辑,umi 默认是 ['umi'] 这一个 chunk)
	  chunks: 'initial', // async 的模块将不参与这个逻辑
	  minChunks: 2, // 至少被两个入口 chunk 复用的模块才会被提取
	},
  },
});

把 node_modules 中的内容全部打到一个 chunk 里面

config.optimization.splitChunks({
  cacheGroups: {
	vendors: {
	  name: 'vendors', // 没有 name 的话会按照规则生成多个 chunk。name 相同使得 chunk 都被合并到一起了,不再受到其它规则的约束。
	  chunks: 'all',
	  test: /[\\/]node_modules[\\/]/,
	},
  },
});

尽可能的多文件,每个模块一个 chunk

这个示例只是一个演示,通常你的项目不应该这么做,这样会导致请求数过多。

config.optimization.splitChunks({
  maxAsyncRequests: 10000,
  chunks: 'async', // 这里只是针对 async 的 chunk,因为 async 的 chunk 都是自动的异步加载的,分多少个都没关系,但是对于 initial 的 chunk,需要手动引入
  minChunks: 1,
  minSize: 0,
});

上面这个配置试了下在 pro 的 dashboard 页面能够拆出 57 个 js。

image

async 不提取 common,每个动态加载的模块一个 chunk

config.optimization.splitChunks({
  maxAsyncRequests: 1, // 每个 async 的 chunk 抽取过后还是只有一个
  // minSize 这里就不管用了,虽然 minSize 优先级最高,但是再高也没法再削减入口的 JS 的大小。
});

其它一些有趣的点

  • 多 entry 不仅可以用来多个独立的应用的配置,还可以用来实现一个应用打包成为多个包。参考官网的 https://webpack.js.org/guides/output-management/ 这个说明。
  • 老版本的 webpack 是使用 require.enture 来做动态加载,它是 node 的一个未能正式发布的特性。

参考文章

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

No branches or pull requests

2 participants