vue2 splitchunk分包优化项目实战&技术分享

1.目标

结合vue2项目聊一下优化的思路。主要聊一下webpack分包方向。
项目环境:Vue2 + webpack4
项目结构:pc和mobile集成在一个项目中,PC端:element-ui + vue(部分页面会用到vant + jkUI),Mobile: vant + vue + jkUI
分析工具:Chrome、webpack-bundle-analyzer插件

2.前置工作

安装插件:

 npm i webpack-bundle-analyzer -D

配置脚本:

"analyze": "cross-env NODE_ENV=production ANALYZER=true vue-cli-service build"

修改配置(vue.config.js):

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
// ...省略部分代码
// chainWebpack方式
config
  .when(process.env.ANALYZER == 'true', config => {
    config.plugin('webpack-bundle-analyzer').use(BundleAnalyzerPlugin)
  })

// configureWebpack方式
process.env.ANALYZER == "true" && config.plugins.push(new BundleAnalyzerPlugin());

3.启动项目分析资源加载

 npm run dev
图1-f12查看network.png

简单分析上图,可以得出结论:
1.移动端加载了非必要资源:element-ui
2.vendors体积过大,导致后续加载阻塞
接下来先从这两个方向分析bundle问题

4.项目bundle分析

npm run analyze

以cms项目代码为例,执行以上命令分析


图2-初始bundle分布.png
图3-bundle大小.png

结合之前的结论和上面的bundle分布图,决定从下面几个方向进行优化:
1.分离jk-ui组件库
2.分离lodash库
3.分离moment库

5.开始优化

webpack4中主要通过splitchunk来进行分包操作

// 项目初始配置
splitChunks: {
  chunks: 'all',
  cacheGroups: {
    libs: {
      name: 'chunk-libs',
      test: /[\\/]node_modules[\\/]/,
      priority: 10,
      chunks: 'initial' // only package third parties that are initially dependent
    },
    elementUI: {
      name: 'chunk-elementUI',
      priority: 20, 
      test: /[\\/]node_modules[\\/]_?element-ui(.*)/, 
      enforce: true
    },
    echarts: {
      name: 'chunk-echarts', 
      priority: 12, 
      test: /[\\/]node_modules[\\/]_?echarts(.*)/
    },
    commons: {
      name: 'chunk-commons',
      test: resolve('src/components'), 
      minChunks: 3, 
      priority: 5,
      reuseExistingChunk: true
    }
  }
}

介绍一下all, initial, async(默认)区别:
all: 把动态和非动态模块同时进行优化打包;所有模块都扔到 vendors.bundle.js 里面
initial: 把非动态模块打包进 vendor,动态模块优化打包
async: 把动态模块打包进 vendor,非动态模块保持原样(不优化)

结合图2-bundle分布图分析修改splitchunk配置:

// 修改后配置
splitChunks: {
  chunks: 'all',
  cacheGroups: {
    libs: {
      name: 'chunk-libs',
      test: /[\\/]node_modules[\\/]/,
      priority: 10,
      chunks: 'all'
    },
    elementUI: {
      priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
      test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
      reuseExistingChunk: true
    },
    jkUI: {
      priority: 20, 
      test: /[\\/]node_modules[\\/]_?jk-ui(.*)/,
      maxSize: 460800, // gzip 150kb
      minSize: 245760, 
      reuseExistingChunk: true
    },
    lodash: {
      priority: 20, 
      test: /[\\/]node_modules[\\/]_?lodash(.*)/,
      reuseExistingChunk: true
    },
    moment: {
      priority: 20, 
      test: /[\\/]node_modules[\\/]_?moment(.*)/,
      reuseExistingChunk: true
    }
  }
}
图4-分包后bundle分布.png

图5-分包后bundle大小.png

6.根据bundle分析代码

对项目代码进行分析,可以看出一些明显的问题:


图6-lodash全局引入问题.png

图7-moment全局引入问题.png

图8-element-ui全局引入问题.png

1.lodash全局引入,改成按需加载
2.moment引入,可以采用day.js替换
3.element-ui是全局引入的,包体积有点偏大,改成按需加载

7.进一步优化

1.将引入lodash的地方改为按需引入,或者改成utils自己封装的方法

import { throttle } from 'lodash'
import { debounce } from 'lodash'
import { cloneDeep } from 'lodash'
// 改为
import throttle from 'lodash/throttle'
import debounce from 'lodash/debounce'
import cloneDeep from 'lodash/cloneDeep'
图9-优化后的lodash体积减小.png

重新运行分析后发现lodash体积非常小,并且没有单独分离出来,可以删除splitchunk中的lodash配置

2.将引入momentjs的地方,采用dayjs替换

// 去除moment引用并安装dayjs
moment().format('YYYY-MM-DD')
moment(date).isValid()
// 改为
dayjs().format('YYYY-MM-DD')
dayjs(date).isValid()

重新运行分析后momentjs包被换掉,总体体积减小,可以删除splitchunk中的moment配置


图10-优化moment使用后总体bundle体积减小.png

3.接下来优化element-ui部分

在这之前我们先看一下jk-ui部分的加载问题


图11-访问mobile对应的路由,加载了jk-ui组件库.png
图12-访问pc下的cms-page-list路由,也加载了jk-ui组件库.png

显然这个地方不是我们理想的情况,理想情况下应该是移动端才会加载jk-ui组件库,也就是按需加载,只有用到的页面才会加载对应的库,分析一下原因,应该是由于我们对于jk-ui配置的问题,当我们不设置chunks时,默认继承spilitChunks.chunks属性,也就是all,设置为all的chunks会被默认加载,现在我们改为async重新运行看看

jkUI: {
  priority: 20,
  test: /[\\/]node_modules[\\/]_?jk-ui(.*)/,
  maxSize: 460800,
  minSize: 245760,
  reuseExistingChunk: true,
  chunks: 'async' // 新增,之前为默认继承外层的all属性
}

重新运行后访问两个页面路由:


图13-重新访问mobile路由,正常加载jk-ui组件库资源.png
图14-重新访问pc端路由,jk-ui资源不再加载.png

通过上述实践,当动态加载的资源,设置成async之后被分离出来单独的chunk,只会在用到的地方才加载,设置成all则不行。

回到我们开始的话题,继续优化element-ui库,从上述图13中其实可以看到,mobile路由加载了element资源,其实这并不是我们所希望的,所以分两个点来优化element-ui。(重点:需要保证element-ui为动态加载,并且需要设置为async模式。)
-第一个目标:优化element-ui体积
-第二个目标:实现element-ui在pc端路由才加载

图15-原element-ui加载代码.png

查看项目代码,可以看出element-ui是全局加载的,会导致element-ui包体积很大,这一点可以通过按需引入来解决问题。从之前的bundle分析中大概可以看到element-ui的体积大概为159kb(gziped)

由于element-ui的范围较广,选取一个pc页面cms-page-list作为示例:

// 删除main.js全局加载
if (!isMobile()) {
  // require('element-ui/lib/theme-chalk/index.css')
  // const ElementUI = require('element-ui')
  // Vue.use(ElementUI, { size: 'small' })
} else {
  const sensors = require('@/utils/sensors').default
  Vue.prototype.$sensors = sensors
}
// babel.config.js配置按需引入,参考下element官网
[
  'component',
  {
    libraryName: 'element-ui',
    styleLibraryName: 'theme-chalk'
  }
]
// 页面级别按需引入element组件,页面中子组件引用的element组件也需要按相同方式引入
import { Input, Button, Form, Select, DatePicker, Table, TableColumn } from 'element-ui'
// 省略...
components: {
  ElInput: Input,
  ElButton: Button,
  ElForm: Form,
  ElSelect: Select,
  ElDatePicker: DatePicker,
  ElTable: Table,
  ElTableColumn: TableColumn
}
// 删除utils/decorator.js首行引用,否则会造成mobile页面引用关系,由于mobile页面中引用了decorator文件,会导致element被引入
import MessageBox from 'element-ui'
// 修改splitChunks中element-ui相关配置
elementUI: {
  priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
  test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
  reuseExistingChunk: true,
  chunks: 'async'  // 新增,之前为默认继承外层的all属性
}

优化后:


图16-优化element-ui后pc正常加载element-ui资源,且体积减小.png

图17-优化element-ui后mobile不加载element-ui资源.png

图18-优化后bundle分布图.png

图19-优化后chunk体积减小.png

从上图看出chunk-libs中有vant库,可以采用同样的方式,结合按需加载和async模式抽离,防止在pc端页面冗余加载vant。

// 新增
vant: {
  test: /[\\/]node_modules[\\/]vant[\\/]/,
  priority: 20,
  reuseExistingChunk: true,
  chunks: 'async'
}
图20-分离vant后bundle体积.png

优化后vant从chunk-libs中分离,且在引用到的页面中才会加载。


图21-分离vant后mobile正常加载vant库.png

图22-分离vant后pc不加载vant库.png

8.最终配置

splitChunks: {
  chunks: 'all',
  minChunks: 1,
  maxAsyncRequests: 30, // 最多30个请求
  maxInitialRequests: 30, // 最多首屏加载30个请求
  cacheGroups: {
    libs: {
      name: 'chunk-libs',
      test: /[\\/]node_modules[\\/]/,
      priority: 10
    },
    elementUI: {
      priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
      test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
      reuseExistingChunk: true,
      chunks: 'async'
    },
    jkUI: {
      priority: 20,
      test: /[\\/]node_modules[\\/]_?jk-ui(.*)/,
      maxSize: 460800,
      minSize: 245760,
      reuseExistingChunk: true,
      chunks: 'async'
    },
    vant: {
      test: /[\\/]node_modules[\\/]vant[\\/]/,
      priority: 100,
      reuseExistingChunk: true,
      chunks: 'async'
    }
}

9.总结:

1.element-ui不能直接引入,否则无法在分包后达到最优体积,直接import('element-ui')或者import ElementUI from 'element-ui'都会在最后打包生成chunk时生成包含element全量包,所以要采用页面级别引入组件的方式来做按需引入。
2.入口文件main.js中也不能通过import { MessageBox } from 'element-ui'Vue.prototype.$message = MessageBox方式挂在到Vue的原型上,否则也会导致生成的chunk包含element整个包。可以在app.vue文件中挂载

import MessageBox from 'element-ui'
// 省略...
created() {
  Vue.prototype.$MessageBox = MessageBox
}

3.路由懒加载的页面中,import xxx from 'xxx'可以看作动态导入。
4.import('xxx')为动态导入。

问题:
1.为什么vant会被分为多个chunk?
2.分离chunk会额外生成一个css,如何合并?

拓展:
js新特性Import Maps:https://mp.weixin.qq.com/s/6KV1Q-7Wvwb-8E81fTooWA

你可能感兴趣的:(vue2 splitchunk分包优化项目实战&技术分享)