3

工作中一直在做一款公司内部的BI工具,将数据可视化的报表赋能给业务人员,报表配置者通过简单的拖拽操作即可生成报表。随着系统不断的完善,加上运维推广,我们积累了越来越多的用户。这时候用户体验的方方面面都体现出来了。我们也停下产品的功能迭代,将整个系统进行优化,旨在提升用户体验。以下是我对前端项目的优化总结。

Webpack 打包优化

项目中在使用的 Webpack 版本是3.x,本次优化的方案仍然是基于Webpack3.x版本的 Vue 脚手架进行优化。升级4.x在计划中。。。

之前也总结过一次 Webpack 2.x 在Vue2.x项目中的应用,提到过 Webpack 工程的一些优化方案,以下算是一个补充。

开启Gzip

尝试了下开启gzip,直接受益还是比较大的。下面是实际项目中打包结果。

  • Parsed的js,1.38M

parsed-js

  • Gizpped的js - 421.46K

gzipped-js

Webpack__Gzipped_

通过数据分析,减少了70.28%的打包体积。

开启方式,在脚手架中修改配置文件:/config/index.js

// 生产模式
build: {
  productionGzip: true // 开启Gzip压缩
}

同时服务端 nginx 加入配置项

gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 6;
gzip_types application/javascript text/plain application/x-javascript text/css application/xml text/javascript application/json;
gzip_vary on;

重启 nginx 后刷新页面,在Chrome develop toolsNetwork 查看网络链接

Request Headers 中出现 Accept-Encoding: gzip 代表客户端能够理解 gzip 压缩编码方式

gzip_network

Response Headers 中出现 Content-Encoding 代表服务端指明以 gzip 编码方式对数据进行压缩

gzip_network

这一对请求头部关键字搭配出现,说明配置成功。

使用 Preload 插件

preload-webpack-plugin

💡 使用 Resource Hints 中的 preloadprefetch 来提升应用的性能。

关于 preloadprefetch

<link rel="preload"> 是一种 resource hint,用来指定页面加载后很快会被用到的资源,所以在页面加载的过程中,我们希望在浏览器开始主体渲染之前尽早 preload。

<link rel="prefetch"> 是一种 resource hint,用来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。

在 Webpack 中配置 preload

preload-webpack-pluginhtml-webpack-plugin 插件的一个扩展,所以需要搭配使用。

例如配置 preload:

plugins: [
  new HtmlWebpackPlugin(),
  new PreloadWebpackPlugin({
    rel: 'preload',
    as(entry) {
      if (/\.css$/.test(entry)) return 'style';
      if (/\.woff$/.test(entry)) return 'font';
      if (/\.png$/.test(entry)) return 'image';
      return 'script';
    },
    include: ['app']
  })
]

最终在html注入为:

<link rel="preload" as="script" href="app.31132ae6680e598f8879.js">

在 Webpack 中配置 prefetch

prefetch 配合 Vue 中的路由懒加载代码分割更好用

因为本项目可视化工具中没有使用路由,没有配置prefetch

优化package

目前项目中比较常用的工具类库有 lodash、moment、element-ui,对于这些经常使用的类库可以通过 Dllplugin 分离依赖成一个静态资源库。一般不会去改动这个依赖包版本。

不过像lodash、moment是有其他方法来减少打包体积的。

  • 按需加载 element-ui,见官方文档
  • 按需加载 lodash

一般我们使用 lodash 时,不会用到其中所有的函数。有可能用到了几个,这时候可以选择按需引入 lodash,不要引入全量。下面通过安装两个插件:

npm i babel-plugin-lodash lodash-webpack-plugin -D

配置 .babelrc 文件

"plugins": [
  "lodash"
]
  • 使用 dayjs 代替 moment,API基本一样,使用后会发现大部分场景都能使用,而且打包只有 7KB

升级 HTTP2

可视化工具中组件变得越来越丰富,随之带来的页面请求数据接口也逐渐变多,开销在逐渐增大。单个页面数据接口请求几十上百不等。

如果继续使用HTTP1.x,大家都懂的,HTTP1.x协议的局限性,大多数现代浏览器都支持同时一个主机最大请求数量为6个,也就是说,如果这6个接口请求没有返回结果处于pending状态的话,页面就一直刷不出数据,这样给用户的体验是很差的。HTTP2的多路复用解决了这个问题,我们通过将服务器升级为 HTTP2 增大了浏览器请求连接吞吐量,大大提升了应用的性能。

HTTP2 简介

HTTP2.0 可以让我们的应用更快、更简单、更健壮

          --- 《Web性能权威指南》

HTTP 2.0 的目的就是通过支持请求与响应的多路复用来减少延迟,通过压缩 HTTP 首部字段将协议开销降至最低,同时增加对请求优先级和服务器端推送的支持。

HTTP 2.0 性能增强的核心,全在于新增的二进制分帧层,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。

HTTP 2.0 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。相应地,很多流可以并行地在同一个 TCP 连接上交换消息。

HTTP 2.0 的二进制分帧机制解决了 HTTP 1.x 中存在的队首阻塞问题, 也消除了并行处理和发送请求及响应时对多个连接的依赖。结果,就是应用速度更快、开发更简单、部署成本更低。

HTTP2 优化

  • 域名分区在 HTTP 2.0 之下属于反模式,因为多个连接会抵消新协议中首部压缩和请求优先级的效用
  • 去掉不必要的资源打包,例如生成雪碧图,支持了 HTTP 2.0,很多小资源都可以并行发送,导致打包资源的效率反而更低
  • 使用客户端缓存应用资源
  • 部署 HTTP 2.0 的同时部署TLS协议(传输层安全协议),即HTTPS

使用 HTTP 缓存

缓存应用资源,避免每次请求都发送相同的内容。浏览器在下载静态资源后,使用缓存将下载过的资源维护好,这样下次加载网页时直接使用本地的副本。减少了资源请求以及等待时间。

Cache-Control

通用的HTTP请求头首部字段,只需指定一个明确的缓存时间即可。可以配置在 nginx 配置文件里。

location ~ .*\.(js|css|ttf|svg|ico){
    add_header Cache-Control  max-age=86400;
}

页面第一次加载

缓存前

再次加载

缓存后

缓存验证

缓存验证

可以看到加入缓存后,Status Code 为 200 OK (from memory cache),缓存时间为:max-age=86400

Vue 批量渲染组件

业务场景中,随着应用变得越来越复杂,加载一个页面可能需要渲染过多的组件,渲染多个组件有两种策略:

  • 遍历所有组件,每一个接口请求返回数据时去渲染组件
  • 请求所有接口,所有数据返回时批量渲染组件

通过实践发现,后者渲染更快,后者消除了每次请求接口之后渲染组件的时间,因为多次渲染组件会带来额外的Scripting开销,比如Vue中的 computedwatch;同时结合 HTTP2 的多路复用,请求多个接口也会很快的响应。

示例代码:

// 批量更新组件方法
batchUpdateComponent({ dispatch }, promises) {
  // 请求所有接口
  return Promise.all(promises.map(p => p.catch(() => undefined)))
    .catch(err => {
      console.log(err)
    })
    .then(res => {
      // 一次性渲染组件
      res && dispatch('updateComponent', res)
    })
}
💡 如果 Promise 的 catch 回调返回了 undefined,那么 Promise 的失败就会被当做成功来处理。
使用 ES2018 的提案 Promise.finally

Vue 异步组件

项目中应用业务代码量在不断攀升,写了很多业务组件,其实在一定场景下,并非所有组件都需要渲染,比如,可视化工具有编辑模式和预览模式。编辑模式需要使用 Code Mirror 用来编写一些 SQL 语句,预览模式时候就不需要使用。

组件正常引入:

import CustomSql from '@/components/CustomSql'

export default {
  components: {
    CustomSql
  }
}

组件异步引入:

// ES6 结合 Webpack 
export default {
  components: {
    CustomSql: () => import('./CustomSql')
  }
}
Vue中路由懒加载就是使用异步组件Webpack代码分割功能实现的。

SVG优化

随着项目中组件的增多,组件的icon随之也变的多了。大部分icon是svg格式,我们可以使用 SVG Sprite 技术管理SVG图标。

SVG Sprite 技术

所谓 SVG Sprite 类似于CSS中的Sprite技术。将图标整合在一起,实际呈现的时候准确显示特定图标。

SVG Sprite 技术最佳实践是:

  • 使用 symbol 元素整合图标
  • 使用 use 元素来使用图标

使用例子:

<svg>
    <!-- symbol definition  NEVER draw -->
    <symbol id="sym01" viewBox="0 0 150 110">
      <circle cx="50" cy="50" r="40" stroke-width="8" stroke="red" fill="red"/>
      <circle cx="90" cy="60" r="40" stroke-width="8" stroke="green" fill="white"/>
    </symbol>
    
    <!-- actual drawing by "use" element -->
    <use xlink:href="#sym01"
         x="0" y="0" width="100" height="50"/>
    <use xlink:href="#sym01"
         x="0" y="50" width="75" height="38"/>
    <use xlink:href="#sym01"
         x="0" y="100" width="50" height="25"/>
</svg>

组件化 SvgIcon

基于Vue封装的 SVG ICON 组件

// @/components/SvgIcon.vue
<template>
  <svg :class="svgClass" aria-hidden="true" v-on="$listeners">
    <use :xlink:href="iconName" />
  </svg>
</template>
    
<script>
export default {
  name: 'SvgIcon',
  props: {
    iconClass: {
      type: String,
      required: true
    },
    className: {
      type: String,
      default: ''
    }
  },
  computed: {
    iconName() {
      return `#icon-${this.iconClass}`
    },
    svgClass() {
      return 'svg-icon ' + this.className
    }
  }
}
</script>
    
<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>

自动化引入 SVG

将 src/assets/icons 下所有icon动态引入

// @/plugins/svgicon.js
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon'
    
Vue.component('svg-icon', SvgIcon)
    
const requireAll = requireContext => requireContext.keys().map(requireContext)
    
const svgIcons = require.context('./components', false, /\.svg$/)
requireAll(svgIcons)

打包 SVG Sprite

我们可以用 svg-sprite-loader 这个插件来生成 SVG Sprite,通过组件的方式引入 svg icon。

基于 Webpack 3.x 的配置方法如下:

// 通过 exclude/include 来区分哪些属于svg icon,哪些属于image
{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  loader: 'url-loader',
  exclude: [resolve('src/assets/icons')],
  options: {
    limit: 10000,
    name: utils.assetsPath('img/[name].[hash:7].[ext]')
  }
},
{
  test: /\.svg$/,
  loader: 'svg-sprite-loader',
  include: [resolve('src/assets/icons')],
  options: {
    symbolId: 'icon-[name]'
  }
}

总结

本次性能优化关键点:

Webpack方面:

  • 开启Gzip,直接收益比较大
  • 使用preload插件,预先声明要使用到的资源
  • 尽可能优化package,做到按需加载,减少打包体积

网络方面:

  • 升级服务器为HTTP2,结合HTTPS是最佳实践
  • 使用 HTTP 缓存策略,最好的性能是不用请求

Vue实践方面:

  • 渲染组件时机,建议在全部接口请求返回后去批量渲染
  • 将不常用的特定场景下使用的组件写成异步组件

资源方面:

  • 项目中使用较多SVG时,可以选择使用“SVG Sprite”技术管理

最后

项目初始,由于工期紧张,我们急着迭代功能,目标是交付功能完备的应用,用户量增长的时候就该停下来好好考虑考虑如何提升应用的性能了。纵使应用的功能再完备,如果用户体验非常差,那是不是值得反思,性能优化是一件需要持续做的事情。

我想借用一下《Web性能权威指南》里,Ilya Grigorik 提到的:“💡我们关心的不止是交付能用的应用,我们目标是交付最佳性能!” 来总结性能优化的实践,同时提醒自己,在做项目的时候尽可能的提前想到性能优化的点。

参考

《Web性能权威指南》
原文🚀 记一次前端性能优化

xiaoluoboding
2.1k 声望503 粉丝

I 💗 Web Dev, 💻 I'm a Senior Frontend Engineer