记一次Vue项目打包优化

接手了一个移动端Vue项目,由于历史原因,整个项目打包速度和页面加载速度都比较慢,本文记录了优化该项目的一些工作。之前整理过一篇webpack折腾记(四):性能优化,本文可以算作该篇文章的一次实践。

<!--more-->

1. 项目背景

整个项目存在下面两个问题。

第一个问题是:打包体积很大,原始JS代码3M多~,gzip后也要400多个k,此外css文件gzip后近160k,

另外发现这个项目部署的时候甚至没有配置CDN缓存~相当于进入单次进入页面就需要加载很多静态资源,部署时间和用户访问静态资源加载时间都比较长,因此需要优化第三方依赖的打包。

第二个问题是:整个项目使用的是vue cli2,整个打包脚本的运行,在生产机器上居然要跑两分钟左右,单单是webpack打包都要花费四五十秒

基于上面两个问题,每次部署都是一场噩梦,点击发布后需要部署很长时间,页面加载速度也很慢,急需优化

2. 减少代码体积

除了常规的异步路由等操作之外,还从下面几个方面优化代码体积

2.1. 减少base64图片内联

由于代码中很多地方直接在js中使用require('xxx.png')的方式引入图片,小图片会打包成base64内联图片,导致打包后的JS文件体积较大,且很难被gzip压缩

一种办法是将url-loader的尺寸限制参数调低,减少内联图片的数量,但是这会导致部分小图片各自都需要走单独的HTTP请求,是一种得不偿失的做法。

对于小图片而言,更常见的做法是使用精灵图,将多张小图标图片合并成一张整图,然后走单独的HTTP请求。使用精灵图有两个注意事项

  • 移动端一般会使用rem等屏幕适配,如果使用rem单位来控制background-positon属性,但还是有可能遇见"精灵图背景错位"的问题,可以使用百分比来配置background-positon
  • 如果将现有图标重构为精灵图,成本也不小

还是使用字体图标比较好,但是也存在开发成本(突然发现上一次在项目中使用字体图标,貌似是两年以前的事情了~有点怀念)

所以最后的解决办法是:把所有使用的图标都进行了压缩,这个项目之前的图标都是直接使用设计提供的源文件,没有进行任何压缩,导致转成的base64体积更大了。将所有图片进行压缩带来的一个后续问题是:小体积的图标变得更多了(/捂脸),所以最后内联打包到JS文件中的体积并没有很明显地减少;但是就整体图片数量而言,节省了很多稍大图片的HTTP请求。

2.2. Element UI

由于历史原因,这个移动端项目中居然使用了Element UI,并且还是全局引入的,但实际上只使用了一小部分功能,所以这一块是优化的重点。

首先将Vue.use(ElementUI)删掉

然后,需要找到使用了哪些组件,这是个体力活,大概就是全局查询使用<el-组件的文件,以及使用了this.$messagethis.$confirm等接口的地方

接着将找到的组件按需引入,为了避免挨个文件去修改,偷了个懒,统一将使用到的组件的引入放在一个独立的入口文件里面

// registerComponent.js
import {Row,Col,Button,Tabs,TabPane,Input,Collapse,CollapseItem,Car,Progress,Message} from "element-ui";

let components = [Row,Col,Button,Tabs,TabPane,Input,Collapse,CollapseItem,Car,Progress]

components.forEach(comp => {
  Vue.component(comp.name, comp)
})
Vue.prototype.$message = Message;

准后后面逐步将用到的组件如RowCol等移除,到时候再从此处移除对于ElementUI的依赖。

最后,根据官网的按需引入文档,安装babel-plugin-component插件,配置.babelrc即可。

由于还是需要一些全局样式和公共代码,所以按需加载并没法实现精准地只加载对应模块的功能。经过上面的步骤,可以将ElementUI的体积缩小到300kb左右。

2.3. moment

由于项目需要一些日期处理的工具函数,于是直接引入了moment,众所周知,moment的多语言本地化locale目录占用的体积相当庞大,但是对该项目而言基本上没有使用。所以,优化moment的第一步就是将他的多语言干掉,这个可以使用webpack.IgnorePlugin实现

在webpack的插件配置项添加

plugins: [
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]

即使是去掉了locale目录,moment在未压缩的时候也有140kb左右的大小,就一个工具函数库而言,这个体积有点太大了。一种处理方法是使用[https://www.npmjs.com/package/dayjs]来替代moment。

由于dayjsmoment存在相同的API,可以直接通过alias实现将moment替换成dayjs,可以迅速将140kb的moment替换成6kb的dayjs。

resolve: {
    alias: {
        'moment': 'dayjs'
    }
},

2.3.1. 异步组件

由于某个组件依赖了codemirror这个库实现功能,导致整个依赖大概增加了400多kb。考虑到由于目前只有该组件依赖了codemirror,所以可以通过注册异步组件的形式减少主包的文件体积

Vue.component('codePreview', () => ({
  component: import('@/xxx/code-preview'),
  delay: 50,
  timeout: 6000
}))

这样只有当需要该组件的时候,才会加载对应的分包。

后续:由于异步组件会导致用户体验不太好,然后又改回同步组件了。对于这种比较大的依赖库,除了一股脑打包到vendor.js文件中,还可以通过script标签直接加载CDN文件,然后通过externals来减少依赖大小。

3. 打包速度优化:升级vue-cli

使用speed-measure-webpack-plugin测量了一下打包速度

上图是在我本地i7 16G内存的MBP上打包的速度截图,真实服务器比这个性能要差一些

从上面的截图可以看见,打包耗时主要集中在UglifyJsPluginsass-resources-loadervue-loaderpx2rem-loader上,由于vue-cli2使用的是webpack3,如果单独升级webpack,会带来一系列的改动,不如直接升级到vue-cli4

准备工作:

  • 安装vue-cli最新版,vue -V目前最新的版本是@vue/cli 4.1.1
  • 由于项目多个分支的node_moduels是公用的,所以最好重新拉一个项目进行升级操作,避免升级过程中插入的一些紧急需求。

首先切换到项目所在目录,然后运行vue create projectName,会提示目录已存在,然后选择Merge合并

合并后新的package.jsonreadme.md、还有一些公共文件会被vue-cli4覆盖,整个升级需要进行两个部分

第一个任务是恢复被覆盖的依赖,需要将旧项目中的dependencies,以及一些devDependenciespostcss-px2rem-exclude等依赖,添加到新的package.json中,下图是迁移后package.json发生的一些变化。

在升级时还发现了一些很少被用到的模块如stylus,只有一个vue文件使用到,然后就直接将它干掉了~

然后使用yarn install安装依赖,由于之前项目也配置了eslint,因此整个项目继续使用之前的.eslintrc.js即可

第二个任务是将vue-cli2中修改的一些webpack配置迁移到vue.config.js中,包括新增的loaderplugins,此外,环境变量需要修改到.env.dev等文件中,参考环境变量

至此整个升级过程还算比较顺利。最后,记得在部署的时候,需要先删掉之前的node_modules,然后重新安装。

升级升级到vue-cli4后附带的一个功能是可以使用vue ui了,这样可以更方便地启动项目、分析打包。

整个打包速度的提升是十分明显的,那么问题来了:为什么升级后的打包速度会快这么多呢?后面会整理一篇关于vue-cli的源码分析文章~

4. 小结

本文主要记录了一个Vue项目的打包优化

  • 通过升级vue-cli优化打包速度
  • 通过图片优化、按需加载等方式减少打包后的文件体积

代码优化并配置完Nginx缓存之后,整个应用的开发体验和用户体验有了非常明显的提升,

  • 现在基本上二十来秒就可以完成整个应用的部署,
  • 手机浏览器首次首屏打开速度在1s以内

比起刚接手的时候要好一些了。此外还剩一些TODO的工作,如移除所有ElementUI、重构业务逻辑等工作需要进行,后面边开发优化啦~

(PS:实现了oPic之后,写博客放图方便多了~