# 版权赛事详情页的体验优化

优化时间:2024.3.20

# 背景

目前在职主要负责的内容是抖音等宿主的端内 H5/Lynx(字节内部的高效跨平台框架)页面的编写

其中我负责的一个页面:赛事详情页,是抖音游戏(游戏赛事直播间、赛事搜索)赛事的重要信息呈现页面。

live_scene

search_scene

example_shot

不同的版权赛事厂商会提供接口返回我们指定的数据结构,实现赛事信息的呈现。

以 PEL 春季赛为例,在 2.29-3.3 的四天比赛中,日均看播 UV 超 1xx 万,而页面的峰值 PV 也达 1xx 万

但是页面的首屏性能颇为不佳(尤其是在 Android 机型下)

# 性能现状

我取了一段比赛进行时(访问高峰期的时间范围)的性能数据作为基准样本,分析 ActualFMP、Load 事件、秒开绿

ActualFMP 定义:接口返回时间 - window.performance.timing.navigationStart

这个性能指标接近于 LCP,在此页面中接近于真实的用户看到有意义内容的时间。

# 线上统计

指标类别统计分位表现备注
Load90 分位iOS:700ms-800ms 左右,Android:1.3s
ActualFMp50 分位iOS:450ms 左右,Android:900ms 左右
ActualFMP90 分位iOS:900ms 左右,Android:1.5s~1.7s
TotalActualFMP(ActualFMP + 容器启动时间)50 分位iOS:800ms 左右,Android:1.3s
秒开率(取 TotalActualFMP≤1s 占总量的比值)-iOS:81.41%,Android:24.53%。整体:54.84%

# 线下具体分析

以我手上的 Android 测试机为例,将网络限制至条件下述进行测试:

  • 8Mb/s 的上传 & 下载
  • 50ms 响应时间

得到 window.performance.timing(解读见最后) 以及下述的监控数据:

{
    "connectStart": 1719898088842,
    "navigationStart": 1719898088839,
    "secureConnectionStart": 0,
    "fetchStart": 1719898088842,
    "domContentLoadedEventStart": 1719898090267,
    "responseStart": 1719898089287,
    "domInteractive": 1719898089442,
    "domainLookupEnd": 1719898088842,
    "responseEnd": 1719898089295,
    "redirectStart": 0,
    "requestStart": 1719898088925,
    "unloadEventEnd": 0,
    "unloadEventStart": 0,
    "domLoading": 1719898089313,
    "domComplete": 1719898090301,
    "domainLookupStart": 1719898088842,
    "loadEventStart": 1719898090302,
    "domContentLoadedEventEnd": 1719898090268,
    "loadEventEnd": 1719898090302,
    "redirectEnd": 0,
    "connectEnd": 1719898088842
}
指标定义timestamp
TTFB发出页面请求到接收到应答数据第一个字节的时间总和448ms
DOMReady页面 DOM 树创建完成1426ms
DCL(DOM Content Loaded)HTML 加载与解析完成1627ms
FCP(First Contentful Paint)首次内容绘制时间1668ms
Load资源加载完毕 loadEventEnd - navigationStart1675.9ms
LCP(Largest Contentful Paint)最大内容绘制2080ms

分析

# 结论

从图内可以看到,浏览器先是加载 HTML,然后并行加载 js 模块以及页面样式表

这些模块的大小共计 203KB(编码),其中最有优化空间的主 js 模块花了 773.34 ms(762.772ms 网络 + 10.568ms 资源加载)加载,接着长达一个 160ms 的 long task 去解析并执行 js

直到 1428ms 时刻处理完成并触发 DCL。

# 优化思路

first_paint

这些流程一般来说都是串行的,只要我们能把一些串行的环节改造成并行,或者降低该环节的耗时,就可以达到优化首屏的效果

针对这个页面的一些特点,我定制了如下的思路:

think

  • 前置
    • 将首屏的接口进行 prefetch,使页面初始化与请求数据并行,少了一个环节,预计优化 100ms-200ms
    • 主 HTML 资源使用内部框架的能力进行 Preload。
  • 简化
    • 首屏时把非首屏可见内容延迟渲染
    • 干掉类似功能的依赖,降低包体积
    • Treeshaking。例如 Lodash 替换为 Loadsh-es,才能配合 Treeshaking 按需引入
    • 版本相近的间接依赖,尽可能锁同一版本
  • 拆分
    • 使用直播的插件方案,将一些高频使用的库外置拆分出去,有效减少主包大小并利用并行加载。
    • 将一些非首屏必须的依赖进行动态导入

# 优化效果

在线包、离线包体积均降了 10%(374K -> 339K)

数据预取下首屏直出效果更强、几乎看不见 loading

以下为三月底统计数据

场景效果备注
普通搜索入口 (ActualFMP 90 分位)Android:1.29s => 1.03s (-20%)。iOS:838ms => 529ms(-36.9%)。整体:1145ms => 879.8ms (-23.1%)

秒开率(整体):54.84% => 73.43%(优化上线一天后)=> 85.48%(上线三月后离线率较高时)

# 附:window.performance 采集性能数据的解读

performance-exam

# 时间节点

  • 初始化
    • navigationStart :完成卸载前一个文档的时间点
    • redirectStart :若有重定向,则为重定向开始时间,否则为 0
    • redirectEnd :若有重定向,则为重定向结束时间,否则 0
    • unloadStartunloadEnd :若当前文档和上一个文档来自不同的源(origins),设置这两为 0,否则记录实际时间。
  • 请求
    • fetchStart :浏览器发起资源请求,或者读取缓存(如果有)的开始时间
    • domainLookupStart :查询 DNS(若有)的开始时间。如果没有发起查询,那就保持与 fetchStart 一致
    • domainLookupEnd :同上,只不过表示结束时间
    • connectStart :建立 TCP 连接的开始时间(如果需要建立连接),否则和 domainLookupEnd 相同
    • connectEnd:同上,只不过表示结束时间
    • secureConnectionStart:HTTPS 的安全连接握手过程时间前,如果没有使用 HTTPS 则为 0。或者不可用时返回 undefined
    • requestStart:客户端发送请求的时刻。
    • responseStart:客户端收到应答首个字节的时刻
    • responseEnd:客户端收到应答最后一个字节的时刻
  • 解析渲染阶段
    • domLoading:用户代理设置当前文档为 loading 状态,一般为浏览器即将开始解析第一批收到的 HTML 文档字节时
    • domInteractive:用户代理设置当前文档为可交互,一般为浏览器完成对所有 HTML 解析并且 DOM 构建完成的时间点(DOM 准备就绪的时间点
    • domContentLoaded:用户代理触发 DCL 的时间,一般为 DOM 准备就绪且没有样式表阻止 JS 执行的时刻(可以开始构建渲染树),DOM、CSSOM 均准备就绪
    • domComplete:用户代理设置文档为 complete,即所有处理完成,并且网页上的所有资源(图片)都下载完毕。
    • loadEventStart:用户代理触发 Load 事件之前
    • loadEventEnd:用户代理完成 Load 事件

performance-exam

# 时刻范围 / 耗时、意义

这些指标基本都是仅在双方非 0 值时计算有意义。

  • Redirect(重定向时间):rediectEnd - redirectStart
  • AppCache(缓存): domainLookupStart - fetchStart
  • DNS:domainLookupEnd - domainLookupStart
  • TCP:connectEnd - connectStart
  • SSL:connectEnd - secureConnectionStart
  • Request:reponseStart - requestStart
  • Response: responseEnd - responseStart
  • Load(页面完全加载总时间): LoadEventEnd - navigationStart
  • DOMReady:domContentLoadedEventStart - fetchStart
  • DOMParse:domInteractive - responseEnd
  • ResourceLoad: LoadEventStart - domContentLoadedEventEnd
  • TTFB(发出页面请求到接收到应答数据第一个字节的时间总和): responseStart - navigationStart

# 参考

Navigation Timing (w3.org)

Navigation Timing Level 2 (w3.org)

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

orange 微信支付

微信支付

orange 支付宝

支付宝

orange 贝宝

贝宝