x93kexi

x93kexi

Webpack 分包实践

什么是分包#

分包(Code Splitting)是一种优化技术,用于将应用程序的代码分割成多个较小的包,而不是将所有代码打包成一个单一的文件。这种技术可以显著提高应用程序的性能和加载速度,特别是在大型应用中。

Webpack 中的分包#

Webpack 经过初始化、编译、优化、输出和插件执行等多个阶段,把源码输出为一个个包(Chunk)。Webpack 中的分包行为自 Webpack4 开始是一个默认行为,并且在配置文件中可以通过optimization.splitchunks这个对象字面量来配置相关行为 (https://webpack.js.org/plugins/split-chunks-plugin/)。

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        // 拆分从node_modules中导入的依赖
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        // 拆分公共模块
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

分包策略#

分包的大致思路有以下两种

  • 动态导入(dynamic import)
  • 拆分大文件,合并小文件和公共模块

动态导入#

动态导入也可以说是懒加载,在非必要的时候不拉取模块文件,只在使用时才从服务端获取相关文件。Js 内置的import()语句、React 的lazy方法和 vue-router 开箱即用的路由懒加载都是动态加载的实现。

包拆分与合并#

  • Webpack 在输出时会根据配置的 filename 给每个 Chunk 加上 hash,合理地拆分那些不会变动的模块,文件能更长久地缓存在客户端,并且在多次打包部署中,缓存仍会有效,
  • 根据网络资源,把大 Chunk 分为多个小 Chunk,能更高效地加载应用。比如 http2 下,这种操作会进一步提高网络资源的利用率
  • 抽离出几个 Chunk 的公共部分,避免模块的重复注入

实践#

  1. cdn 与缓存组比对
    cdn 导入包内容实际上也是实现长效缓存的一种方式,但随着 tree-shaking 的广泛应用,cdn 某种意义上来讲也成了一种负优化。
1. cdn导入
//webpack.config.js

modules.exports={
  externals: {
    lodash: '_'
  }
}

//index.html
//使用externals将lodash排除,再在模版文件中通过script加载,实际上是直接拉取了整个lodash库
<script src="bootcdn/ajax/[email protected]/lodash.min.js">

2. 配置缓存组
modules.exports={
  optimization.splitchunks: {
    chunks: "all",
    cacheGroups: {
      // 最后的优化阶段,会把并未使用的lodash方法给移除掉,相较于cdn导入,需要加载的代码量无疑是更少的
	  lodash: {
	    test: [\\/]node_modules[\\/]lodash,
	  }
    }
  }
}

但是在某些场景下 cdn 仍有用武之地,比如服务器使用 http1.1 进行数据传输同时依赖较小或者全量使用依赖。

  1. 合理拆分不易变动的模块
    Webpack4 的默认拆包行为会把从 node_modules 中导入的依赖全部打包到一个 Chunk 中。其中一些经常变动的依赖,比如公司内部的 sdk 或组件库可以采用默认的分包策略,但一些长期无变动的依赖,就可以使用缓存组拆分,并长期缓存在浏览器
modules.exports={
  optimization.splitchunks: {
    chunks: "all",
    cacheGroups: {
	  vue: {
	    test: [\\/]node_modules[\\/](vue|vue-router|vuex),
	  },
	  lodash: {
	    test: [\\/]node_modules[\\/]lodash
	  },
	  element: {
		test: [\\/]node_modules[\\/]element-ui
	  }
    }
  }
}
  1. 懒加载
    通过 Js 内置的 import () 方法来动态地导入组件
export default {
  component: () => import('./A.vue'),
}

同理,动态地导入路由组件

const router = createRouter({ 
// ... 
  routes: [ 
    { path: '/a', component: () => import('./views/A.vue') }, 
  ]
})

一些框架内置的懒加载方法,比如 react 中的 lazy

import { useState, Suspense, lazy } from 'react';
import Loading from './Loading.js';

const MarkdownPreview = lazy(() => delayForDemo(import('./MarkdownPreview.js')));

export default function MarkdownEditor() {
  const [showPreview, setShowPreview] = useState(false);
  const [markdown, setMarkdown] = useState('Hello, **world**!');
  return (
    <>
      <textarea value={markdown} onChange={e => setMarkdown(e.target.value)} />
      <label>
        <input type="checkbox" checked={showPreview} onChange={e => setShowPreview(e.target.checked)} />
        Show preview
      </label>
      <hr />
      {showPreview && (
        <Suspense fallback={<Loading />}>
          <h2>Preview</h2>
          <MarkdownPreview markdown={markdown} />
        </Suspense>
      )}
    </>
  );
}

function delayForDemo(promise) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  }).then(() => promise);
}

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。