代码分割

159 阅读6分钟

三种方式:

  • 入口点分割

  • 动态导入和懒加载

入口点分割

module.exports = {
    mode: 'development',
    devtool: false,
    entry: {
        page1: './src/page1.js',
        page2: './src/page2.js',
        page3: './src/page3.js'
    },

有3个entry,自然dist里也会有3个文件分别与之对应

多个page可能也对应多个HtmlWebpackPlugin插件的实例

这种方法的问题

如果入口 chunks 之间包含重复的模块(lodash),那些重复模块都会被引入到各个 bundle 中

不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码

动态导入和懒加载

用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载 在给单页应用做按需加载优化时

一般采用以下原则:

对网站功能进行划分,每一类一个chunk

对于首次打开页面需要的功能直接加载,尽快展示给用户,某些依赖大量代码的功能点可以按需加载

被分割出去的代码需要一个按需加载的时机

这部分内容详见webpack优化 -> moduleId和chunkId的优化

preload

每个页面加载的资源有优先级

一个资源的加载的优先级被分为五个级别,分别是

Highest 最高

High 高

Medium 中等

Low 低

Lowest 最低

异步/延迟/插入的脚本(无论在什么位置)在网络优先级中是 Low

对于本页面要用到的关键资源,包括关键js、字体、css文件,preload将会把资源得下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度

在资源上,动态调用import导入的地方,添加预先加载的注释,你指明该模块需要立即被使用

npm install --save-dev @vue/preload-webpack-plugin

const HtmlWebpackPlugin = require('html-webpack-plugin');
const AssetPlugin = require('./asset-plugin');
+ const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin');
const path = require('path');
module.exports = {
    mode: 'development',
    devtool: false,
    entry: {
        main: './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        //初始(initial)chunk 文件的名称
        filename: '[name].[chunkhash].js',
        //此选项决定了非初始(non-initial)chunk 文件的名称
        chunkFilename: '[name].[chunkhash].js',
    },
    plugins: [
+        new PreloadWebpackPlugin()
    ]
}

在打包之后,模板文件中会生成这样一行代码,来完成异步加载:

<link rel="preload" as="script" href="video.js">
document.getElementById('btn').addEventListener('click', () => {
  import(
    `./video.js`
    /* webpackPreload: true */
    /* webpackChunkName: "utils" */
  ).then(res => {
  	console.log(res.default)
  })
})

video.js:

module.exports = '播放';

以上注释中,webpackChunkName就指定了异步加载的模块名为utils,而不是默认生成的路径名拼接起来的名字

在本案例中,video在preload预加载了之后并没有立即使用,而是在按钮点击的时候才使用,所以浏览器会有warning:

除了preload之外,还有prefetch这种加载策略

prefetch 跟 preload 不同,它的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源

<link rel="prefetch" href="utils.js" as="script">
button.addEventListener('click', () => {
  import(
    `./utils.js`
    /* webpackPrefetch: true */
    /* webpackChunkName: "utils" */
  ).then(result => {
    result.default.log('hello');
  })
});

preload vs prefetch

preload 是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源

而 prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源

所以建议:对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch

splitChunks

基本概念

module:就是js的模块化webpack支持commonJS、ES6等模块化规范,简单来说就是你通过import语句引入的代码

chunk:chunk的三种形式

  • 你的项目入口(entry)
  • 通过import()动态引入的代码
  • 通过splitChunks拆分出来的代码

\

bundle:bundle是webpack打包之后的各个文件,一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出

以下面的模块关系为例,来学习分包:

各page模块代码如下:

page1.js:

let module1 = require('./module1');
let module2 = require('./module2');
let $ = require('jquery');
import( /* webpackChunkName: "asyncModule1" */'./asyncModule1');

page2.js:

let module1 = require('./module1');
let module2 = require('./module2');
let $ = require('jquery');
console.log(module1, module2, $);

page3.js:

let module1 = require('./module1');
let module3 = require('./module3');
let $ = require('jquery');
console.log(module1, module3, $);

module1、module2、module3都是各console出一条信息来

最初,chunks(以及内部导入的模块)是通过内部 webpack 图谱中的父子关系关联的。CommonsChunkPlugin 曾被用来避免他们之间的重复依赖,但是不可能再做进一步的优化。

从 webpack v4 开始,移除了 CommonsChunkPlugin,取而代之的是 optimization.splitChunks。

缓存组是用来指定代码块分割的条件,哪些模块应该被提取到哪些代码块中

缓存组可以继承和/或覆盖来自 splitChunks.* 的任何选项,例如下面的配置中default这个cacheGroups.chunk就指定:有2个代码块共享就可以放到这个chunk中,从而覆盖了splitChunks.minChunks的配置

    optimization: {
        splitChunks: {
            //all=async+initial 表示哪些代码块需要分割,默认是async异步 all等于同步的initial加异步的async
            chunks: 'all',
            //生成 chunk 的最小体积(以 bytes 为单位) 分割出去的代码最小的体积是多少 0就是不限制
            minSize: 0,
            //拆分前必须共享模块的最小 chunks 数,比如module1被3个代码块引用, module2被 2个代码块引入
            minChunks: 1,
            //缓存组可以继承和/或覆盖来自 splitChunks.* 的任何选项
            //缓存组是用来指定代码块分割的条件,哪些模块应该被 提取哪些代码块中
            cacheGroups: {
              	//默认第三方缓存组
                defaultVendors: {
                  	//控制此缓存组选择的模块。省略它会选择所有模块
                    //它可以匹配绝对模块资源路径
                    //如果某个模块资源的绝对路径匹配此正则的话,那么这个模块就可以被提供到此代码块中
                		test: /[\/]node_modules[\/]/,
                    priority: -10
                },
                default: {
                		//指定拆分前模块被 多少个代码块共享 的话才会提取到此代码块中
                    minChunks: 2,
                  	//一个模块可以属于多个缓存组 jquery
                    //优化将优先考虑具有更高 priority(优先级)的缓存组
                    //默认组的优先级为负,以允许自定义组获得更高的优先级(自定义组的默认值为 0)
	                  priority: -10
                }
            }

对于目前的page1 page2 page3的案例来说,jquery被引用了3次,它既属于defaultVendors这个缓存组,有属于default这个缓存组,因此有了priority选项,会将模块打到priority值较高的chunk中去

利用这个配置打包后结果如下:

page1 page1.js

page2 page2.js

page3 page3.js

asyncModule1 import( /* webpackChunkName: "asyncModule1" */'./asyncModule1');

defaultVendors-node_modules_jquery_dist_jquery_js jquery

defaultVendors-node_modules_lodash_lodash_js lodash

default-src_module1_js

default-src_module2_js

default和defaultVendors是两个内置默认的缓存组,设置为false以后可以将它关掉