提起性能优化 很多人眼前浮现的面试经验是不是历历在目呢?反正,性能优化在我看来他永远是前端领域的热度之王。
先说一下性能优化的方案:
一、基础优化(代码以及编码规范)
1、v-if 和 v-show 区分使用场景 v-if=false时不渲染DOM,v-show会预渲染DOM,v-if 在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建
2、computed 和 watch 区分使用场景 computed:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch:更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
3、v-for 遍历必须为 item 添加 key,且避免同时使用 v-if 在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js 内部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值对比,较快地定位到 diff 。
v-for 比 v-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。
4、事件及销毁 Vue组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 也就是说,在js内使用addEventListener等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露
created() {
addEventListener('touchmove', this.touchmove, false)
},
beforeDestroy() {
removeEventListener('touchmove', this.touchmove, false)
}
5、长列表以及不需要数据劫持的场景 Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze() (冻结) 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。或者可以用 Object.seal() (密封)
export default {
data: () => ({
users: {}
}),
async created() {
const users = await axios.get("/api/users");
this.users = Object.freeze(users);
}
}
二、网络相关
1、CDN加速 大幅减少生产环境体积; CDN网络将用户请求重定向到就近服务节点; 因为CDN是另一个域名,可以单启一个TCP不占用服务器压力; 可以减少多余cookie发送; 在index.html中引入cdn资源
...
<body>
<div id="app">
</div>
<!-- built files will be auto injected -->
<script src="https://cdn.bootcss.com/vue/2.5.2/vue.min.js"></script>
<script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js"></script>
<script src="https://cdn.bootcss.com/vue-resource/1.5.1/vue-resource.min.js"></script>
</body>
...
修改 build/webpack base.conf.js
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
externals:{
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex':'Vuex',
'vue-resource': 'VueResource'
},
...
}
修改src/main.js src/router/index.js 注释掉import引入的vue,vue-resource
2、合理缓存 将长时间不改变的第三方库或者静态资源设置强缓存,max-age; 其他静态资源设置协商缓存;
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存),如果对缓存机制还不是了解很清楚的,可以参考作者写的关于 HTTP 缓存的文章《彻底弄懂HTTP缓存机制及原理》,这里不再赘述。
3、预处理 对于现代浏览器来说,可以给link标签添加preload,prefetch,dns-prefetch属性
preload:预加载相关资源,preload的资源会和页面需要的静态资源并行加载 prefetch:空闲加载,其他资源加载完空间时间加载; dns-prefetch:让浏览器提前对域名进行解析,减少DNS查找的开销,如果你的静态资源和后端接口不是同一个服务器的话,可以将考虑你后端的域名放入link标签加入dns-prefetch属性
4、HTTP2 如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同一域名的tcp连接数量是有限制的(chrome为6个)超过规定数量的tcp连接,则必须要等到之前的请求收到响应后才能继续发送,而http2则可以在一个tcp连接中并发多个请求没有限制,在一些网络较差的环境开启http2性能提升尤为明显
这里极力推荐在支持https协议的服务器中使用http2协议,可以通过web服务器Nginx配置,或是直接让服务器支持http2
nginx开启http2非常简单,在nginx.conf中只需在原来监听的端口后面加上http2就可以了,前提是你的nginx版本不能低于1.95,并且已经开启了https
性能优化标准
既然说性能优化,那他总得有一个公认的标准,这就是我们很多次听到的Lighthouse
在很多单位,都有着自己的性能监控平台,我们只需要引入相应的sdk,那么在平台上就能分析出你页面的存在的性能问题,大家是不是学的很神奇!
其实除了苛刻的业务,需要特殊的定制,大多数的情况下我们单位的性能优化平台本质上其实就是利用无头浏览器(Puppeteer)跑Lighthouse。
理解了我们单位的性能监控平台的原理之后,我们就能针对性的做性能优化,也就是面向Lighthouse编程
Lighthouse
lighthouse 是 Google Chrome 推出的一款开源自动化工具,它可以搜集多个现代网页性能指标,分析 Web 应用的性能并生成报告,为开发人员进行性能优化的提供了参考方向。
说起Lighthouse 在现代的谷歌浏览器中业已经集成
他可以分析出我们的页面性能,通过几个指标
Lighthouse 会衡量以下性能指标项:
- 首次内容绘制首次内容绘制(First Contentful Paint)。即浏览器首次将任意内容(如文字、图像、canvas
等)绘制到屏幕上的时间点。 - 可交互时间可交互时间(Time to
Interactive)。指的是所有的页面内容都已经成功加载,且能够快速地对用户的操作做出反应的时间点。 - 速度指标速度指标(Speed
Index)。衡量了首屏可见内容绘制在屏幕上的速度。在首次加载页面的过程中尽量展现更多的内容,往往能给用户带来更好的体验,所以速度指标的值约小越好。 - 总阻塞时间总阻塞时间(Total Blocking Time)。指First Contentful Paint 首次内容绘制
(FCP)与Time to Interactive 可交互时间 (TTI)之间的总时间 - 最大内容绘制最大内容绘制(Largest Contentful Paint)。度量标准报告视口内可见的最大图像或文本块的呈现时间
- 累积布局偏移累积布局偏移(# Cumulative Layout
Shift)。衡量的是页面整个生命周期中每次元素发生的非预期布局偏移得分的总和。每次可视元素在两次渲染帧中的起始位置不同时,就说是发生了LS(Layout Shift)。
在一般情况下,据我的经验,由于性能监控平台的和本地平台的差异,本地可能要达到70分,线上才有可能达到及格的状态,如果有性能优化的需求时,大家酌情处理即可(不过本人觉得,及格即可, 毕竟大学考试有曰:60分万岁,61分浪费,传承不能丢,咱们要把更多的时间,放到更重要的事情上来!)
通用常规优化手段
lighthouse的的牛x之处就是它能找出你页面中的一些常规的性能瓶颈,并提出优化建议,比如:
于是针对这些优化建议,我们需要做一些常规的优化:
- 减少未使用的javascript
- 移出阻塞渲染的资源
- 图片质量压缩
- 限制使用字体数量,尽可能少使用变体
- 优化关键渲染路径:只加载当前页面渲染所需的必要资源,将次要资源放在页面渲染完成后加载
通用性能优化分析
我们知道lighthouse 中有六个性能指标,而在这六个指标中,LCP、 FCP、speed index、 这三个指数尤为重要,因为在一般情况下 这个三个指标会影响 TTI、TBT、CLS 的分数
所以在我们在优化时, 需要提高LCP、 FCP和speedIndex 的分数,经过测试, 即使是空页面也会有时间上的损耗, 初始分数基本都是0.8秒
注意: 需要值得大家注意的是,我们当前所有测试全部建立在,移动端(之所以用移动端,是由于pc 的强大算力,很少有性能瓶颈)的基础上,并且页面上必须有一下内容,才能得出分数,内容必须包括一下的一种或者多种
- 内嵌在svg元素内的image元素
- video元素(使用封面图像)
- 通过url()函数(而非使用CSS 渐变CSS 渐变)加载的带有背景图像的元素
- 包含文本节点或其他行内级文本元素子元素的块级元素块级元素块级元素。
否则就会有如下错误
接下来我们就从LCP、 FCP和speedIndex 这三个指标入手
FCP(First Contentful Paint)
顾名思义就是首次内容绘制,也就是页面最开始绘制内容的时间,但是由于我们现在开发的页面都是spa应用,所以,框架层面的初始化是一定会有一定的性能损耗的,以vue-cli 搭建的脚手架为例,当我初始化空的脚手架,打包后上传cdn部署,FCP 就会从0.8s提上到1.5秒,由此可见vue 的diff 也不是免费的他也会有性能上的损耗
在优化页面的内容之前我们声明三个前提
- 提高FCP的时间其实就是在优化关键渲染路径关键渲染路径
- 如果它是一个样式文件(CSS文件),浏览器就必须在渲染页面之前完全解析它(这就是为什么说CSS具有渲染阻碍性)
- 如果它是一个脚本文件(JavaScript文件),浏览器必须: 停止解析,下载脚本,并运行它。只有在这之后,它才能继续解析,因为
JavaScript 脚本可以改变页面内容(特别是HTML)。(这就是为什么说JavaScript阻塞解析)
针对以上的用例测试,我们发现,无论我们怎么优化,框架本身的性能损耗是无法抹除的,我们唯一能做的就是让框架更早的去执行初始化,并且初始化更少的内容,可做的优化手段如下:
- 所有初始化用不到的js 文件全部走异步加载,也就是加上defer或者asnyc
,并且一些需要走cdn的第三方插件需要放在页面底部(因为放在顶部,他的解析会阻止html 的解析,从而影响css 等文件的下载,这也是雅虎军规的一条) - js 文件拆包,以vue-cli 为例,一般情况下我们可以通过cli的配置 splitChunks
做代码分割,将一些第三方的包走cdn,或者拆包。如果有路由的情况下将路由做拆包处理,保证每个路由只加载当前路由对应的js代码 - 优化文件大小 减少字体包、css文件、以及js文件的大小(当然这些 脚手架默认都已经做了)
- 优化项目结构,每个组件的初始化都是有性能损耗的,在在保证可维护性的基础上,尽量减少初始化组件的加载数量
- 网络协议层面的优化,这个优化手段需要服务端配合纯前端已经无法达到,在现在云服务器盛行的时代,自家单位一般都会默认在云服务器中开启这些优化手段,比如开启gzip,使用cdn
等等
其实说来说去,提高FCP 的核心只有理念之后两个 减少初始化视图内容和 减少初始化下载资源大小
LCP(Largest Contentful Paint)
顾名思义就是最大内容绘制, 何时报告LCP,官方是这样说的
为了应对这种潜在的变化,浏览器会在绘制第一帧后立即分发一个largest-contentful-paint类型的PerformanceEntryPerformanceEntry,用于识别最大内容元素。但是,在渲染后续帧之后,浏览器会在最大内容元素发生变化时分发另一个PerformanceEntryPerformanceEntry。
例如,在一个带有文本和首图的网页上,浏览器最初可能只渲染文本部分,并在此期间分发一个largest-contentful-paint条目,其element属性通常会引用一个<p> 或<h1> 。随后,一旦首图完成加载,浏览器就会分发第二个largest-contentful-paint条目,其element属性将引用<img> 。
需要注意的是,一个元素只有在渲染完成并且对用户可见后才能被视为最大内容元素。尚未加载的图像不会被视为"渲染完成"。 在字体阻塞期字体阻塞期使用网页字体的文本节点亦是如此。在这种情况下,较小的元素可能会被报告为最大内容元素,但一旦更大的元素完成渲染,就会通过另一个PerformanceEntry对象进行报告。
其实用大白话解释就是,通常情况下,图片、视频以及大量文本绘制完成后就会报告LCP
理解了这一点,的优化手段就明确了,尽量减少这些资源的大小就可以了,经过测试,减少首屏渲染的图片以及视频内容大小后,整体分数显著提高,提供一些优化方法:
- 本地图片可以使用在线压缩工具自己压缩
- 接口中附带图片,一般情况下单位中都有对应的oss或者cdn传参配置通过地址栏传参方式控制图片质量
- 图片懒加载
SpeedIndex(速度指数)
Speed Index采用可视页面加载的视觉进度,计算内容绘制速度的总分。为此,首先需要能够计算在页面加载期间,各个时间点“完成”了多少部分。在WebPagetest中,通过捕获在浏览器中加载页面的视频并检查每个视频帧(在启用视频捕获的测试中,每秒10帧)来完成的,这个算法在下面有描述,但现在假设我们可以为每个视频帧分配一个完整的百分比(在每个帧下显示的数字)
以上是官方解释的计算方式,其实通俗的将,所谓速度指数就是衡量页面内容填充的速度
一图胜千言
经过测试,跟LCP相同,图片以及视频内容对于SpeedIndex的影响巨大,所有优化方向,通之前一致,总的来说,只要提高LCP 以及FCP 的时间SpeedIndex 的时间就会有显著提高
不过需要注意的是,接口的速度也会影响SpeedIndex的时间,由于AJAX流行的今天,我们大多数的数据都是使用接口拉取。如果接口速度过慢,他就会影响你页面的初始渲染, 导致性能问题,所以,在做性能优化的同时,请求后端伙伴协助,也是性能优化的一个方案
排查性能瓶颈
上述分析,根据三个指标提供了一些常规的优化手段,那么在这些优化手段中,有的你可以立马排查到,并且优化例如:
优化图像,优化字体大小 跟服务端配合利用浏览器缓存机制.启用cdn、启用gzip等 减少网络协议过程中的消耗,减少http 请求、减少dns查询、避免重定向 优化关键渲染路径,异步加载js等 但是有的优化手段我们不容易排查,因为他是打在包里面的,这个js 文件包含了很多逻辑怎么办,这里我有两个手段或许能够帮助排查出性能瓶颈发生在哪里:
分析包内容 在通常情况下,我们无法判断的优化点,都是在打包后,我们无法分析出,那些东西不是我们在首屏必须需要的,从而不能做出针对新的优化,为了解决当前问题,各大bundle厂商也都有各自的分析包的方案
以vue-cli 为例
"report": "vue-cli-service build --report"
我们只需要在脚手架中提供以上命令,就能在打包时生成,整个包的分析文件
如上图所示 在打包后就能分析出打包后的js 文件他包含什么组件,如此以来,我们就能知道那些文件是没必要同步加载的,或者走cdn的,通过配置将他单独的隔离开来,从而找出性能的问题
利用chorme devtool 的代码覆盖率
如下图所示,
利用 devtool的代码覆盖率检查就能知道那些js 或者css 文件的代码没有被使用过,结合包内容的分析,我们就能大概的猜出性能的瓶颈在哪里从而做相应的特殊处理
针对vue 的特殊优化
以上内容都是通用的一些优化手段,您在哪都能查到,只是我表达了一下做这些常规优化的深层原因。能让您更清楚的了解这些原因之后,在性能瓶颈的时候能游刃有余,而不是为了面试死记硬背,一到用的时候就不灵
然后我司是vue啊,咱得上得vue 的手段
图片懒加载
所谓图片懒加载,就是页面只渲染当前可视区域内的图片,如此一来,减少了其他图片渲染数量,能大大提高SpeedIndex和LCP的时间,从而提高分数
在vue中提起图片懒加载插件,首推vue-lazyload
使用方式简单,功能丰富
虚拟滚动
在一含有长列表页面中,你有没有发现你是往下越滑越卡,此时虚拟滚动就排上用场了, 他的基本原理就是只渲染可视区域内的几条数据,但是模拟出正常滑动的效果,因为每次只渲染可是剧域内的数据,在滑动的时候他的性能就会有飞速提升
在vue中比较好用的插件有两个vue-virtual-scroller 和vue-virtual-scroll-list
目前我司统一用的vue-virtual-scroll-list 他下拉的时候到了分页的地方能加些loading提示
vue 中的函数式组件
在vue中我们知道组件的初始化是比较损耗性能的,大家可以去试一下,使用vue 直接渲染一个文字内容,和直接渲染一个app.vue 组件他的分数是略有不同的。
但是当有了函数式组件,这个问题就迎刃而解了
因为函数是组件顾名思义他就是个函数,说白了就是个render函数,他少了组件初始化的过程,省去了很多初始化过程的开销
什么时候用函数式组件呢?
当你的组件中没有业务逻辑只展示内容时,这时候函数式组件就派上用场了
利用v-show 、KeepAlive 复用dom
我们知道v-show是通过display 控制dom的展示隐藏,他并不会删除dom 而我们在切换v-show的时候其实是减少了diff的对比,而KeepAlive 则是直接复用dom,连diff 的过程都没了,并且他们俩的合理使用还不会影响到初始化渲染。如此一来减少了js 的执行开销,但是值得注意的是,他并不能优化你初始化的性能,而是操作中的性能
分批渲染组件
在前面我们提到过SpeedIndex 的渐进渲染是提高SpeedIndex的关键,有了这个前提,我们就可以分批异步渲染组件。先看到内容,然后在渲染其他内容
举个例子:
<template>
<div>
{{ data1 }}
</div>
<div v-if="data1">
{{ data2 }}
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
let data1 = ref('')
let data2 = ref('')
const data = {
data1: '这是渲染内容1',
data2: '这是渲染内容2'
}
data1.value = data.data1
requestIdleCallback(() => {
data2.value = data.data2
})
return {
data1,
data2
}
},
}
</script>
上述例子比较简单可能描述的不太贴切,在这里特此说明一下,当前方法适用于组件内容较多,每次render 时间过长,导致白屏时间过长,比如,一次拉取用户列表,那么分批渲染就非常合适,先展示一部分用户信息,最后直到慢慢将所有内容渲染完毕。如此对浏览器的SpeedIndex 也非常友好。
|