工作中一直在做一款公司内部的 BI 工具,将数据可视化的报表赋能给业务人员,报表配置者通过简单的拖拽操作即可生成报表。随着系统不断的完善,加上运维推广,我们积累了越来越多的用户。这时候用户体验的方方面面都体现出来了。我们也停下产品的功能迭代,将整个系统进行优化,旨在提升用户体验。以下是我对前端项目的优化总结。
Webpack 打包优化
项目中在使用的 Webpack
版本是 3.x,本次优化的方案仍然是基于 Webpack3.x 版本的 Vue
脚手架进行优化。升级 4.x 在计划中。。。
之前也总结过一次 Webpack 2.x 在 Vue2.x 项目中的应用,提到过 Webpack
工程的一些优化方案,以下算是一个补充。
开启 Gzip
尝试了下开启 gzip,直接受益还是比较大的。下面是实际项目中打包结果。
-
Parsed
的 js,1.38M
-
Gizpped
的 js – 421.46K
通过数据分析,减少了 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 tools
中 Network
查看网络链接
Request Headers
中出现 Accept-Encoding: gzip
代表客户端能够理解 gzip
压缩编码方式
Response Headers
中出现 Content-Encoding
代表服务端指明以 gzip
编码方式对数据进行压缩
这一对请求头部关键字搭配出现,说明配置成功。
使用 Preload 插件
???? 使用
Resource Hints
中的 preload 与 prefetch 来提升应用的性能。
关于 preload
与 prefetch
<link rel="preload">
是一种 resource hint,用来指定页面加载后很快会被用到的资源,所以在页面加载的过程中,我们希望在浏览器开始主体渲染之前尽早 preload。
<link rel="prefetch">
是一种 resource hint,用来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。
在 Webpack 中配置 preload
preload-webpack-plugin
是 html-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 中的 computed
或 watch
;同时结合 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 性能权威指南》
- Resource Hints
- W3C Resource Hints
- Preload: What Is It Good For?
- Getting Ready For HTTP2
- HTTP 2.0 压缩算法
- Promise.all for Rejections and Resolves
- SVG Sprite 技术介绍
原文???? 记一次前端性能优化