百度App自2016年上半年尝试Feed流业务形态,至2017年下半年,历经10个版本的迭代,基本完成了产品形态的初步探索。在整个Feed流形态的闭环中,新闻详情页(文中称为落地页)作为重要的组成部分,如果打开页面后,loading时间过长,会严重影响用户体验。因此我们针对落地页这种H5的首屏展现速度进行了长期优化,本文会详细阐述整个优化思路和技术细节
二、方法论
通过分析用户反馈,发现当时的落地页从点击到首屏展现平均需要3s的时间,每次用户兴致勃勃的想要浏览感兴趣的文章时,却因为过长的loading时间,而不耐烦的选择了back。为了提升用户体验,我们进行了以下工作:
- 通过用户反馈、QA测试等多种渠道,发现落地页首屏加载慢问题
- 定义首屏性能指标(首屏含图,以图片加载为准;首屏无图,以文字渲染结束为准)
- NA、内核、H5三方针对自己加载H5的流程进行划分并埋点上报
- 统计侧根据三端上报的数据产出平均值、80分位值的性能报表
- 分析性能报表,找到不合理的耗时点,并进行优化
- 以AB实验方式,对比优化前后的性能报表数据,产出优化效果,同时评估用户体验等相关指标
- 按照长期优化的方式,不断分析定位性能瓶颈点并优化,以AB实验方式评估效果,最终达到我们的落地页秒开目标
三、Hybrid方案简述及性能瓶颈
(一)方案简述
优化之前,我们与业内大多数的App一样,在落地页的技术选型中,为了满足跨平台和动态性的要求,采用了Hybrid这种比较成熟的方案。Hybrid,顾名思义,即混合开发,也就是半原生半Web的方式。页面中的复杂交互功能采用端能力的方式,调用原生API来实现。成本低,灵活性较好,适合偏信息展示类的H5场景
下面用一张图来表示百度App中Hybrid的实现机制和加载流程
(二)性能瓶颈
为了分析Hybrid方案首屏展现较慢的原因,找到具体的性能瓶颈,客户端和前端分别针对各自加载过程中的关键节点进行埋点统计,并借由性能监控平台日志进行展示,下图是截取的某一天全网用户的落地页首屏展现速度80分位数据
各阶段性能点可以按Hybrid加载流程进行划分,可以看到,从点击到首屏展现,大致需要2600ms,其中初始化NA组件需要350ms,Hybrid初始化需要170ms,前端H5执行JS获取正文并渲染需要1400ms,完成图片加载和渲染需要700ms的时间
我们具体分析下四个阶段的性能损耗主要发生在哪些地方: 1) 初始化NA组件 从点击到落地页框架初始化完成,主要工作为初始化WebView,尤其是第一次进入(WebView首次创建耗时均值为500ms)
- Hybrid初始化
这个阶段的工作主要包含两部分,一个是根据调起协议中传入的相关参数,校验解压下发到本地的Hybrid模板,大致需要100ms的时间;此外,WebView.loadUrl执行后,会触发对Hybrid模板头部和Body的解析
- 正文加载&渲染
执行到这个阶段,内核已经完成了对Hybrid模板头部和body的解析,此时需要加载解析页面所需的JS文件,并通过JS调用端能力发起对正文数据的请求,客户端从Server拿到数据后,用JsCallback的方式回传给前端,前端需要对客户端传来的JSON格式的正文数据进行解析,并构造DOM结构,进而触发内核的渲染流程;此过程中,涉及到对JS的请求,加载、解析、执行等一系列步骤,并且存在端能力调用、JSON解析、构造DOM等操作,较为耗时
- 图片加载
第(3)步中,前端获取到的正文数据包含落地页的图片地址集,在完成正文的渲染后,需要前端再次执行图片请求的端能力,客户端这边接收到图片地址集后按顺序请求服务器,完成下载后,客户端会调用一次IO将文件写入缓存,同时将对应图片的本地地址回传给前端,最终通过内核再发起一次IO操作获取到图片数据流,进行渲染;
总体来看,图片渲染的时间依赖前端的解析效率、端能力执行效率、下载速度、IO速度等因素
通过分析,延伸出对Hybrid方案的一些思考:
- 渲染为什么这么慢
- 图片请求能否提前
- 串行逻辑是否可以改为并行
- WebView初始化时间是否还可以优化
四、百度App落地页优化方案
(一)CloudHybrid
基于之前对Hybrid性能的分析,我们内部孵化了一个叫做CloudHybrid的项目,用来解决落地页首屏展现慢的痛点;一句话来形容CloudHybrid方案,就是采用后端直出+预取+拦截的方式,简化页面渲染流程,提前化&并行化网络请求逻辑,进而提升H5首屏速度
1.后端直出-快速渲染首屏
a. 页面静态直出
对于Hybrid方案来说,端上预置和加载的html文件只是一个模板文件,内部包含一些简单的JS和CSS文件,端上加载HTML后,需要执行JS通过端能力从Server异步请求正文数据,得到数据后,还需要解析JSON,构造DOM,应用CSS样式等一系列耗时的步骤,最终才能由内核进行渲染上屏;为了提升首屏展示速度,可以利用后端渲染技术(smarty)对正文数据和前端代码进行整合,直出首屏内容,直出后的html文件包含首屏展现所需的内容和样式,内核可以直接渲染;首屏外的内容(包括相关推荐、广告等)可以在内核渲染完首屏后,执行JS,并利用preact进行异步渲染
百度APP直出方案:
对于客户端来说,从CDN中拉取到的html都是已经在server渲染好首屏的,这样的内容无需二次加工,展现速度可以大大提升,仅直出一点,手百Feed落地页的首屏性能数据就从2600ms优化到2000ms以内
b. 动态信息回填
为了保证首屏渲染结果的准确性,除了在server侧对正文内容和前端代码进行整合外,还需要一些影响页面渲染的客户端状态信息,例如首图地址、字体大小、夜间模式等 这里我们采用动态回填的方式,前端会在直出的html中定义一系列特殊字符,用来占位;客户端在loadUrl之前,会利用正则匹配的方式,查找这些占位字符,并按照协议映射成端信息;经过客户端回填处理后的html内容,已经具备了展现首屏的所有条件
c. 动画间渲染
先看下优化前后效果:
优化后-图2
正常来说,直出后的页面展现速度已经很快了;但在实际开发中,你可能会遇到即使自己的数据加载速度再快,仍然会出现Activity切换过程中无法渲染H5页面的问题(可以通过开发者模式放慢动画时间来验证),产生视觉上的白屏现象(如上面图1) 我们通过研究源码发现,系统处理view绘制的时候,有一个属性setDrawDuringWindowsAnimating,从命名可以看出来,这个属性是用来控制window做动画的过程中是否可以正常绘制,而恰好在Android 4.2到Android N之间,系统为了组件切换的流程性考虑,该字段为false,我们可以利用反射的方式去手动修改这个属性,改进后的效果见上面图2
/**
- 让 activity transition 动画过程中可以正常渲染页面
/
private void setDrawDuringWindowsAnimating(View view) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
// 1 android n以上 & android 4.1以下不存在此问题,无须处理
return;
}
// 4.2不存在setDrawDuringWindowsAnimating,需要特殊处理
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
handleDispatchDoneAnimating(view);
return;
}
try {
// 4.3及以上,反射setDrawDuringWindowsAnimating来实现动画过程中渲染
ViewParent rootParent = view.getRootView().getParent();
Method method = rootParent.getClass()
.getDeclaredMethod(“se
tDrawDuringWindowsAnimating”, boolean.class);
method.setAccessible(true);
method.invoke(rootParent, true);
} catch (Exception e) {
e.printStackTrace();
}
}
/* - android4.2可以反射handleDispatchDoneAnimating来解决
*/
private void handleDispatchDoneAnimating(View paramView) {
try {
ViewParent localViewParent = paramView.getRootView().getParent();
Class localClass = localViewParent.getClass();
Method localMethod = localClass.getDeclaredMethod(“handleDispatchDoneAnimating”);
localMethod.setAccessible(true);
localMethod.invoke(localViewParent);
} catch (Exception localException) {
localException.printStackTrace();
}
}
2.智能预取-提前化网络请求
经过直出的改造之后,为了更快的渲染首屏,减少过程中涉及到的网络请求耗时,我们可以按照一定的策略和时机,提前从CDN中请求部分落地页html,缓存到本地,这样当用户点击查看新闻时,只需从缓存中加载即可
手百预取服务架构图
目前手百预取服务支撑着图文、图集、视频、广告等多个业务方,根据业务场景的不同,触发时机可以自定义,也可以遵循我们默认的刷新、滑停、点击等时机,此外,我们会对预取内容进行优先级排序(根据资源类型、触发时机),会动态的根据当前手机状态信息进行并发控制和流量控制,在一些降级场景中,server还可以通过云控的方式来控制是否预取以及预取的数量
3.通用拦截-缓存共享、请求并行
在落地页中,除了文本外,图片也是重要的组成部分。直出解决了文字展现的速度问题,但图片的加载渲染速度仍不理想,尤其是首屏中带有图片的文章,其首图的渲染速度才是真正的首屏时间点 传统Hybrid方案,前端页面通过端能力调用NA图片下载能力来缓存和渲染图片,虽然实现了客户端和前端图片缓存的共享,但由于JS执行时机较晚,且多次端能力调用存在效率问题,导致图片渲染延后
初步改进方案:为了提升图片加载速度,减少JS调用耗时,改为纯H5请求图片,速度虽然有所提升,但是客户端和前端缓存无法共享,当点击图片调起NA图片查看器时,无法做到沉浸式效果,且仍需重复下载一次图片,造成流量浪费 终极方案:借由内核的shouldInterceptRequest回调,拦截落地页图片请求,由客户端调用NA图片下载框架进行下载,并以管道方式填充到内核的WebResourceResponse中
此方案在满足图片渲染速度的同时,解耦了客户端和前端代码,客户端充当server角色,对图片进行请求和缓存控制,保证前端和客户端可以共用图片缓存,改造后的方案,非首图展现流程,页面不卡顿,首屏80分位值缩短80ms~150ms
效果如下:
优化前Hybrid方案
优化后通用拦截方案
4.整体方案流程
非首图展现流程,页面不卡顿,首屏80分位值缩短80ms~150ms
效果如下:
优化前Hybrid方案
[外链图片转存中…(img-twaTZasH-1642753243931)]
优化后通用拦截方案
[外链图片转存中…(img-kDmAqQjM-1642753243931)]