如何收集和评估首屏性能优化的效果
针对用户访问的关键路径,比如从首页跳转到列表页、从首页跳转到个人中心、从列表页跳转到商详页等,哪些关键页面需要做首屏优化,确定下来范围。
制定每个页面场景的首屏优化目标,比如打开首页,现阶段在低端机上花了2000ms,只要首屏优化到1000ms内就算达标(低端机容易出现性能问题,高端机在性能问题上的表现不明显)。
测试人员根据关键路径,使用低端机操作页面转场,并进行视频录制,将1秒划分为60帧,即每帧约16.7毫秒(为了方便计算,可以改为每一帧20毫秒)。由于网络等影响因素比较多,每一次录制得出的数据都不一样,测试人员需要重复录制10次,去掉10次中的异常数值后取平均值,该平均值作为本次统计的耗时。
举个例子,视频中在第915帧点击了列表页商品,开始转场跳商详,在第970帧时商详页展示主UI,那么耗时为(970-915)*20=1100ms
。
另外,测试人员需要在发版前、发版后都进行录制,要求发版前录制主要是为了避免性能问题暴露到生产环境上。
排查工具使用
git
有时候发版前,测试人员跟你说xx页面的性能数据比前一天的上涨了几百毫秒,你可以通过指定目录下的24小时内git commit内容改动,快速定位有性能问题的代码改动。
Performance
可以通过谷歌浏览器的Performance面板录制具体页面的trace,进行分析,记得把cpu降速6x
Memory
可以用来排查内存泄漏,先点击强制进行垃圾回收,然后观察指标是否大幅上升
监控面板
通过普罗米修斯prom-client
和grafana收集数据和搭建监控面板,统计服务端接口的耗时情况
vue组件的优化
vue组件重复渲染
在编写vue组件实现需求过程中,需要注意是否存在vue组件重复渲染的问题。我们可以利用renderTriggered
检测此类问题,renderTriggered
是一个开发环境下使用生命周期钩子,当响应式数据更新导致组件重新渲染时会触发这个钩子。
如果是Vue2,可以用Vue.mixin
进行全局混入,会作用到每一个之后创建的Vue实例
Vue.mixin({
renderTriggered(event) {
console.log(`[${this.$options.name}] renderTriggered: `, event)
},
})
打印效果如下:
如果是Vue3(Vue3不提供Vue.mixin
),可以通过修改Vue源码实现:
方案一:
// 我们自己实现的方法
const devRenderTriggered = function (event) {
const name = publicThis.$options.name || publicThis.$options.__name
console.log(`[${name}] renderTriggered: `, event)
}
// 在vue源码中找到注册renderTriggered的地方,增加逻辑当取不到renderTriggered时就取devRenderTriggered进行注册
registerLifecycleHook(onRenderTriggered, renderTriggered || devRenderTriggered)
方案二(相对方案一更简单一点):
app.mixin({
renderTriggered(event) {
const name = this.$options.name || this.$options.__name
console.log(`[${name}] renderTriggered: `, event)
},
})
在Vue3项目中,方案一或者方案二都可以实现下面的打印效果:
数据处理
- 尽量避免不必要的响应式数据绑定,比如不可变常量
- 对接口数据做精简化处理
spa应用转场走服务端渲染
在spa应用中,页面转场一般都是客户端渲染(不走ssr),但是在一些低端设备中进行完整的vue组件渲染耗时会很久,对此做了一个优化的方案:在低端设备中页面转场走ssr,把组件编译渲染从低端设备转移到服务端,低端设备只需要进行水合。在用户触发转场从A页面跳到B页面时,判断设备(通过userAgent识别出安卓机和iphone8),如果命中判断条件,就在服务端执行ssr render,把render结果(主要是上下文数据和html content)返回给客户端,此时客户端只需要进行hydration即可。
效果是好的,但是具体代码改动比较复杂,涉及的测试场景多,比如loading态处理、需要同时兼容两套转场逻辑等。测试成本也会增加,测试人员需要针对安卓机和苹果机型进行转场场景的测试,判断转场使用了哪种技术方案,可能有些bug只在安卓机转场才能复现。
资源的优化
打包工具的资源分析插件
可以用上述插件进行包体积的分析,做针对性的优化,比如js chunk拆分等。举个常见案例,常量的代码如果和工具方法的代码都写在一个js文件里,常量在多个页面被使用,而工具方法只在一个页面被使用,打包时常量和工具方法被打进一个chunk里,其他页面引用该chunk有多余资源浪费。
资源的预取
<link rel="preload" href="https://xx.com/a.webp" as="image">
preload表示资源的优先级最高,比如可以把首屏加载的图片设置为preload。
<link rel="prefetch" href="https://yy.com/915443.js" as="script">
prefetch表示该资源在当前页面用不到,但是下个页面可能会用到,会等当前其他资源加载好了之后,下载队列空闲,该资源才会被下载缓存起来。比如首页跳转列表页,在首页设置prefetch列表页的主chunk。github.com/GoogleChrom… ,quicklink这个库是跟prefetch有关的。
<link rel="preconnect" href="https://cc.com">
preconnect的作用是提前和目标服务器进行连接,比如提前和图片服务器连接好。
<link rel="dns-prefetch" href="https://dd.com">
dns-prefetch的作用类似preconnect
script async: 解析html时,如果遇到标识为async的script标签,不会阻塞解析HTML的剩余代码,会异步请求加载该js脚本,但是脚本加载完成之后会阻塞停止解析html,立即执行脚本,最后等执行完脚本再继续解析html代码。
script defer: 解析html时,如果遇到标识为defer的script标签,不会阻塞解析HTML的剩余代码,会异步请求加载该js脚本,但是脚本加载完成之后不会立即执行脚本,而是等解析完html代码再执行该js脚本。
图片的优化
图片自动转webp
在首页、列表页等ssr页面,判断设备是否支持webp,将结果写入cookie中,这样做可以在node端读取cookie判断,支持webp的话就进行转换(需要图片服务器同时提供png、webp等格式)
const isSupportWebp = function () {
if (typeof window === 'undefined') {
return false
}
const $canvas = document.createElement('canvas')
const data = $canvas && $canvas.toDataURL('image/webp', 0.5)
return data.indexOf('data:image/webp') === 0
}
const transformWebp = function (imgUrl) {
const imgHosts = ['img1.com', 'img2.com']
const canTransform = imgHosts.some(host => imgUrl.includes(host))
if (canTransform) {
return imgUrl.replace(/\.(jpg|jpeg|png)$/, '.webp')
}
return imgUrl
}
图片手动预取
比如我们可以在首页空闲时主动预取一些个人中心的图片,当跳到个人中心时图片就是从缓存中读取
export const preloadImgs = async (imgs = [], timeout = 3000) => {
return Promise.race([
new Promise((resolve) => {
let loadedNum = 0
const imgLoadHandle = () => {
loadedNum++
if(loadedNum === imgs.length) {
resolve()
}
}
imgs.forEach((src) => {
const img = new Image()
img.src = src
img.onload = imgLoadHandle
img.onerror = imgLoadHandle
})
}),
new Promise((resolve) => {
setTimeout(() => {
resolve()
}, timeout)
})
])
}
在测试图片的预取是否生效时记得不要开启浏览器Network面板的disable cache,开启了会导致图片缓存不生效。
lazyload
非首屏图片设置lazyload
图片裁切
手机宽度有很多种宽度,ipad设备宽度一般比手机宽度大很多,根据设备宽度获取不同size的图片(图片服务器需要提供不同size的图片),比如375宽度只需要750宽度的图片,不需要获取1366宽度图片,750宽度图片比1366宽度图片小很多。
针对列表的优化
列表页首屏
对于列表页面的首屏,如果第一页就ssr直出20个列表item,直出内容很多,而屏幕第一屏只够展示前6个item:
- 方案一:改成第一页在服务端请求6个列表item,服务端渲染6个列表item,等在客户端ssr水合完成之后,再发一次请求获取剩下的20-6=14个列表item并进行渲染
- 方案二:改成第一页在服务端请求20个列表item,服务端渲染6个列表item,等在客户端ssr水合完成之后,将14个列表item进行渲染
分片渲染
在低端机中,列表页一次性地渲染所有item,渲染耗时久,很容易出现页面白屏和翻页操作卡顿。
分片渲染是一种按顺序执行任务的方法,创建一个任务队列,并利用定时器逐个处理这些任务。
const setListItems = (products = [], chunkSize = 6, handleAdd) => {
let start = 0;
const processChunk = () => {
const end = Math.min(start + chunkSize, products.length);
const chunkProducts = products.slice(start, end);
start = end;
handleAdd(chunkProducts);
if (start < products.length) {
window.requestAnimationFrame(processChunk)
}
}
processChunk()
}
下面图片中可以看到分片渲染处理之后,被分成了许许多多的块,而不是一整片都是红的
缓存
接口缓存
像一些对实时性要求不高的接口,可以设置1分钟/2分钟的内存缓存,在缓存有效期内重复触发不会调接口,而是直接读取缓存数据。接口缓存可以搭配接口预请求,比如在A页面请求准备好B页面的数据,设置1分钟缓存,这样1分钟内转场到B页面时就不用调接口,更快地获取到数据。
redis缓存
比如榜单数据一般不会经常变化,在服务端可以设置榜单数据redis缓存十分钟,使用redis时需要注意缓存key的合理性,考虑redis缓存的命中率,监控redis相关指标,注意控制好redis的使用成本。
service worker缓存
html文件、js文件、接口数据缓存,具体配置可以看workbox的文档 github.com/googlechrom…
存在的问题
旧数据问题:
- 比如提交表单后,重新获取的数据应该是更新后的数据,但是获取到了旧的缓存数据
- 本地开发时,vite的文件缓存和service worker配置的文件缓存有冲突,导致开发时提示获取不到某个js文件
代码复杂度问题:
- 性能优化的代码往往自带骚操作,代码不易读懂
- 有很多是针对具体业务场景做侵入性地改造,与业务逻辑强耦合,业务开发人员有时会踩坑里,有时也会误删性能优化的代码
参考