热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

前端性能优化方法与实战05指标采集:白屏、卡顿、网络环境指标采集方法

上一讲我们介绍了首屏时间的指标采集,这一讲我们来聊聊前端其他的性能指标采集,比如白屏、卡顿和网络环境。你乘火车、地铁、飞机都走过安检吧?

上一讲我们介绍了首屏时间的指标采集,这一讲我们来聊聊前端其他的性能指标采集,比如白屏、卡顿和网络环境。

你乘火车、地铁、飞机都走过安检吧?如果说首屏时间类似你过安检的时刻,那么,白屏时间就是你排队到安检点的时间,而卡顿,就是你排的队伍停止了,前面人的不动了。本来,大家都希望过安检的时间越快越好,结果,排队花时间,停滞不动更耗时间,这无疑会让人越来越没有耐心。

浏览器的白屏和卡顿也是如此,它们直接影响用户的体验,影响用户对平台的信任。而网络环境呢,它的影响更大,同时也是性能优化的盲区,这一点我在之前的移动端 M 站性案例分析里面就中介绍过。所以,这一讲,我们就专门聊聊这三方面的指标采集。


白屏指标采集

白屏时间是指从输入内容回车(包括刷新、跳转等方式)后,到页面开始出现第一个字符的时间。白屏时间的长短会影响用户对 App 或站点的第一印象。

白屏指标怎么采集呢?我们先来回顾一下前面讲过的浏览器的页面加载过程:

客户端发起请求 -> 下载 HTML 及 JS/CSS 资源 -> 解析 JS 执行 -> JS 请求数据 -> 客户端解析 DOM 并渲染 -> 下载渲染图片-> 完成渲整体染。

在这个过程中,客户端解析 DOM 并渲染之前的时间,都算白屏时间。所以,白屏时间的采集思路如下:白屏时间 = 页面开始展示时间点 - 开始请求时间点。如果你是借助浏览器的 Performance API 工具来采集,那么可以使用公式:白屏时间 FP = domLoading - navigationStart。

这是浏览器页面加载过程,如果放在 App场景下,就不太一样了,App下的页面加载过程:

初始化 WebView -> 客户端发起请求 -> 下载 HTML 及 JS/CSS 资源 -> 解析 JS 执行 -> JS 请求数据 -> 服务端处理并返回数据 -> 客户端解析 DOM 并渲染 -> 下载渲染图片 -> 完成整体渲染。

App下的白屏时间,多了启动浏览器内核,也就是 Webview 初始化的时间。这个时间必须通过手动采集的方式来获得,而且因为线上线下时间差别不大,线下采集即可。具体来说,在 App 测试版本中,程序在 App 创建 WebView 时打一个点,然后在开始建立网络连接打一个点,这两个点的时间差就是 Webview 初始化的时间。


卡顿指标采集

所谓卡顿,简单来说就是页面出现卡住了的不流畅的情况。 提到它的指标,你是不是会一下就想到 FPS(Frames Per Second,每秒显示帧数)?FPS 多少算卡顿?网上有很多资料,大多提到 FPS 在 60 以上,页面流畅,不卡顿。但事实上并非如此,比如我们看电影或者动画时,素虽然 FPS 是 30 (低于60),但我们觉得很流畅,并不卡顿。

FPS 低于 60 并不意味着卡顿,那 FPS 高于 60 是否意味着一定不卡顿呢?比如前 60 帧渲染很快(10ms 渲染 1 帧),后面的 3 帧渲染很慢( 20ms 渲染 1 帧),这样平均起来 FPS 为95,高于 60 的标准。这种情况会不会卡顿呢?实际效果是卡顿的。因为卡顿与否的关键点在于单帧渲染耗时是否过长。

但难点在于,在浏览器上,我们没办法拿到单帧渲染耗时的接口,所以这时候,只能拿 FPS 来计算,只要 FPS 保持稳定,且值比较低,就没问题。它的标准是多少呢?连续 3 帧不低于 20 FPS,且保持恒定。

以 H5 为例,H5 场景下获取 FPS 方案如下:

var fps_compatibility= function () {return (window.requestAnimationFrame ||window.webkitRequestAnimationFrame ||function (callback) {window.setTimeout(callback, 1000 / 60);});
}();
var fps_config={lastTime:performance.now(),lastFameTime : performance.now(),frame:0
}
var fps_loop = function() {var _first = performance.now(),_diff = (_first - fps_config.lastFameTime);fps_config.lastFameTime = _first;var fps = Math.round(1000/_diff);fps_config.frame++;if (_first > 1000 + fps_config.lastTime) {var fps = Math.round( ( fps_config.frame * 1000 ) / ( _first - fps_config.lastTime ) );console.log(`time: ${new Date()} fps is:`, fps);fps_config.frame = 0; fps_config.lastTime = _first ; }; fps_compatibility(fps_loop);
}
fps_loop();
function isBlocking(fpsList, below=20, last=3) {var count = 0for(var i = 0; i if (fpsList[i] && fpsList[i] else {count = 0}if (count >= last) {return true}}return false
}

利用 requestAnimationFrame 在一秒内执行 60 次(在不卡顿的情况下)这一点,假设页面加载用时 X ms,这期间 requestAnimationFrame 执行了 N 次,则帧率为1000* N/X,也就是FPS。

由于用户客户端差异很大,我们要考虑兼容性,在这里我们定义 fps_compatibility 表示兼容性方面的处理,在浏览器不支持 requestAnimationFrame 时,利用 setTimeout 来模拟实现,在 fps_loop 里面完成 FPS 的计算,最终通过遍历 fpsList 来判断是否连续三次 fps 小于20。

如果连续判断 3次 FPS 都小于20,就认为是卡顿。

那么,在 App 侧,怎么采集卡顿指标呢?

App 侧可以拿到单帧渲染时长,直接让 App 取到单帧渲染时长,如果在 Android 环境下,可以直接取到单帧渲染时长。代码如下:

private void calculateLag(long frameTimeNanos){
/*final long frameTimeNanos = mChoreographer.getFrameTimeNanos();*/
mLastFrameTimeNanos = System.nanoTime();if (mLastFrameTimeNanos != 0) {long costTime= (frameTimeNanos - mLastFrameTimeNanos)/ 1000000.0F;//计算成毫秒//严重卡顿,单帧超过250msif (costTime>= bigJankTime) {bJank = true;} else if (costTime>= criticalBlockTime) {//超过50msmCriticalBlockCount++;} else {if (bJank) {//严重卡顿上报逻辑} else if (mCriticalBlockCount >= cStuckThreshold) {//卡顿上报逻辑,5次50ms}}}mLastFrameTimeNanos = frameTimeNanos;
}

通过 mChoreographer.getFrameTimeNanos 和 System.nanoTime 计算出单帧渲染时长,如果单帧渲染时长超过 250ms,则严重卡断,反之连续 5 次超过 50ms,判定为卡顿。

如果是 iOS 场景,要复杂一些,需要借助 CFRunLoop 来取到单帧渲染时长(CFRunLoop,它负责监听输入源,并调度处理)。代码如下:

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{MyClass *object = (__bridge MyClass*)info; // 记录状态值object->activity = activity;    // 发送信号dispatch_semaphore_t semaphore = moniotr->semaphore;dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver
{CFRunLoopObserverContext context &#61; {0,(__bridge void*)self,NULL,NULL};CFRunLoopObserverRef observer &#61; CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);// 创建信号semaphore &#61; dispatch_semaphore_create(0);   // 在子线程监控时长dispatch_async(dispatch_get_global_queue(0, 0), ^{while (YES){// 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)long st &#61; dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));if (st !&#61; 0){if (activity&#61;&#61;kCFRunLoopBeforeSources || activity&#61;&#61;kCFRunLoopAfterWaiting){if (&#43;&#43;timeoutCount <5)continue;// 检测到卡顿&#xff0c;进行卡顿上报}}timeoutCount &#61; 0;}});

通过 CFRunLoopObserverContext 将休眠、唤醒的状态通知 Observer&#xff0c;然后通过 dispatch_async 在子线程时监控节点之间的时间&#xff0c;来计算主线程的时长。

这里监控主线程是否卡顿这块儿&#xff0c;借鉴了导航 App 对交通堵塞问题的判断逻辑。

导航 App 无法判断某个地点是否出了问题&#xff0c;如车坏在当路&#xff0c;正在施工&#xff0c;或者发生事故剐蹭了这些&#xff0c;但可以借助 GPS 和定位仪&#xff0c;拿到你两个节点之间的行驶速度&#xff0c;就可以推断出这个地点是否拥堵。这里的监控思路也正是如此&#xff0c;使用状态kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个节点之间的运行时间&#xff0c;和某个阈值&#xff08;250ms&#xff09;做比较&#xff0c;根据比较结果判定主线程是否出现卡顿。

为什么会出现 App 白屏时间过长或卡顿问题呢&#xff1f;一般 WebView 初始化慢、DNS 解析慢、视图树过于复杂和主线程被阻塞等都会导致问题出现&#xff0c;但很多情况下白屏时间和卡顿都和网络环境有关。为了保证页面顺畅&#xff0c;我们需要做一些服务降级处理&#xff0c;比如对电商网站来说&#xff0c;高清图可以用文本代替&#xff0c;仅展示购买按钮和价格等核心内容。而要实现这个功能&#xff0c;就必须先做好网络环境采集。


网络环境采集

为什么不能直接拿到网络环境数据呢&#xff1f;如果在 App 内&#xff0c; 我们可以通过 App 提供的接口获取到网络情况&#xff0c;但在端外&#xff08;App 外部环境&#xff0c;比如微信里面的页面&#xff0c;或者PC站、手机浏览器下的页面&#xff09;我们就没法直接拿到当前网络情况了。这时怎么办呢&#xff1f;

一个做法是拿到两张不同尺寸图片的加载时间&#xff0c;通过计算结果来判定当前网络环境。

具体来说&#xff0c;我们在每次页面加载时&#xff0c;通过客户端向服务端发送图片请求&#xff0c;比如&#xff0c;请求一张 11 像素的图片和一张 33 像素的图片&#xff0c;然后在图片请求之初打一个时间点&#xff0c;在图片 onLoad 完成后打一个时间点&#xff0c;两个时间点之差&#xff0c;就是图片的加载时间。

接着&#xff0c;我们用文件体积除以加载时间&#xff0c;就能得出两张图片的加载速度&#xff0c;然后把两张图片的加载速度求平均值&#xff0c;这个结果就可以当作网络速度了。

因为每个单页面启动时&#xff0c;都会做一次网速采集&#xff0c;得到一个网络速度&#xff0c;我们可以把这些网络速度做概率分布&#xff0c;就能得出当前网络情况是 2G &#xff08;750-1400ms&#xff09;、3G &#xff08;230-750ms&#xff09;、4G或者WiFi&#xff08;0-230ms&#xff09;。

下面这张图是 2016 年我在做移动端 M 站性能优化项目时&#xff0c;做的图片测速结果分布。横坐标是速度&#xff0c;纵坐标是网速在分布中的分位值&#xff0c;最左侧是 wifi网络&#xff0c;中间是 3G 网络&#xff0c;最右侧是 2G 网络。

image (4).png


图片测速结果分布


根据这张图&#xff0c;你会发现自己的用户都停留在什么网段。比如&#xff0c;我在 2016 年发现&#xff0c;58 同城的用户测速分布&#xff0c;50% 的用户停留在 2G 水平。知道了这点&#xff0c;我们后续针对的优化手段就会更多侧重 2G 下的网络优化方案了。


小结

溪风的思维导图05.png

前面我们详细讲了白屏时间采集、卡顿指标采集和网络环境采集&#xff0c;有了这个采集&#xff0c;我们就能很容易定位用户体验层的很多问题&#xff0c;比如加载感受、交互感受和弱网下的服务降级处理&#xff0c;等等。

在白屏部分&#xff0c;里面提到的更偏加载阶段的白屏&#xff0c;实际工作中我们会遇到不少广义上的白屏&#xff0c;比如后端接口异常导致的白屏&#xff0c;数据加载中产生的白屏&#xff0c;甚至还有图片与视频加载过程或等待过程中的白屏。

那么&#xff0c;现在就给你留一个问题&#xff1a;这些广义的白屏问题怎么采集监控呢&#xff1f;

欢迎在评论区和我沟通。下一讲&#xff0c;我们进入上报 SDK及策略设计部分。

源码地址&#xff1a;https://github.com/lagoueduCol/WebPerformanceOptimization-xifeng/tree/master/chapter5




精选评论


**燕&#xff1a;

有点没太明白文中前60针 没帧10ms 后3⃣️针 20s 最后计算fps为95是怎么得出来&#xff1f;



    讲师回复&#xff1a;

    赞认真阅读和思考&#xff0c;1000/&#xff08;(6010&#43;320)/3&#xff09;&#xff0c;算出来每帧耗费的时间&#xff0c;因为是毫秒&#xff0c;用1000去除得出1秒内多少帧也就是fps



**熙&#xff1a;

请问小程序中如何获取呢



    讲师回复&#xff1a;

    目前我们主要做了首屏时间采集逻辑&#xff0c;首屏时间是路由开始到 setData 结束。



推荐阅读
  • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • QuestionThereareatotalofncoursesyouhavetotake,labeledfrom0ton-1.Somecoursesmayhaveprerequi ... [详细]
  • 为什么三角形与菜单背景的颜色不同? - Why is the triangle a different colour shade to the menu background?
    Imnotunderstandingastowhythetrianglewhichappearswhenthemousehoversoverthemenuitem, ... [详细]
  • webui之常用js操作(webui界面是什么)
    本文目录一览:1、web前端开发需要掌握的几个必备技术 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • 本文讲述了作者通过点火测试男友的性格和承受能力,以考验婚姻问题。作者故意不安慰男友并再次点火,观察他的反应。这个行为是善意的玩人,旨在了解男友的性格和避免婚姻问题。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • 本文是一篇翻译文章,介绍了async/await的用法和特点。async关键字被放置在函数前面,意味着该函数总是返回一个promise。文章还提到了可以显式返回一个promise的方法。该特性使得async/await更易于理解和使用。本文还提到了一些可能的错误,并希望读者能够指正。 ... [详细]
author-avatar
lovely尤研君2007
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有