kangyana / daily-question

When your heart is set on something, you get closer to your goal with each passing day.
https://www.webpack.top
MIT License
3 stars 0 forks source link

【Q138】前端如何进行高效的分包 #138

Open kangyana opened 1 year ago

kangyana commented 1 year ago

1. 为什么需要分包?

为什么需要进行分包,一个大的 bundle.js 不好吗?

极其不建议,可从两方面进行考虑:

2. 如何更好的分包?

打包工具运行时

webpack(或其他构建工具) 运行时代码不容易变更,需要单独抽离出来,比如 webpack.runtime.js。 由于其体积小,必要时可注入 index.html 中,减少 HTTP 请求数,优化关键请求路径

前端框架运行时

React(Vue) 运行时代码不容易变更,且每个组件都会依赖它,可单独抽离出来 framework.runtime.js。 请且注意,务必将 React 及其所有依赖(react-dom/object-assign)共同抽离出来,否则有可能造成性能损耗,见下示例

假设仅仅抽离 React 运行时(不包含其依赖)为单独 Chunk,且每个路由页面为单独 Chunk。某页面不依赖任何第三方库,则该页面会加载以下 Chunk

文件 体积 必须加载
webpack.runtime.js 5KB
framework.runtime.js 30KB
page-a.chunk.js 50KB
vendor.chunk.js 50KB

因 webpack 依赖其 object-assign,而 object-assign 将被打入共同依赖 vendor.chunk.js,因此此时它必回加载,但是该页面并不依赖任何第三方库,完全没有必要全部加载 vendor.chunk.js

将 React 运行时及其所有依赖,共同打包,修复结果如下,拥有了更完美的打包方案。

文件 体积 必须加载
webpack.runtime.js 5KB
framework.runtime.js 30KB
page-a.chunk.js 50KB

高频库

一个模块被 N(2 个以上) 个 Chunk 引用,可称为公共模块,可把公共模块给抽离出来,形成 vendor.js

问:那如果一个模块被用了多次 (2 次以上),但是该模块体积过大(1MB),每个页面都会加载它(但是无必要,因为不是每个页面都依赖它),导致性能变差,此时如何分包?

答:如果一个模块虽是公共模块,但是该模块体积过大,可直接 import() 引入,异步加载,单独分包,比如 echarts

问:如果公共模块数量多,导致 vendor.js 体积过大(1MB),每个页面都会加载它,导致性能变差,此时如何分包

答:有以下两个思路

3. 使用 webpack 分包

在 webpack 中可以使用 SplitChunksPlugin (opens new window)进行分包,它需要满足三个条件:

以下是 next.js 的默认配置,可视作最佳实践

源码位置: next/build/webpack-config.ts

{
  // Keep main and _app chunks unsplitted in webpack 5
  // as we don't need a separate vendor chunk from that
  // and all other chunk depend on them so there is no
  // duplication that need to be pulled out.
  chunks: (chunk) =>
    !/^(polyfills|main|pages\/_app)$/.test(chunk.name) &&
    !MIDDLEWARE_ROUTE.test(chunk.name),
  cacheGroups: {
    framework: {
      chunks: (chunk: webpack.compilation.Chunk) =>
        !chunk.name?.match(MIDDLEWARE_ROUTE),
      name: 'framework',
      test(module) {
        const resource =
          module.nameForCondition && module.nameForCondition()
        if (!resource) {
          return false
        }
        return topLevelFrameworkPaths.some((packagePath) =>
          resource.startsWith(packagePath)
        )
      },
      priority: 40,
      // Don't let webpack eliminate this chunk (prevents this chunk from
      // becoming a part of the commons chunk)
      enforce: true,
    },
    lib: {
      test(module: {
        size: Function
        nameForCondition: Function
      }): boolean {
        return (
          module.size() > 160000 &&
          /node_modules[/\\]/.test(module.nameForCondition() || '')
        )
      },
      name(module: {
        type: string
        libIdent?: Function
        updateHash: (hash: crypto.Hash) => void
      }): string {
        const hash = crypto.createHash('sha1')
        if (isModuleCSS(module)) {
          module.updateHash(hash)
        } else {
          if (!module.libIdent) {
            throw new Error(
              `Encountered unknown module type: ${module.type}. Please open an issue.`
            )
          }

          hash.update(module.libIdent({ context: dir }))
        }

        return hash.digest('hex').substring(0, 8)
      },
      priority: 30,
      minChunks: 1,
      reuseExistingChunk: true,
    },
    commons: {
      name: 'commons',
      minChunks: totalPages,
      priority: 20,
    },
    middleware: {
      chunks: (chunk: webpack.compilation.Chunk) =>
        chunk.name?.match(MIDDLEWARE_ROUTE),
      filename: 'server/middleware-chunks/[name].js',
      minChunks: 2,
      enforce: true,
    },
  },
  maxInitialRequests: 25,
  minSize: 20000,
}