Kwin Huang

不会搞艺术的程序员不是好设计师.

vue首屏加载速度优化

2019-08-12

背景

后台管理型项目就是页面多, 当页面多的时候, 首屏加载的静态资源(css/js文件)就会很多, 我们项目就在被客户投诉说首页加载速度太慢, 遂排查是什么原因, 到底是前端的问题还是后台的问题(判断后台的问题可以参考我的另一片文章TTFB时间过长)

初始加载的时候, 一共请求了195个资源, 传送了18.1M的数据, 一共用了3.3s 才加载完成, 经分析, 其中大部分是js文件

request

遂去网上寻找优化之道, 找到几种优化的思路

  1. vue-router路由懒加载
  2. 去除首页不必要的依赖(去除组件的全局引入)
  3. 手动引入模块库的方法
  4. 使用更轻量的工具库
  5. CDN优化
  6. nginx开启gzip
  7. ...

解决方法

1. vue-router路由懒加载

官方文档已经给出了对应的方法, 这里简单说说

vue 一共有三种方法实现按需加载

1.1. vue的异步组件

/* vue异步组件技术 */
{
  path: '/home',
  name: 'home',
  component: resolve => require(['@/components/home'], resolve),
},
{
  path: '/index',
  name: 'Index',
  component: resolve => require(['@/components/index'], resolve),
},
{
  path: '/about',
  name: 'about',
  component: resolve => require(['@/components/about'], resolve),
},

1.2. es的提案import()

// 下面3行代码,没有指定webpackChunkName,每个组件打包成一个js文件。
const Home = () => import('@/components/home')
const Index = () => import('@/components/index')
const About = () => import('@/components/about')
// 下面3行代码,指定了相同的webpackChunkName,会合并打包成一个js文件。 把组件按组分块
const Home =  () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/home')
const Index = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/index')
const About = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/about')

{
  path: '/home',
  name: 'home',
  component: Home,
},
{
  path: '/index',
  name: 'Index',
  component: Index,
},
{
  path: '/about',
  name: 'about',
  component: About,
},

注意: 官方文档有一次说明, 如果使用babel, 要安装一个 babel 的插件 syntax-dynamic-import 来让babel识别这个语法, 还有一个webpack 的配置, 在 output 中加个 chunkFilename

安装依赖

npm install --save-dev @babel/plugin-syntax-dynamic-import

并且在webpack的output中加入

chunkFilename: '[name].js'

1.3. webpack的 require, ensure()

/* 组件懒加载方案三: webpack提供的require.ensure() */
{
  path: '/home',
  name: 'home',
  component: r => require.ensure([], () => r(require('@/components/home')), 'demo')
},
{
  path: '/index',
  name: 'Index',
  component: r => require.ensure([], () => r(require('@/components/index')), 'demo')
},
{
  path: '/about',
  name: 'about',
  component: r => require.ensure([], () => r(require('@/components/about')), 'demo-01')
}

目前最常用的是第二种方法

2. 去除不必要的依赖(去除组件的全局引入)

大家看一看自己的 main.js 文件, 是不是有一些不需要注册在全局的组件, 但是为了方便省事, 而放在了全局, 如果发现使用的不多的, 可以去除全局的, 进行按需加载

import ImageComponent from 'COMMON/imageComponent'
import InfiniteLoading from 'COMMON/infiniteLoading'
import SearchDialog from 'COMMON/SearchDialog'
import BasicTable from 'COMMON/BasicTable'
import VueQriously from 'vue-qriously'

Vue.use(ImageComponent)
Vue.use(InfiniteLoading) // 可以去除
Vue.use(SearchDialog) // 可以去除
Vue.use(BasicTable)  // 可以去除
Vue.use(VueQriously)  // 可以去除

3. 手动引入模块库的方法

默认引入 ECharts 是引入全部的import * as ECharts from 'echarts' 我们只需要部分组件,只需引入自己需要的部分。

import VueECharts from 'vue-echarts/components/ECharts.vue'
import 'echarts/lib/chart/line'
import 'echarts/lib/chart/bar'
import 'echarts/lib/chart/pie'
import 'echarts/lib/component/title'
import 'echarts/lib/component/tooltip'
import 'echarts/lib/component/legend'
import 'echarts/lib/component/markPoint'

使用lodash尽量使用哪个方法 引入那个方法

// 在组件文件中一般是这样引入的:
import _ from 'lodash';

// 但是如果这个组件你只需要使用lodash其中的一个方法, 你可以这样引入
import cloneDeep from 'lodash/cloneDeep';
// 或者
const cloneDeep= require('lodash/cloneDeep');

当然你如果文件中使用了较多的lodash方法的情况下, 不太美观, 且并不方便. 那么我们可以借助于lodash-webpack-plugin, 去除未引入的模块, 需要和babel-plugin-lodash插件配合使用

// 1  安装插件
npm i -S lodash-webpack-plugin babel-plugin-lodash

// 2  修改 webpack.conf.js 文件
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');

plugins: [ new LodashModuleReplacementPlugin()]

// 3  .bablerc中配置
"plugins": ["transform-runtime","transform-vue-jsx","lodash"]

// 或者在webpack.conf.js的rules配置
{
  test: /\.(js|jsx)$/,
  loader: 'babel-loader',
  exclude: /node_modules/,
  include: [resolve('src'), resolve('test')],
  options: {
    plugins: ['lodash'],
  }
}

4. 使用更轻量级的工具库

moment是处理时间的标杆,但是它过于庞大且默认不支持tree-shaking,而且我们的项目中只用到了moment(), format(), add(), subtract()等几个非常简单的方法,有点大材小用,所以我们用 date-fns 来替换它,需要什么方法直接引入就行。

5. CDN优化

我们发现, Vue 全家桶以及 ElementUI 仍然占了很大一部分 vendors 体积,这部分代码是不变的,但会随着每次 vendors 打包改变 hash 重新加载。我们可以使用 CDN 剔除这部分不经常变化的公共库。我们将vue,vue-router,vuex,axios,jquery,underscore,使用CDN资源引入。国内的CDN服务推荐使用 BootCDN

首先我们要在 index.html 中, 添加 CDN 的相关代码

<html>
...
<link href="https://cdn.bootcss.com/element-ui/2.7.2/theme-chalk/index.css" rel="stylesheet">
  </head>
  <body>
    <div id="app"></div>
    <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
    <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script>
    <script src="https://cdn.bootcss.com/vue-router/3.0.4/vue-router.min.js"></script>
    <script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
    <script src="https://cdn.bootcss.com/element-ui/2.7.2/index.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore-min.js"></script>
  </body>
</html>

在 vue.config.js 中加入 webpack 配置代码,关于 webpack 配置中的 externals ,参考地址

configureWebpack: {
  externals: {
    'vue': 'Vue', // 左侧vue是我们自己引入时候要用的,右侧是开发依赖库的主人定义的不能修改
    'vue-router': 'VueRouter',
    'vuex': 'Vuex',
    'element-ui': 'ELEMENT',
    'axios': 'axios',
    'underscore' : {
      commonjs: 'underscore',
      amd: 'underscore',
      root: '_'
    },
    'jquery': {
      commonjs: 'jQuery',
      amd: 'jQuery',
      root: '

    }
  },
}

去除 vue.use() 相关代码 需要注意的是,通过 CDN 引入,在使用 VueRouter Vuex ElementUI 的时候要改下写法。CDN会把它们挂载到window上,因此不再使用Vue.use(xxx)

也不在需import Vue from 'vue', import VueRouter from 'vue-router' 等。

剔除全家桶和Element-ui等只有,剩下的需要首次加载 vendors 就很小了。

使用 CDN 的好处有以下几个方面

(1)加快打包速度。分离公共库以后,每次重新打包就不会再把这些打包进 vendors 文件中。

(2)CDN减轻自己服务器的访问压力,并且能实现资源的并行下载。浏览器对 src 资源的加载是并行的(执行是按照顺序的)。

6. 检查 Nginx 是否开启 gzip

如下图所示,开启了 gzip 后 js 的大小比未开启 gzip 的 js 小 2/3 左右,所以如果没开启 gzip ,感觉我们做的再多意义也不大,如何看自己的项目有没有开启 gzip,如下图所示,开启了 gzip,在浏览器的控制台 Content-Encoding 一栏会显示gzip,否则没有。 Nginx 如果开启 gzip,请自行搜索,或者叫服务端来开启。

webpack chorme