前端性能指标与计算

 

在前端需求项目落地的过程中,偶尔会出现用户反馈应用操作不流畅,点击后需要较长时间才能交互的情况,后续我们前端开发者会针对这种情况排查优化并交付。

在这些性能优化的过程中,我思考是否有指标来量化前端的性能,经过多方收集资料与实践后,得出这篇前端性能指标与计算的文章,由于开发团队使用的技术偏向 Vue 、 UniApp , 因此这边文章主要是针对这两种技术栈,也就是使用 Vue 来构建的前端 H5 应用以及使用 UniApp 来构建的 Android 或者 iOS 系统上运行的小程序应用来讲解例子,希望对你有所帮助或启发。

前端的主要性能指标

在 H5 应用中衡量性能指标,通常采用谷歌 Chrome 团队提出的 核心网页指标 .这个核心网页指标有下面三个指标

  • 最大内容绘制, Largest Contentful Paint (LCP):衡量加载性能。 为了提供良好的用户体验,LCP 应在网页首次开始加载后的 2.5 秒内发生。

  • 首次输入延迟, First Input Delay (FID):衡量互动。为了提供良好的用户体验,页面的 FID 应不超过 100 毫秒。

  • 累积布局偏移, Cumulative Layout Shift (CLS):衡量视觉稳定性。为了提供良好的用户体验,页面应保持 0.1 或更低的 CLS。

需要特别说明的是在谷歌在其2023年开发者大会上提出将在2024年3月份,使用与下一次绘制的互动, Interaction to Next Paint (INP) 取代 FID ,在此文中我们更关注 INP 。

在常规的前端开发过程中,由于 CLS 主要是用来衡量突然出现的页面元素位置布局改变,一般不会出现指标异常的问题,因此我们不用特别关注。我们需要特别关注的是 LCP 、 FID 与 INP, LCP 这个指标是衡量页面加载速度的首要指标, FID 、 INP 是衡量用户操作后响应速度的首要指标。

LCP指标

INP指标

如何计算主要性能指标

计算 LCP

万维网联盟W3C 制定的浏览器实现标准会在绘制网页的过程中会不断分派 largest-contentful-paint 类型的 PerformanceEntry , 因此我们只需要使用 PerformanceObserver 监听 PerformanceEntry 的分发,就能知道 LCP 的时间,理论上我们只需要监听5秒内的最大元素绘制,因为超过5秒就是很慢,不存在监听的意义。代码如下

  new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('LCP candidate:', entry.startTime, entry);
  }
}).observe({type: 'largest-contentful-paint', buffered: true});

实际上我们会使用工具 灯塔 或者 js 库web-vitals来获取 LCP 指标。但是针对 SPA 应用这些工具收集的都只是首页的指标,不会针对具体页面计算指标,这里我们举例使用某个管理后台的数据权限配置界面的加载组织树缓慢问题,我们该如何取得这个具体页面的 LCP 呢。

这里我们直接使用Vue nexttick API 来计算, nexttick API 提供了一个数据改变,最新的一次 DOM 更新完成后的回调,我们可以先记录用户触发输入时候的时间,再使用 nexttick API 回调获得用户输入后最新的一次 DOM 更新完成的时间,这个页面最大的元素是两个全量树表,计算它们的 DOM 更新耗时可以得出这个页面的 LCP 的时间指标。其代码如下:

...
<script>
...
export default {
	...
	methods: {
		...
		// 获取组织树数据
    	getOrgTreeData () {
			this.calcStartTime = new Date().getTime();
			getTreeData()
        		.then((res) => {
					if (res.code === '000000') {
            			this.data = res.data
            			this.data2 = res.data
            			this.allDataTree = JSON.parse(JSON.stringify(res.data))
            			this.currentTree = JSON.parse(JSON.stringify(this.allDataTree))
						this.$nextTick(() => {
							let endTime = new Date().getTime();
							let time = endTime - this.calcStartTime;
							console.warn('---开始时间---'+ this.calcStartTime);
						    console.warn('---结束时间---'+endTime);
							console.warn('---结算后耗时---'+time);
						})
          			 }
				 })
		  },
		...
	}
}
</script>
...

优化前结果 4182毫秒 优化后结果 3229毫秒 百分之23

另外我们团队的项目还有在手机 APP 上面运行的由 UniApp 编译出的 wgt 小程序。由于 UniApp 生成的 wgt 小程序的Js 代码是运行在独立的引擎,不是在 webview 里面,不支持 window、document、navigator 等浏览器的 js API 。因此也无法获取 PerformanceObserver 来监听计算 LCP 。使用 nextTick API 来计算 LCP 也过于麻烦,只能针对特定页面。那么有没有一种方法可以获取每一个页面的性能呢?

但他山之石可以攻玉,经过多方查询资料,我发现字节跳动的火山引擎的小程序Pro监控方案提出了对于小程序的监控理论基础,虽然是对于微信小程序的,但是 UniApp 小程序的实现生命周期是与微信小程序相似的,因此有一定参考意义。

小程序首页性能计算

从火山引擎的参考小程序Pro监控方案原理可以得到启发,我们需要关注的是三个指标,首屏加载耗时,每一页的渲染耗时,还有首次冷启动渲染耗时,首次冷启动渲染耗时等于首屏加载耗时加首页的渲染耗时。

计算这些小程序性能指标需要监听 App 和 Page 的生命周期,那么我们如何做呢,我这边使用的是 Vue 的全局 mixin,在项目的 main.js 插入代码如下

...
// #ifdef APP-PLUS
Vue.mixin({
	onLaunch(options) {
		uni.console.log('---mixin onLaunch---')
		uni.console.log(options)
		plus.runtime.getProperty(plus.runtime.appid, function(wgtInfo) {
		    // uni.console.log('name: ' + wgtInfo.name);
		    // uni.console.log('version: ' + wgtInfo.version);
		    // uni.console.log('code: ' + wgtInfo.version.code);
		    // uni.console.log('appId: ' + wgtInfo.appid);
			// 应用加载首页面的总时间,从开始加载首页面到首页面加载完成,单位为ms https://www.html5plus.org/doc/zh_cn/runtime.html#plus.runtime.launchLoadedTime
		    uni.console.log('--plus库统计应用加载首页面的总时间--: ' + plus.runtime.launchLoadedTime);
			uni.console.log('--应用启动时间戳--: ' + plus.runtime.startupTime);
		});
		let m_launch_time = new Date().getTime();
		uni.console.log('---launch_time---: ' + m_launch_time);
		this.globalData = {
			...this.globalData,
			launchTime: m_launch_time,
		}
	},
	onLoad(query) {
		uni.console.log('---mixin onLoad---')
		let pages = getCurrentPages()
		let page = pages[pages.length - 1]
		if(page) {
			let pagePath = page.route
			uni.console.log(pagePath)
			let mGlobalData = getApp().globalData
			let page_load_start_time = new Date().getTime();
			uni.console.log('---页面开始加载时间---: ' + page_load_start_time);
			mGlobalData[pagePath] = {
				path:pagePath,
				pageLoadStartTime:page_load_start_time,
			}
			if(!mGlobalData.firstLoadTime) {
				// 计算首页加载耗时, page_load_start_time - launchTime
				mGlobalData.firstLoadTime = page_load_start_time - mGlobalData.launchTime
				uni.console.log('---首页加载耗时---: ' + mGlobalData.firstLoadTime);
			}
		}
	},
	onReady() {
		uni.console.log('---mixin onReady---')
		let pages = getCurrentPages()
		let page = pages[pages.length - 1]
		if(page) {
			let pagePath = page.route
			uni.console.log(pagePath)
			let mGlobalData = getApp().globalData
			if(mGlobalData[pagePath]) {
				let page_render_end_time = new Date().getTime();
				// 计算页面加载总耗时,构建+渲染时间, page_render_end_time - pageLoadStartTime
				let page_load_time = page_render_end_time - mGlobalData[pagePath].pageLoadStartTime;
				uni.console.log(pagePath+ '---页面加载总耗时---: ' + page_load_time);
				mGlobalData[pagePath] = {
					path:pagePath,
					pageLoadTime:page_load_time,
				}
				// 若没有展示过首屏冷启动渲染总耗时,计算并展示
				if(!mGlobalData.firstRenderTime) {
					let first_render_time = mGlobalData.firstLoadTime + page_load_time
					uni.console.log('---首屏加载耗时---: ' + mGlobalData.firstLoadTime);
					uni.console.log(pagePath+ '---首屏冷启动渲染耗时总耗时---: ' + first_render_time);
					mGlobalData.firstRenderTime = first_render_time;
				}
			}
		}
	},
})
// #endif
...
const app = new Vue({
	...App
});
app.$mount();

在小程序中实际计算结果示例如下图。

小程序性能指标实践示例

计算 INP

INP 从用户开始互动到下一帧显示视觉反馈的那一刻的时间。计算 INP 在 Vue 里面,我们可以同样直接使用 Vue nexttick API 来计算。

这里用一个管理后台项目的配置界面的搜索功能简单比较优化前与优化后的 INP 指标,其代码如下。

...
<script>
...
export default {
	...
	methods: {
		...
		// 点击搜索触发函数
		queryInfo () {
			this.calcStartTime = new Date().getTime();
      		this.$refs.tree.filter(this.filterText)
      		this.$nextTick(()=>{
				let endTime = new Date().getTime();
				let time = endTime - this.calcStartTime;
				console.warn('---开始时间---'+ this.calcStartTime);
				console.warn('---结束时间---'+endTime);
				console.warn('---结算后耗时---'+time);
			})
		},
		...
	}
}
</script>
...

优化前后 INP 指标计算对比

优化前INP计算 优化后INP计算

总结概括

通过上面的前端性能指标介绍与计算示例,以及在uni小程序中如何转换思路计算性能指标。希望能让大家在项目中如何把自己的优化成功具像化,得到一点启发与收获,感谢。

参考文档

Google Web.DEV 网页性能指标
字节跳动火山引擎小程序Pro监控性能监控产品介绍