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

【Q735】前端如何进行高效的分包 #761

Open
shfshanyue opened this issue Nov 28, 2021 · 1 comment
Open

【Q735】前端如何进行高效的分包 #761

shfshanyue opened this issue Nov 28, 2021 · 1 comment

Comments

@shfshanyue
Copy link
Owner

No description provided.

@shfshanyue
Copy link
Owner Author

如何正确地进行分包

为什么需要分包?

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

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

  1. 一行代码将导致整个 bundle.js 的缓存失效
  2. 一个页面仅仅需要 bundle.js 中 1/N 的代码,剩下代码属于其它页面,完全没有必要加载

如何更好的分包?

打包工具运行时

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

前端框架运行时

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

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

  1. webpack.runtime.js 5KB ✅
  2. framework.runtime.js 30KB ✅
  3. page-a.chunk.js 50KB ✅
  4. vendor.chunk.js 50KB ❌ (因 webpack 依赖其 object-assign,而 object-assign 将被打入共同依赖 vendor.chunk.js,因此此时它必回加载,但是该页面并不依赖任何第三方库,完全没有必要全部加载 vendor.chunk.js)

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

  1. webpack.runtime.js 5KB ✅
  2. framework.runtime.js 40KB ✅ (+10KB)
  3. page-a.chunk.js 50KB ✅

高频库

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

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

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

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

答:有以下两个思路

  1. 思路一: 可对 vendor.js 改变策略,比如被引用了十次以上,被当做公共模块抽离成 verdor-A.js,五次的抽离为 vendor-B.js,两次的抽离为 vendor-C.js
  2. 思路二: 控制 vendor.js 的体积,当大于 100KB 时,再次进行分包,多分几个 vendor-XXX.js,但每个 vendor.js 都不超过 100KB

使用 webpack 分包

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

  1. minChunks: 一个模块是否最少被 minChunks 个 chunk 所引用
  2. maxInitialRequests/maxAsyncRequests: 最多只能有 maxInitialRequests/maxAsyncRequests 个 chunk 需要同时加载 (如一个 Chunk 依赖 VendorChunk 才可正常工作,此时同时加载chunk数为 2)
  3. minSize/maxSize: chunk 的体积必须介于 (minSize, maxSize) 之间

以下是 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,
}

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

1 participant