VueCli 包分割

VueCli 对包进行分隔 | 子包优化 | vendors块过大 | 首屏加载慢 | webpack 打包 | 模块拆分子模块

Posted by luoruiqing on September 9, 2020

这个教程将Vue项目从 vendors的5MB 优化到 几百KB, 包含对模块细拆分

首先解决这个问题有几种方式:

  1. CDN 全局注入依赖
  2. 自定义分块

CDN注入依赖的方式, 需要取决于CDN的响应速度, 若CDN响应较慢, 自己的页面打开速度就会受到影响, 若自身服务器不存在速度上的瓶颈, 还是推荐使用 自定义分块, 因为稳定性和快速渲染是最重要的

环境

预览环境

大概长这样, 可以明确看到每个 模块 的大小, 以及各个 模块 下的 子模块 分布情况, 自行百度了解

img

浏览器环境

为了对网站加载情况验证, 打包后是否能正常使用等, 需要获取加载资源的过程分析工具, 以 Chrome 为例

img

  1. 打开chromenetwork 工具
  2. 禁用缓存, (如果对加载性能要求高, 可以创建一个符合情况的网络)
  3. 分析耗时

分析

日志

打包完成后的输出, 可以直观的看到直接依赖的每个包的大小情况等等, 不包含子模块的分布

dist/static/js/chunk-vendors.cf91892a.js     4835.36 KiB      1864.70 KiB
dist/static/js/chunk-89828764.77f88b15.js    3313.86 KiB      1263.45 KiB
dist/static/js/chunk-04dfe695.183b3c8f.js    1830.89 KiB      598.43 KiB

预览

  1. 使用浏览器打开 report.html 文件, 例如: <项目>/dist/report.html文件
  2. 执行下面的命令, 然后浏览器打开 http://0.0.0.0:8000/report.html
    1
    2
    
     # 机器需要装有 python3, 记住这条命令, 能在目录切换和命令行上节省不少时间, 进行快速调试
     cd <项目路径> && npm run build && cd dist && python -m http.server
    

打包

img

根据日志或者预览环境的信息, 可以看到 vendors 居然占用到了4.72MB 的大小, 同时包含 element-ui, echarts, lodash 等多个子包在内.

拆一个包

万事开头难, 先拿 element-ui 开刀试试效果, 修改配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// vue.config.js
module.exports = {
    // ...
    chainWebpack(config) {
        // 生成环境开启
        if (process.env.NODE_ENV !== 'development') {
            // 抽包
            config.optimization.splitChunks({
                cacheGroups: {
                    // 第三方块抽离 ****************************************************************
                    "chunk-element-ui": {
                        name: "chunk-element-ui",
                        test: /[\\/]node_modules[\\/]element-ui/, // 匹配所有element-ui相关的包
                        chunks: "all",
                        reuseExistingChunk: true,
                        enforce: true
                    },
                }
            })
        } 
    }
}

打包:

1
cd <项目路径> && npm run build && cd dist && python -m http.server

打开页面看看加载是否存在问题 http://0.0.0.0:8000

依赖警告

img

控制台有几个警告信息, 正好是刚刚修改的 element-ui 的拆包 并且服务已经白屏无法正常打开, 错误信息如下:

The resource XXX was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate as value and it is preloaded intentionally.

这个问题是 更改过入口或使用 多入口开发(多页应用)或未使用默认配置导致的, 需要 显式声明块和页面的依赖关系 , 修改自定义的抽块依赖配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// vue.config.js
module.exports = {
    pages: {
        index: {
            chunks: [
                'index', //  这是基本页面块, 不能少
                'chunk-element-ui',  // 这里是自定义拆分的块名称, 要对应 optimization.splitChunks 下的name填写 
            ],
        },
    },
    chainWebpack(config) {
        // 生成环境开启
        if (process.env.NODE_ENV !== 'development') {
            // 抽包
            config.optimization.splitChunks({
                cacheGroups: {
                    // 第三方块抽离 ****************************************************************
                    "chunk-element-ui": {
                        name: "chunk-element-ui",
                        test: /[\\/]node_modules[\\/]element-ui/, // 匹配所有element-ui相关的包
                        chunks: "all",
                        reuseExistingChunk: true,
                        enforce: true
                    },
                }
            })
        } 
    }
}

注意: pages.[PAGE].chunks 字段与 optimization.splitChunks.cacheGroups 内的 name 字段需要 一一对应

再次打包, 并开启浏览器检查时候还有白屏警告的情况

拆分成功

通过浏览器打开页面检查一切正常

img

此时通过预览页可以看到 element-ui 被单独拆到了一个包内, 但是 vendors 这个包不见了(先忽略这个问题), 打开浏览器页面还是正常的.

打开预览后入口文件由 4.72MB4.19MB 效果立竿见影

加载瓶颈

img

从网络时间线可以看到浏览器在加载资源的顺序和耗时, 当 html文件 加载完成后, 所有的 chunk 都是同时并发加载的, 这时候就会有木桶效应, 整个页面会等待最大的块的响应, 这里的 index.js 的块依然过大, 继续拆分

逻辑复用

根据上面的配置, 可以看到每个块的 name 都是要写入到 pages.[PAGE].chunks 中, 为了方便后续维护, 写个逻辑兼容一下, 很简单代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 分块逻辑
const SPLIT_CHUNKS = {
    'chunk-element-ui': { 'test': /[\\/]node_modules[\\/]element-ui/ },
}
// 增加附加配置
Object.keys(SPLIT_CHUNKS).forEach((name, priority) => Object.assign(SPLIT_CHUNKS[name], {
    name,
    priority, // 优先级, 根据下标决定
    enforce: true,
    reuseExistingChunk: true,
    chunks: SPLIT_CHUNKS[name].chunks || 'all', // 默认 all
}))
// 配置
module.exports = {
    pages: {
        index: {
            chunks: ['index', ...Object.keys(SPLIT_CHUNKS)], // 声明块到index页面
        },
    },
    chainWebpack(config) {
        // 生成环境开启
        if (process.env.NODE_ENV !== 'development') {
            // 抽包
            config.optimization.splitChunks({
                cacheGroups: { ...SPLIT_CHUNKS } // 插入分块配置
            })
        } 
    }
}

全部抽离

回到 index.js 这个块依然很大的问题, 需要将其他较大 模块 依照优先级都拆分出来, 我的个人配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const SPLIT_CHUNKS = {
    'chunk-element-ui': { 'test': /[\\/]node_modules[\\/]element-ui/ },
    'chunk-vue': { 'test': /[\\/]node_modules[\\/]vue/ },
    'chunk-zrender': { 'test': /[\\/]node_modules[\\/]zrender/ },
    'chunk-mockjs': { 'test': /[\\/]node_modules[\\/]mockjs/ },
    'chunk-lodash': { 'test': /[\\/]node_modules[\\/]lodash/ },
    'chunk-vue-grid-layout': { 'test': /[\\/]node_modules[\\/]vue-grid-layout/ },
    'chunk-codemirror': { 'test': /[\\/]node_modules[\\/]codemirror/ },
    'chunk-highcharts': { 'test': /[\\/]node_modules[\\/]highcharts/ },
    'chunk-jsoneditor': { 'test': /[\\/]node_modules[\\/]jsoneditor/ },
    'chunk-jspdf': { 'test': /[\\/]node_modules[\\/]jspdf/ },
    'chunk-xlsx': { 'test': /[\\/]node_modules[\\/]xlsx/ },
    'chunk-echarts': { 'test': /[\\/]node_modules[\\/]echarts/ },
}

打包:

img

这次的基本拆分的非常细了, index.js 块的大小已经从 4.72MB 缩减到了 660.04KB, 质的提升, 同时我们看一下加载

img

虽然看上去访问的时间不太长, 时间线基本上都是很快响应, 主要是因为访问的是本地的网络和文件, 但是 echarts 模块依然是明显的过大, 依然拉长了响应处理的时间, 想要解决这个问题的办法就是继续拆包.

子包拆分

打开预览页发现 echarts地图JSON数据 依然占据了太多的空间, 以及xlsx也能继续优化, 那我们先针对 echarts 进行再切割

img

修改配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const SPLIT_CHUNKS = {
    'chunk-element-ui': { 'test': /[\\/]node_modules[\\/]element-ui/ },
    'chunk-vue': { 'test': /[\\/]node_modules[\\/]vue/ },
    'chunk-zrender': { 'test': /[\\/]node_modules[\\/]zrender/ },
    'chunk-mockjs': { 'test': /[\\/]node_modules[\\/]mockjs/ },
    'chunk-lodash': { 'test': /[\\/]node_modules[\\/]lodash/ },
    'chunk-vue-grid-layout': { 'test': /[\\/]node_modules[\\/]vue-grid-layout/ },
    'chunk-codemirror': { 'test': /[\\/]node_modules[\\/]codemirror/ },
    'chunk-highcharts': { 'test': /[\\/]node_modules[\\/]highcharts/ },
    'chunk-jsoneditor': { 'test': /[\\/]node_modules[\\/]jsoneditor/ },
    'chunk-jspdf': { 'test': /[\\/]node_modules[\\/]jspdf/ },
    'chunk-xlsx': { 'test': /[\\/]node_modules[\\/]xlsx/ },
    'chunk-echarts': { 'test': /[\\/]node_modules[\\/]echarts/ },
    // 地图单独拆分
    'chunk-echarts-map': { 'test': /[\\/]node_modules[\\/]echarts[\\/]map[\\/]json[\\/]province/ },
}
Object.keys(SPLIT_CHUNKS).forEach((name, priority) => Object.assign(SPLIT_CHUNKS[name], {
    name,
    priority, // 这里 ********************************************************
    enforce: true,
    reuseExistingChunk: true,
    chunks: SPLIT_CHUNKS[name].chunks || 'all', 
}))

这里的配置需要注意 priority(优先级) , 若一个 [CHUNK].test 有多个能命中的, 优先级高的会优先执行

代码中 priority 是根据下标大小决定优先级的, 所以 chunk-echarts-map 要高于 chunk-echarts 在优先级, 这样才能正确分包, 所以使用这段代码配置一定要注意配置顺序.

img

这样就完成了包的分离, 块变得更小

这里说一下为什么会有优先级, 当 reuseExistingChunk 设置为 true 的时候, 如果这个包已经打过, 其他规则就会忽略这个已经打过的包, 转为依赖他, 而 优先级 决定了规则匹配的顺序.

vendors

如何找回 vendors 呢? 其实这个是 webpack 保留的分组, 拷贝一份默认配置到 config.optimization.splitChunks 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const DEFAULT_CHUNKS = {
    common: {
        name: "chunk-common",
        chunks: "initial",
    },
    vendors: {
        name: "chunk-vendors",
        test: /[\\/]node_modules[\\/]/,
        chunks: "initial",
        // minChunks: Infinity, // 无穷的小块
    }
}
// ...
config.optimization.splitChunks({
    cacheGroups: {  ...DEFAULT_CHUNKS, ...SPLIT_CHUNKS } // 插入分块配置
})

默认的分块 优先级 不设置的话是 负数,

完成

至此完成了包的分块切割, 要根据自身的业务进行合理的拆分才能让页面加载速度更快.

提升

压缩

开启压缩可以进一步加快请求速度, 使用 compression-webpack-plugin 插件

安装到调试包列表

1
2
3
npm install compression-webpack-plugin --save-dev
// or
yarn add compression-webpack-plugin --dev

增加压缩插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const CompressionWebpackPlugin = require('compression-webpack-plugin')

module.exports = {
    configureWebpack: {
        plugins: [
            new CompressionWebpackPlugin({
                algorithm: 'gzip',
                test: new RegExp('\\.(' + ['js', 'css', 'html', 'json'].join('|') + ')$'),
                threshold: 10240,
                minRatio: 0.8,
                // deleteOriginalAssets: true
            }),
        ]
    },
}

修改nginx配置支持gzip传输

1
2
3
4
5
6
7
8
9
10
11
server {
    # ...

    gzip on;
    gzip_min_length 1k;
    gzip_comp_level 9;
    gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
    gzip_vary on;
    gzip_disable "MSIE [1-6]\.";

}

简单配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module.exports = {
    pages: {
        index: {
            chunks: [
                'index', // >>> 这是基本页面块, 不能丢掉
                'chunk-element-ui',  // 这里是刚刚的块的名称, 要对应填写
            ],
        },
    },
    chainWebpack(config) {
        // 生成环境开启
        if (process.env.NODE_ENV !== 'development') {
            // 抽包
            config.optimization.splitChunks({
                cacheGroups: {
                    // 第三方块抽离 ****************************************************************
                    "chunk-element-ui": {
                        name: "chunk-element-ui",
                        test: /[\\/]node_modules[\\/]element-ui/, // 匹配所有element-ui相关的包
                        chunks: "all",
                        reuseExistingChunk: true,
                        enforce: true
                    },
                }
            })
        } 
    }
}

参数说明

  • pages.[PAGE].chunks: 手动分块后需要说明页面的引用块有哪些
  • optimization.splitChunks.cacheGroups.[CHUNK]
    • name: 块的名称. 用于在 pages.[PAGE].chunks 内声名块的使用
    • test: 块的匹配规则. 包含三种类型的匹配方式
      • function: 方法匹配. 返回一个布尔值用于确认是不是要分到当前的块内, 格式: function (module, chunk) => boolean
      • RegExp: 正则匹配.
      • String: 直接字符匹配(完全匹配).
    • chunks: 包含的块的类型. 可以使 all | async | initial, 这里不多做解释自行了解即可, 个人推荐只使用 all
    • reuseExistingChunk: 是否重用现有的块. 若不重用可能会导致多页场景或多个位置导入包的时候, 打出多个类似的包来, 这样会较浪费客户机流量重复下载相同的包.
    • enforce: 是否强制执行. 必须开启, 如果不开启强制执行, 在某些情况下你的 cacheGroups[CHUNK] 打包配置并不能生效, 你会发现明明写了配置, 包还是没切出来
    • priority : 优先级, 数字越大优先级越高. 这个比较重要, 如果你需要对单个包抽离成多个子包, 例如 echarts 抽离成 echart-mapechart-component 等等, 优先级是需要最先考虑的配置

[CHUNK].test

对于 [CHUNK].test 的正则可以通过拷贝包路径后在控制台反复练习, 正则也是开发不可缺少的必要技能, 建议勤加练习.

1
2
/[\\/]node_modules[\\/]element-ui/.test('/node_modules/element-ui')
// true

如果是有太多依赖的工程, 不建议将 [CHUNK].test 用方法的形式, 会有大量的日志吐出到命令行, 会崩溃的.

技巧

optimization.splitChunks.cacheGroups.[CHUNK].chunks 的参数

因为 VueCli 在一直升级, Webpack 也在升级, 所以打包体验越来越好, 针对老的项目优化比较费劲, 新的版本无需过多配制打包后反而很合理, 例如 echarts 导入地图的时候, 就自动就完成了切分, 但是在 VueCli3 中地图会被包含到 echarts 中, 所以打包参数还是需要合理调整的, 教程的建议中是都使用 all, 实际上在新版 VueCli4 中使用 initial 便会自动拆分地图, 而且更加合理, 所以要根据不同项目的工具依赖 包使用情况合理进行配置.