没关注?伸出手指点这儿---
本文转载自百度App技术(订阅号 id:gh_59f5931152fe),经平台同意授权转载。
1、背景
百度App自2016年上半年尝试Feed流业务形态,至2017年下半年,历经10个版本的迭代,基本完成为了制品形态的初步探索。在全部Feed流形态的闭环中,资讯详情页(文中叫作为落地页)做为要紧的构成部分,倘若打开页面后,loading时间过长,会严重影响用户体验。因此呢咱们针对落地页这种H5的首屏展现速度进行了长时间优化,本文会仔细阐述全部优化思路和技术细节。
2、办法论
经过分析用户反馈,发掘当时的落地页从点击到首屏展现平均必须3s的时间,每次用户兴致勃勃的想要浏览感兴趣的文案时,却由于过长的loading时间,而不耐烦的选取了back。为了提高用户体验,咱们进行了以下工作:
经过用户反馈、QA测试等多种途径,发掘落地页首屏加载慢问题
定义首屏性能指标(首屏含图,以照片加载为准;首屏没图,以文字渲染结束为准)
NA、内核、H5三方针对自己加载H5的流程进行划分并埋点上报
统计侧按照三端上报的数据产出平均值、80分位值的性能报表
分析性能报表,找到不恰当的耗时点,并进行优化
以AB实验方式,对比优化前后的性能报表数据,产出优化效果,同期评定用户体验等关联指标
根据长时间优化的方式,持续分析定位性能瓶颈点并优化,以AB实验方式评定效果,最后达到咱们的落地页秒开目的
3、Hybrid方案简述及性能瓶颈
(一)方法简述
优化之前,咱们与业内大都数的App同样,在落地页的技术选型中,为了满足跨平台和动态性的需求,采用了Hybrid这种比较成熟的方法。Hybrid,顾名思义,即混合研发,亦便是半原生半Web的方式。页面中的繁杂交互功能采用端能力的方式,调用原生API来实现。成本低,灵活性较好,适合偏信息展示类的H5场景。
下面用一张图来暗示百度App中Hybrid的实现机制和加载流程。
(二)性能瓶颈
为了分析Hybrid方法首屏展现较慢的原由,找到详细的性能瓶颈,客户端和前端分别针对各自加载过程中的重要节点进行埋点统计,并借由性能监控平台日志进行展示,下图是截取的某一天全网用户的落地页首屏展现速度80分位数据。
各周期性能点能够按Hybrid加载流程进行划分,能够看到,从点击到首屏展现,大致必须2600ms,其中初始化NA组件必须350ms,Hybrid初始化必须170ms,前端H5执行JS获取正文并渲染必须1400ms,完成照片加载和渲染必须700ms的时间。
我
们详细分析下四个周期的性能损耗重点出现在那些地区:
1) 初始化NA组件 从点击到落地页框架初始化完成,重点工做为初始化WebView,尤其是第1次进入(WebView首次创建耗时均值为500ms)
2) Hybrid初始化
这个周期的工作重点包括两部分,一个是按照调起协议中传入的关联参数,校验解压下发到本地的Hybrid模板,大致必须100ms的时间;另外,WebView.loadUrl执行后,会触发对Hybrid模板头部和Body的解析
3) 正文加载&渲染
执行到这个周期,内核已然完成为了对Hybrid模板头部和body的解析,此时必须加载解析页面所需的JS文件,并经过JS调用端能力发起对正文数据的请求,客户端从Server拿到数据后,用JsCallback的方式回传给前端,前端必须对客户端传来的JSON格式的正文数据进行解析,并构造DOM结构,从而触发内核的渲染流程;此过程中,触及到对JS的请求,加载、解析、执行等一系列过程,并且存在端能力调用、JSON解析、构造DOM等操作,较为耗时
4) 照片加载
第(3)步中,前端获取到的正文数据包括落地页的照片位置集,在完成正文的渲染后,必须前端再次执行照片请求的端能力,客户端这边接收到照片位置集后按次序请求服务器,完成下载后,客户端会调用一次IO将文件写入缓存,同期将对应照片的本地位置回传给前端,最后经过内核再发起一次IO操作获取到照片数据流,进行渲染;
总体来看,照片渲染的时间依赖前端的解析效率、端能力执行效率、下载速度、IO速度等原因
经过分析,延伸出对Hybrid方法的有些思考:
渲染为何这么慢
照片请求能否提前
串行规律是不是能够改为并行
WebView初始化时间是不是还能够优化
4、百度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渲染好首屏的,这般的内容没需二次加工,展现速度能够大大提高,仅zj出一点,Feed落地页的首屏性能数据就从2600ms优化到2000ms以内。 b. 动态信息回填
为了保准首屏渲染结果的准确性,除了在server侧对正文内容和前端代码进行整合外,还必须有些影响页面渲染的客户端状态信息,例如首图位置、字体体积、夜间模式等。这儿咱们采用动态回填的方式,前端会在直出的html中定义一系列特殊字符,用来占位;客户端在loadUrl之前,会利用正则匹配的方式,查询这些占位字符,并根据协议映射成端信息;经过客户端回填处理后的html内容,已然具备了展现首屏的所有要求。 c. 动画间渲染
先看下优化前后效果:
优化前-图1
优化后-图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("setDrawDuringWindowsAnimating", 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.整体方法流程
(二)新的优化尝试1.WebView预创建
为了减少WebView的性能损耗,咱们能够在合适机会提前创建好WebView,并存入缓存池,当页面必须表示内容时,直接从缓存池获取创建好的WebView,按照性能数据表示,WebView预创建能够减少首屏渲染时间200ms+。
详细以Feed落地页为例,当用户进入手百并触发Feed吸顶操作后,咱们会创建第1个WebView,当用户进入落地页后,会从缓存池中取出来渲染H5页面,为了不影响页面的加载速度,同期保准下次进入落地页缓存池中仍然有可用的WebView组件,咱们会在每次页面加载完成(pageFinish)或back退出落地页的机会,去触发预创建WebView的规律。
因为WebView的初始化必须和context进行绑定,若想实现预创建的规律,必须保准context的一致性,常规做法咱们思虑能够用fragment来实现承载H5页面的容器,这般context能够用外层的activity实例,但Fragment本身的切换流畅度存在必定问题,并且这般做限定了WebView预创建适用的场景。为此,咱们找到了一种更加完美的替代方法,即MutableContextWrapper。
Special version of ContextWrapper that allows the base context to be modified after it is initially set. Change the base context for this ContextWrapper. All calls will then be delegated to the base context. Unlike ContextWrapper, the base context can be changed even after one is already set.简单来讲,便是一种新的context包装类,准许外边修改它的baseContext,并且所有ContextWrapper调用的办法都会代理到baseContext来执行。
下面是截取的一段预创建WebView的代码: /** * 创建WebView实例 * 用了applicationContext */ @DebugTrace public void prepareNewWebView() { if (mCachedWebViewStack.size() < CACHED_WEBVIEW_MAX_NUM) { mCachedWebViewStack.push(new WebView(new MutableContextWrapper(getAppContext())));} } /** * 从缓存池中获取合适的WebView * * @param context activity context * @return WebView */ private WebView acquireWebViewInternal(Context context) { // 为空,直接返回新实例 if(mCachedWebViewStack ==null || mCachedWebViewStack.isEmpty()) { return new WebView(context); } WebView webView = mCachedWebViewStack.pop(); // webView不为空,则起始运用预创建的WebView,并且替换Context MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext(); contextWrapper.setBaseContext(context); returnwebView; }2.NA组件懒加载
a. WebView初始化完成,立刻loadUrl,没需等待框架onCreate或OnResume结束b. WebView初始完成后到页面首屏绘制完成之间,尽可能减少UI线程的其他操作,繁忙的UI线程会拖慢WebView.loadUrl的速度
详细到Feed落地页场景,因为我们的落地页包括两部分,WebView+NA评论组件,正常流程会在WebView初始化结束后,起始评论组件的初始化及评论数据的获取。因为此时评论的初始化仍处在onCreate的UI信息处理中,会严重延迟内核加载主文档的规律。思虑到用户进入落地页的时候,评论组件对用户来讲并不可见,因此将评论组件的初始化延迟到页面的pageFinish机会或firstScreenPaintFinished;80分位性能提高60ms~100ms。 3.内核优化
a. 内核渲染优化:内核中重点分为三个线程(IOThread、MainThread、ParserThread),首要IOThread会从网络端或本地获取html数据,并把数据交给MainThread(渲染线程,非常繁忙,用于JS执行,页面布局等),为了保准MainThread不被阻塞,必须额外起一个后台线程(ParserThread)用来做html的解析工作。ParserThread每解析到落地页html中带有特殊class标记的一个div标签或P标签(图中的first、second)时,就会触发一次MainThread的layout工作,并把layout后得到的高度与屏幕高度进行对比,倘若当前layout高度已然大于屏幕高度,咱们认为首屏内容已然完成布局,能够触发渲染上屏规律,不必等到整篇html所有解析完成再上屏,提前了首屏的渲染时间;80分位下,内核的渲染优化能够提高首屏速度100ms~200ms。
b. 预加载JS:预创建好WebView后,经过预加载JS(与内核约定好的JS内容,内核侧执行该JS时,只做初始化操作),触发WebView初始化规律,缩短后续加载url耗时;80分位性能提高80ms上下。
5、新的问题-流量和速度的平衡
频繁预取会带来流量的浪费:预取的命中率虽然达到了90%以上,但有效率仅有15%。
处理思路:
压缩预取的包体积,减少下行流量
少预取或不预取 (一)精简预取数据:
图文:优化直出html中内联的css、icon等数据,数据体积减少约40% (二)后端智能预取:
1) 图文:经过对图文资源进行评分,来决定4G是不是必须预取,多组AB实验最优效果劣化9.5ms2)视频:为了平衡性能和流量,在性能劣化可接受的范围内(视频起播时间劣化100ms),针对视频部分采用流量高峰期不预取的策略,减少视频总流量约7%,整体带宽峰值下降3% (三)AI智能预取
通用用户操作行径,对Feed预取进行AI预测,减少没效预取的数量。
6、总结&展望
(一)优化总结
在总结之前,先来看下整体优化的前后效果对比:
优化前
优化后
能够看到,经过一系列的优化手段,落地页已然实现了秒开效果。回顾所做的事情,从分析用户反馈到定位性能瓶颈,再到各样优化尝试,发掘所有类似的性能优化手段都能够从以下几点入手:
提前做:包含预创建WebView和预取数据
并行做:包含照片直出&拦截加载,框架初始化周期开启异步线程准备数据等
轻量化:针对前端来讲,要尽可能减少页面体积,删减不必要的JS和CSS,不仅能够缩短网络请求时间,还能提高内核解析时间
简单化:针对简单的信息展示页面,对内容动态性需求不高的场景,能够思虑运用直出替代hybrid,展示内容直接可渲染,没需JS异步加载
(二)TODO
页面的更新机制,日前方法仅适用于偏静态页面,针对动态性需求较高的业务,必须供给页面更新机制,保准每次表示的正确性
开源之路:后续计划将咱们总结下来的这套方法打包开源,前行之路必定坎坷,期盼大众多多支持
【End】
|