文章主要记录学习web性能优化方面的内容。

搭建前端性能平台

主要是要包含首屏,白屏,秒开率,数据瀑布流等,要包含关键指标和个性化细分的性能数据
web后台主要包括后台的性能数据处理前台的可视化展示

  • 数据处理后台

对SDK上报的数据处理和运算,数据入库,数据清洗,数据计算

  • 前台的可视化展示

将数据返回给web后台做可视化
image.png

技术架构

image.png

平台层(黄色)

展示给用户的部分

数据接入层(绿色)

接收SDK上报的性能数据,做数据处理后入库。主要是Nodejs,Node-sechdule(定时任务),Node-mailer(发送邮件)

数据计算层(橙色)

对性能数据做计算处理,需要的技术是Kafka,Spark,Hive,HDFS

存储层(蓝色)

包括MySQL+MongoDB

性能数据处理后台

SDK上报数据处理过程:

  1. 客户端借助SDK做上报,数据接入层接入数据,做协议转换等处理之后,作为生产者向Kafka写入数据
  2. 数据计算层作为消费者,从Kafka读数据存入Hive(Hadoop平台的存储表),Hadoop平台借助Spark做数据分析计算
  3. 借助Hive提供的接口,数据计算层使用SQL语句从Hive拉取计算后的数据到数据库平台(MongoBD),平台层取出数据,准备数据可视化展现的数据

    性能数据后台搭建过程

    入库

    客户端借助SDK上报之后,先由后端服务层做处理,比如Nodejs,利用Controller层对数据作处理,避免出现脏数据(空数据和异常数据),将上报的数据通过URL解析为key-value格式,空数据删除,异常数据舍弃,让数据写入消息队列Kafka。
    为什么不直接存入Hive?因为并发过高数据量过大会导致服务器扛不住导致数据丢失。
    所以先选择Kafka,先把数据写入消息队列。Kafka通过缓存,慢慢接受数据,消息队列可以接收数据之后将其删除,避免数据重复。

    数据清洗和计算

    对Kafka数据做数据清洗和计算
  • 数据清洗,对性能上报单条数据进行核对校验
    • 重复数据,网络出错导致多次上传时候。直接去重复删除
    • 缺失数据,有首屏时间,没有白屏或者卡顿等。在Spark平台上,先根据performance数据补全,无法补全,就直接舍弃。
    • 错误数据,数据超出正常范围,极大值,极小值等,当作无效数据,直接舍弃掉
  • 数据计算,通过Spark平台做计算为可视化做准备
    • 计算首评时间分布,1s-2s占比多少,2s-4s占比多少
    • 秒开率的计算,首屏时间小于等于1秒的数据占比
    • 页面瀑布流时间计算

      准备数据可视化的相关数据

      主要是前端页面展示,登录模块,用户关注的模块信息。
      性能数据都是单条信息,相互之间没有关联可以用MongoDB做存储,可以用Nodejs定时脚本(Node-sechdule)送Spark获取数据到MongoDB中

      可视化前台

      可以放一个大盘页和详情页

      大盘页

      大盘页面包括一个个业务的性能简图,每一个性能图包括首屏时间、秒开率、采样PV数据,点击进入详情可以进入详情页面。
      大盘页面可以自己设置关注相关业务,然后可以在大盘页面看到自己关注的业务数据

      详情页面

      详情页面是为了补充新能简图,除了有秒开率,性能均值细节,白屏均值细节等,还会有终端信息和占比,比如网速占比,IOS占比,方便不同下的场景做优化
      image.png
      image.png

同时,为了还需要一个瀑布流,用来统计秒开率不达标的原因,瀑布流就是统计DNS查询时间,TCP链接、请求耗时、内容传输、资源解析、DOM解析、资源加载时间。
以下是瀑布流时间点
image.png

Nodejs采用Egg.js框架,采用 compression 对 HTTP 传输内容进行 GZip 压缩处理。
预警的话,用Node-schedule 做调度和定时任务的处理,用node-mailer 进行邮件报警
image.png

在以上,最有用的数据还是秒开率,首屏时间、白屏时间。
判断用户是不是第一次访问,可以采用在缓存中设置标志的方式做判断。

监控预警和诊断清单

监控预警

结束Node-schedule做调度和定时任务处理,通过node-mailer 进行邮件报警

准备预警数据

数据清洗完成,一个分支用Spark做计算,另外一个使用Flink实时数据做计算,Flink是实时的,因为监控预警是需要实时的。超过2s可以认定为卡顿数据,标记为预警

数据拉取

然后借助Node-schedule,通过定时任务将预警数据通过Nodejs,拉取到MongoDB的预警列表中。

预警

预警可以通过企业微信或者飞书等、邮件报警、短信报警等。
比如手机列表页,新能标准是首屏1.5s,秒开率90%,超出的话就会在性能平台预警展示。如果超过了10%,会现在企业通讯的工具做通知,超过20%,借助Node-schedule发送邮件,超出30%,发送短息预警。因为可能会浪费资源,一般对App首页进行页面监控。

因为性能问题,预警邮件越发越多怎么办?一般采用增量预警,一段时间内降低优先级不重复预警

问题诊断

如果发现问题,都要先对问题进行诊断。

  1. 确认问题是共性还是个性,共性问题,开始诊断优化,个例问题,因为偶发导致的,不需要专门优化。共性问题可以通过大量用户指标是否正常来判断,比如监控平台指标。

假设某地有一个用户说自己加载页面慢,如何来分析?

  1. 共性问题,查看性能平台,发现在中国移动网络,加载首屏时间很长,超过了3s。然后发现页面瀑布流,负责上传图片JS文件加载特别慢,因为它在资源加载时被阻塞,阻塞的是一个负责对图片上传之后实施滤镜的JS文件。这个文件可以通过defer或者async异步加载,于是调整策略,解决了问题
  2. 个例问题,大部分用户都是正常的,一般需要联系用户单独处理,不需要代码层面做什么。

    诊断清单(具体优化手段)

    性能优化具体相关

    全量VS增量

    指的是列表页面加载数据。
    京东App移动端首屏一般展示4条左右,PC展示50条,一般是同一个接口。如果在App端,返回50条就不合适。一般会在页面滚动或者下拉时候加载。
    一般会在App端除了保持首屏数据,一般会多加载两条保持加载流畅,比如展示4条数据,我们可以拉取6条,这样在往下滚动再去加载更多。

    同步VS异步

    即接口同时加载数据可以并发请求,不要继发请求,比如await之后再写另外一个await就是继发了,可以考虑同时去请求。

    实时VS缓存

    指的是接口数据或者静态资源加载时间过长,可以看下是否使用了实时数据导致。有些数据比如榜单信息可以不用实时去获取,定时更新即可。

    原片VS压缩

    指的是图片替换为webp等格式,不要使用原图。页面在展示时候先设置一张低质量图片,如果许多用户关注,在做进一步优化处理。

    效果评估

    效果评估一般是指业务上的评估,只涉及性能上和一些指标并不够。
    最佳方式是通过性能优化,提高业务数据指标。关注访问用户到订单的转化。
    比如沃尔玛网站在线页面每减少一秒,转化率增加2%,只要访问速度增加,就会用更多的转化。前提是首屏时间远远大于1s,如2s-5s,如果首屏已经是秒开,提升会非常有限。

    AB TEST系统

    为了更好的观测转化率指标,需要通过AB test系统来进行对比。
    具体做法是,可以在性能优化时候,代码中把页面区分为A、B两个版本。这两个版本可以通过条件语句区分代码块,通过模板和路由方式区分。A版本是优化前的,B版本是优化后的,用if语句控制流程做区分,展示A版本,在地址栏传入VERSION=A,之后就可以通过这个埋点统计到各自的转化率了。

在做AB TEST之前,先要做AA测试,即两个版本代码一样,保证波动率比较小,差异只有万分之一再去做AB TEST。

最后,有可能会遇到优化之后转化率降低等情况,有可能兼容性出问题导致bug,一定要做好兼容性测试,针对Top10的机型和弱网环境,做好测试。

image.png

优化首屏

懒加载

懒加载是指长页面在加载过程中,先加载关键内容,延迟加载非关键内容。打开一个页面,如果有部分超出了可视区,先加载可视区内容,剩下内容等进入之后在加载。YouTube这些网站都是这么做的。
另外接口层面,如果首屏只需要几条数据,后端一次给出50条,会导致请求时间过长,导致首屏加载变慢,可以用懒加载,估算下首屏需要几条数据,然后等到了可视区再加载,图片也一样。

缓存

接口缓存

  • 端内(App内),可以使用Native请求实现接口缓存。因为如果使用H5的请求,需要等webview初始化之后才能请求,串行请求。采用Native请求,可以在webview初始化之前就开始请求,可以节省时间。需要封装一个SDK,修改原来数据接口请求方法,实现类似Axios的请求方法,把Get,Post等封装为SDK,调用SDK.axios方法,webview会拦截这个请求,先去查看App本地是否有缓存,有的话走缓存,没有的话,就像服务器请求数据,然后缓存到App中。

    静态资源缓存

    一般页面加载。静态资源会占大多数,所以可以缓存静态资源。

  • 长期不变动的资源,可以设置Cache-Control:max-age=31536000,让浏览器一年之内只使用本地缓存。

  • 经常变动的资源,可以设置etag和If-none-match来判断资源是否有更新

    离线化

    将资源数据放到本地,加载时候走本地资源。也可以讲页面内容缓存在本地,一般适合首页或者列表页面等不需要登录的场景,主要是通过Webpack 的 prerender-spa-plugin 来实现预渲染

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // webpack.conf.js
    var path = require('path')
    var PrerenderSpaPlugin = require('prerender-spa-plugin')
    module.exports = {
    // ...
    plugins: [
    new PrerenderSpaPlugin(
    // 编译后的html需要存放的路径
    path.join(__dirname, '../dist'),
    // 列出哪些路由需要预渲染
    [ '/', '/about', '/contact' ]
    )
    ]
    }

    并行化

    在请求上,减少请求阻塞的问题,去减少首屏时间。主要通过HTTP2.0实现多路复用,之前前端主要采用域名分片来进行,但是会导致DNS解析时间过长,如果采用了HTTP2.0就没有这样的问题

image.png

优化白屏与卡顿

白屏

白屏是指从开始等待页面到第一个字符出现的时间,白屏时间越短,就可以有效降低跳出率
影响白屏时长两个主要因素,DNS查询和首字符串展示

DNS查询优化

DNS是指浏览器发起请求时候,需要将用户输入的域名地址转换为IP地址的过程。
一般需要将DNS查询时间控制在400ms以内,通过直接连接方案可以控制在200ms

普通方案

浏览器

前端页面采用dns-prefetch,在静态资源请求前对域名进行解析,减少用户的等待时间,大概可以节省150ms左右的DNS解析时间。

1
2
3
4
5
<!-- 开启DNS预解析 -->
<meta http-equiv="x-dns-prefetch-control" content="on" />
<!-- 对gstatic域名做解析 -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com/">
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin>
webview

启动App时候,创建一个肉眼不可见的Webview(比如1*1的webview),然后将静态资源路径写入这个webview中,然后做域名解析并放入缓存。之后访问因为已经做过域名解析从缓存中获取即可。浏览器内也可以采用这个方案比如iframe

直连方案

简单来讲就是IP直连,原来请求域名方式,通过SDK进行域名解析,拿到IP直接请求IP地址。
这种方式存在的问题是

  • Https证书,当客户端采用IP直接连接,由于URL是host,所以在证书验证环节,会出现domain不匹配,导致SSL/TLS握手不成功

(以下内容无法理解直接复制原文)
怎么解决呢?在非 SNI(Server Name Indication,表示单 IP多域名)的场景下,可以把证书验证环节独立出来 (如 Hook证书校验环节),然后将 IP 替换为原来的域名。在 SNI 场景下,可以定制 SSLSocketFactory,在 createSocket 时替换为 IP,并进行 SNI/HostNameVerify 配置。
而配置文件方面,一般在域名只有两三个的情况时,我们可以用到它来做 IP 和域名的映射。但随着机房的扩大,每次扩机器都要升级配置文件,后续会非常麻烦。
对此我们可以采用 httpDNS 来解决。这是因为 httpDNS 可以准确调度到对应区域的服务器 IP 地址给用户,同时还可以避免运行商 DNS 劫持。具体来说, SDK 会通过发报文(类似系统向 DNS 运营商发的报文)向 httpDNS 做一个 HTTP 请求(也是通过 IP 直接请求),请求通过后拿到对应域名,然后进行 IP 直连,完成资源或者数据接口请求。

首字符串展示优化

采用loading图或者骨架屏
loading图无法让用户感知到当前的页面加载进度,为了解决这个问题,所以采用骨架屏

卡顿

如果用户说自己的页面比较卡,先通过性能分析平台查看指标,5帧超过50ms,属于比较严重的卡顿。
一般问题可能是,浏览器主线程和合成线程调度不合理,以及计算耗时操作

浏览器主线程和合成线程调度不合理

一般采用transform触发GPU硬件加速渲染

计算耗时操作

空间换时间或者时间换空间
计算耗时操作,比如频繁删除DOM,就可以采用DocumentFragment上操作,再将DOM更新到页面中。
如果是比较大的下载任务,可以通过定时器切割,下载下来然后拼装起来
image.png

上线前的性能专项测试

业务上线前,对性能指标进行测试,确保上线后达标

确定性能测试方案

性能SDK

就是通过做好的性能SDK来测试。但是问题在于请求数量太少,内网访问量有限,性能可视化平台上展现。

录制视频

自动化录制

通过andorid studio的aab工具录制,电脑上可以模拟手机。
通过执行 screenrecord 命令启动录屏功能

1
adb shell screenrecord --time-limit 10 /sdcard/perf.mp4

录屏时间为10s,不设置默认180s,之后会存储到/sdcard/ 目录下,命名为 video.mp4。
然后打开App中的页面,页面加载结束之后按ctrl+c结束录制。
可能会出现的问题:

  • 设备分辨率过高无法录制,需要指定分辨率来录制,命令adb shell screenrecord --size 1280*720 /sdcard/perf.mp4
  • 模拟器无法模拟晃动,多点操作。另外性能上也会慢一点,取决于电脑性能上限
  • 不要旋转手机,会造成画面切断
  • 命令行显示log,有助于调试adb shell screenrecord --time-limit 10 --verbose /sdcard/perf.mp4

    手动录制

    通过手机自带的视频录制来进行。

    设定性能测试标准

    白屏时间 = 白屏最后-帧的时间-点击起始帧时间
    首屏加载时间 = 内容完全加载出来的一刻 - 点击起始帧时间

image.png
卡顿标准是连续5帧超过50ms,判断为卡顿,单帧超过250ms为严重卡顿

性能测试环境搭建及测试

一般页面在集成到App 中时候,需要性能结果达标,需要在不同的网络环境中进行测试。
一般可以实地测试,去网络比较差的地方进行测试。
2G网络,在一定区域内,使用2G网络的人越少信号越好,比如半夜。可以到周围星巴克之类的,进行性能测试,腾讯微信团队也是如此,他们对第三方接入的 H5 ,会到周围各种网络环境下进行性能测试。

  • 4G信号,容易受物体遮挡或者天气影响,空旷地点,天气晴朗会好很多,信号塔附近会更好
  • 2G/3G,信号稳定,在办公区域有wifi信号强的地方,使用弱网人少,情况会不错,星巴克等咖啡厅是首选地点

特定网络下只需要关注首屏时间达标即可,不需要关注秒开率。
视频录制完之后,录制的视频通过分帧计算方式进行,也就是拆分为一帧一帧,主要分为3步:

  • 安装openCV(mac 通过brew,其他环境通过Anaconda来安装),python,通过python的VideoCapture将视频分帧存储
  • 分帧出来的图片命名,使用当前播放时间毫秒命名存储图片
  • 首屏时长计算=首屏结束帧文件名

最后用时长和标准对比即可知道是否达标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cv2
vc = cv2.VideoCapture(r'C:\Users\admin\Desktop\1.mp4') # 读入视频文件,命名cv
n = 1 # 计数
if vc.isOpened(): # 判断是否正常打开
rval, frame = vc.read()
else:
rval = False

timeF = 10 # 视频帧计数间隔频率

i = 0
while rval: # 循环读取视频帧
rval, frame = vc.read()
if (n % timeF == 0): # 每隔timeF帧进行存储操作
i += 1
print(i)
cv2.imwrite(r'C:\Users\admin\Desktop\1/{}.jpg'.format(i), frame) # 存储为图像
n = n + 1
cv2.waitKey(1)
vc.release()

这段代码是通过CV2库,通过VideoCapture读入视频文件,设置每10ms,循环获取视频,然后写入目录为.jpg文件。
image.png
2788.jpg是白屏结束时间,也就是2788ms。Wi-Fi下这个时间比较慢,需要优化。
其中我们判断首屏需要判断视频关键帧,也就是时间点当两张图片的变化值小于5%就可以认为图片趋于稳定,两张图片95%内容都不发生改变就可以认为是视觉稳定的时间点,也就是首屏时间点。
image.png