由于转转前端业务方向主要偏向于 C 端,比如 App端内 H5、 小程序内 H5 等,并且技术栈以 Hybrid 为主(承载容器为转转标准化webview)。但是,近些年随着业务不断扩大,逐渐出现了如乾数据平台、行星平台等 专门服务 B 端的FE项目。但是没有相关性能数据来作为参考支撑,比如需要分析用户体验质量;分析现有页面性能缺陷以及后续需要做性能优化的方向等。因此,需要一款符合转转内部埋点上报体系的 PC 端项目网页的性能统计平台。
由于内部性能埋点统计体系不支持分批/分段上报,每个 Router 都需要作为一个单独的页面进行一次性的性能数据上报。在 B 端,一些新的指标需要支持和特殊处理。因此,在数据采集统计方面,我们会遇到以下几个问题。
定义好哪些性能指标需要上报,是做好一个完善的采集性能数据采集 sdk 的前提条件,经过分析主要将指标分为两类:1. 纯 H5 页面性能指标 2. 页面相关业务性指标。
以上提到的绝大部分指标,可以通过浏览器提供的 PerformanceNavigationTiming PerformanceResourceTiming API 和 谷歌团队提供的 web-vitals 工具函数很方便的进行获取和计算。
所谓业务性指标,主要是作为查询分析的一些要素,比如 我们想查询某个业务线的某个项目的某个页面在某个平台下某个性能指标的表现如何?那么就需要一些非页面性能本身的业务要素指标进行定义和上报统计。
业务指标主要包括:actiontype 埋点类型标识 、 pagetype 业务线/项目标识、pageid 页面标识 、 clientType 端信息、 pagestate 页面状态、pageurl 页面url、 cookieid 用户id、 fromType 来源、 loadcnt 加载次数 等等。
PS: web-vitals 由于在苹果和低版本安卓的兼容性存在问题,因此没有在 C 端作为一个必选项,但 B 端用户绝大多数使用 chromium 内核浏览器,所以大胆的将 web-vitals 纳入采集指标中
上面进行了各种指标的定义,那么如何高效有序的接入到转转埋点体系内进行上报统计呢?转转内部其实已经有了 C 端埋点体系,其实只需要按照一定的规则进行接入即可,主要是性能平台B端项目需要的字段和后端已有日志表结构做好关系映射和扩展。
为了解决上面提到B端项目的特有问题,以及满足上述提到所有性能指标、业务指标都可以很优雅的进行上报统计,方便在代码层面更好的进行结构上的解耦,并且尽量做到性能计算统计相关程序不影响页面本身的性能,在技术实现设计层我们把上面的指标做了一些分类,比如 同步计算指标(基础业务同步指标、基础性能资源同步指标)、异步计算指标(性能异步指标、后置异步指标)等。具体如下图所示。
技术层面指标分类
下面详细介绍一下一些关键逻辑是怎么处理的?各类性能指标具体是怎么计算的?下面列出了部分指标怎么获取和计算的关键代码。
SPA 项目的路由页面的拦截关键逻辑:
const hackRouter = () => {
if (!window?.history?.pushState) {
return;
}
// 浏览器的历史记录发生变化时被触发, 导航前进、后退
const oldOnPopState = window.onpopstate;
window.onpopstate = function(this: WindowEventHandlers, ...args: any[]): any {
const to = window.location.href;
const from = lastHref;
lastHref = to;
// 通知订阅的回调
triggerHandlers('history', {
from,
to
});
if (oldOnPopState) {
try {
return oldOnPopState.apply(this, args);
} catch (e) {}
}
};
// history pushState 或 replaceState 触发,通过 history api 方式
const wrapHistoryFn = (type: 'pushState'|'replaceState') => {
const originalHistoryFunction = window.history[type]
return function(this: History, ...args: any[]): void {
const url = args.length > 2 ? args[2] : undefined;
if (url) {
// coerce to string (this is what pushState does)
const from = lastHref;
const to = String(url);
lastHref = to;
// 通知订阅的回调
triggerHandlers('history', {
from,
to
});
}
return originalHistoryFunction.apply(this, args);
};
};
window.history.pushState = wrapHistoryFn("pushState");
window.history.replaceState = wrapHistoryFn("replaceState");
}
性能基础指标的获取相关代码:
// 获取 PerformanceTiming 相关数据
export const getPerformanceTimingData = (task: TaskTypes) => {
if (!window?.performance?.timing) return {}
const { metrics } = task;
const { state } = task.ctx;
const ptiming = performance.timing;
// 默认为 -1 方便过滤无效值
const result = {
blankTime: -1,
dnsTime: -1,
httpTime: -1,
domTime: -1,
domReady: -1,
// ...
}
// 页面加载状态
if(state === 'pageload') {
// ...
// 白屏
result.blankTime = fix(ptiming.responseStart - ptiming.navigationStart);
// DNS查询
result.dnsTime = fix(ptiming.domainLookupEnd - ptiming.domainLookupStart);
// HTTP请求
result.httpTime = fix(ptiming.responseEnd - ptiming.responseStart);
// 解析dom树
result.domTime = fix(ptiming.domComplete - ptiming.domInteractive);
// DOMready
result.domReady = fix(ptiming.domContentLoadedEventEnd - ptiming.navigationStart)
// ...
}
// 路由切换状态
if (state === 'navigation') {
// ...
}
return result
}
资源相关指标的数据获取关键逻辑:
// 记录
let performanceCursor: number = 0;
// 获取当前页面资源列表
export const startPerformance = (task: TaskTypes) => {
const { timeOrigin } = task.ctx;
if (!window.performance || !window.performance.getEntries || !timeOrigin) {
return;
}
// performanceEntries
const performanceEntries = performance.getEntries();
const pss = performanceEntries.slice(performanceCursor);
// 处理 各种 performanceEntry 资源
formatResourceEntries(task, pss);
performanceCursor = Math.max(performanceEntries.length - 1, 0);
}
export const formatResourceEntries = (task: TaskTypes, entries: PerformanceEntryList) => {
const { state, startTimestamp, timeOrigin } = task.ctx;
const { metrics } = task
entries.forEach(entry => {
const startTime = entry.startTime;
// console.log( timeOrigin, startTime, startTimestamp, timeOrigin + startTime < startTimestamp)
if (state === 'navigation' && timeOrigin + startTime < startTimestamp) {
return;
}
const baseStartTime = startTimestamp - timeOrigin;
switch (entry.entryType) {
case 'navigation':
// 处理 bodysize
// ...
case 'paint':
// 处理 paint 指标 fcp fp
// ...
case 'resource':
// 序列化各种资源, 如js/css/img/jsonp/ajax/fetch/iframe...
calcResource(entry, result, baseStartTime);
}
// ...
}
业务指标数据的获取:
// 初始化基础业务指标
export const initBaseData = (task: TaskTypes) => {
const { params = { backup: {} }, options = {} } = task;
// ...
Object.assign(params, {
pagetype: options?.pagetype || pagetype,
actiontype: options?.actiontype || actiontype,
appid: options?.appid || appid,
// and more ...
});
return task;
}
longTask 的记录获取:
function startLongTasks(): void {
const entryHandler = (entries: PerformanceEntry[]): void => {
for (const entry of entries) {
const startTime = entry.startTime
const duration = entry.duration;
const endTime = startTime + duration;
const longtask = {
name: `longtask-${++n}`,
startTime,
endTime,
duration
}
longTasks.push(longtask);
}
};
if(PerformanceObserver?.supportedEntryTypes?.includes('longtask')) {
// 注册 longtask 异步任务
observe('longtask', entryHandler);
}
}
在实际项目统计时,发现一些性能指标算法的适用性问题需要注意:
- LCP 算法存在的问题。比如:触发条件限制的问题,当检测到用户输入时候 FMP算法会停止计算,就导致某些场景触发不了(比如主要内容还没显示就点击页面)。白屏占位图问题,页面初始有较大的白屏占位图时 即使后面被移除了,LCP 算法还会把它当作主要内容。
- FMP 算法不适合某些特殊场景。比如:2/3 是金刚位图片布局,最下面 1/3 区域有一个瀑布流,由于FMP算法计算规则会导致统计时间在瀑布流请求之后展现后,就导致直观上的页面首屏时间变大。
数据可以计算并获取了,那么如何进行友好的处理上报?
由于内部埋点提下不支持回话形式的分段上报,那么就需要在前端提前准备好所有需要需要上报的数据的处理,整体B端 SPA 项目性能数据处理的上报处理机制,以及同步任务数据、异步任务数据任务的处理流如下图所示。
在进行数据上报时,如果页面的静态资源加载 / ajax请求数量很多时,埋点上报请求接口的 body 会很大,导致请求耗时长而影响页面本身的性能。因此针对 body 过大的问题,对一些资源的统计做了序列化处理。
比如:单条静态资源的原始数据结构为:
const entry:PerformanceResourceTiming = {
"name": "https://xxx.zzz.com/yyy.css?v=5J1NDtbnnIr2Rc2SdhEMlMxD4l9Eydj88B31E7_NhS4",
"entryType": "resource",
"startTime": 1924.6000000238419,
"duration": 1400.5999999642372,
"initiatorType": "link",
"fetchStart": 1924.6000000238419,
"responseEnd": 3325.199999988079,
}
序列化之后,将各个关键数据合并成一个字符串,即:
// 将 entries 分类,并把单个entry 进行字符串化后,再将所有 css entry 合并
const cssEntry:string = 'https://xxx.zzz.com/yyy.css|1924|1924|3325'
可以发现系列化精简后将 255个字符优化成了 42 个字符。
往往B端 SPA 项目静态资源和请求多达几十上百个,这样序列化处理合并之后,能将埋点上报请求 body 体积减少数千个字节。当然了,如果服务支持编解码,还可以通过其他更优的序列化方案进行 body 体积压缩。
在对数据进行处理时,也遇到了一些问题。
每天上报的性能埋点数据存储在哪里?
如何计算数据?如何扩展数据?如何查询数据?
二次计算后的数据量依旧非常大,该怎么办?
为了解决数据处理中的两个核心问题,我们采用了这个完整的流程。在面对如此庞大的数据时,我们需要考虑它们存储在何处。同时,我们也需要考虑如何查找和计算需要的指标。这个流程可以帮助我们更好地处理数据,提高效率。
此外,这个流程还有一个重要的作用,那就是保证数据的准确性和完整性。在数据处理过程中,我们需要遵循一定的规则和标准,以确保数据的可靠性。这样才能让我们在分析数据时得出正确的结论,更好的进行针对性的优化。
web平台部分功能页面展示如下:
历史变化曲线
性能数据查询
在B端项目中,页面性能统计是非常有必要的,因为可以帮助我们了解实际用户的具体页面的加载速度、用户体验,以便了解当前页面的质量,并且为优化页面性能提供方向,从而提高用户满意度。
当前文章:转转B端项目页面性能统计实践
转载注明:http://www.shufengxianlan.com/qtweb/news28/423978.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联