Fork me on GitHub

今日头条搜索品质优化 - 端到端篇

叶航 字节跳动技术质量 稿

导览

为了提升头条端上的搜索性能体验,我们从19年3/4双月开始投入专门的人力做品质优化,通过这段时间的持续投入,搜索整体的性能体验有了明显的提升,基本上与竞品对齐。中间踩过很多坑,也收获了一些经验,这里做一个总结分享,欢迎大家拍砖交流~

为了方便阅读,整体的介绍会拆分为2个部分,本篇介绍一下业务背景和整体的技术挑战,以及端到端场景的品质优化,后续会再对落地页做单独的优化介绍。

业务背景

搜索的业务场景其实比较简单,核心场景是两个:

  • 搜索结果页。也就是展示用户搜索结果的页面,也就是本篇介绍所说的“端到端”场景
  • 落地页。也就是从结果页里点击打开的新页面,可以是站内页面,也可以是站外H5、小程序等。站内页和小程序等体裁性能相对较好,我们的优化主要针对的是站外落地页

端到端场景的技术挑战

看起来很简单的一个场景,为什么会有性能瓶颈,需要专门做优化呢?

基于H5+webview的架构

搜索结果页是基于webview来实现的。相比于纯Native实现,基于webview的架构在性能体验上会有天然的瓶颈,主要是在速度和稳定性上:

  • 速度方面,webview的首次初始化、页面加载流程、webview原生网络模块、web&Ntive通信等环节都存在一定的耗时瓶颈。
  • 稳定性方面,webview的白屏、黑屏等异常也给我们持续带来很多困扰。

聊一聊技术选型背景:

选择H5是由业务形态和业务发展共同决定的。头条搜索是综搜,有大量字节如意的需求(简单说就是不同query可以出不同的结构化样式),卡片样式非常多,而且需要很高的动态性,兼具跨端(Android+iOS+M站)优势的webview+h5是不错的选择(也是早期业务刚启动时唯一的选择)。

经过2年的快速迭代,头条搜索的pv已经近2亿,前端卡片样式达到数百种,业务复杂度已经到达一个相当高的地步。在这个背景下,虽然webview架构带来很大的性能挑战,但相比较做整体技术栈的迁移(比如切Native/Lynx/Flutter)带来的巨大成本(主要是),还是继续基于webview架构来优化有更高的roi。也得益于字节有了TTWebview自研内核,为webview优化提供了非常强有力支撑。

复杂的业务&有限的资源

前面提到过搜索的业务已经比较复杂,对性能方面的影响主要在页面体积和请求耗时方面。完整主文档+核心JS xxk,在网络传输跟js执行阶段耗时都很长,除此之外,影响性能的业务复杂度更多来自头条app本身。

作为一个平台型app,头条承载了大量业务,并且由于快速迭代遗留的历史包袱,头条整体的性能压力已经很大,比如启动速度、cpu、内存等,这些压力也会直接影响到搜索业务上。比如冷启动发起搜索路径会比较长,并且刚启动时由于整体资源紧张,搜索成功率也会显著低,再比如虚拟内存紧张,会直接影响搜索页面出现黑屏的概率。

另一方面,业务众多容易产生相互影响,比如webview的使用:头条里有10个以上的业务会用到webview,其他业务使用webview不当很容易影响到搜索。同样的,搜索做webview预加载的策略,也会很容易使详情页白屏升高。也正因此,相比于其他主打搜索功能的app(百度、浏览器等),头条搜索能使用的资源是相对有限的,没办法做激进的、独占式的webveiw资源使用。

进展总结

  • 搜索成功率提高
  • 搜索80分位耗时降低

优化过程

整体思路

  1. 多端合作共建(前端、服务端、推荐后端、网络、内核)

  2. 问题发现:建立用户视角的全链路数据统计和监控

  3. 数据驱动,识别关键瓶颈,分而治之,极致优化

    1. 网页渲染架构优化
    2. 网络链路优化
    3. 后端优化
    4. 内核加载流程优化
    5. 端上耗时压缩
    6. 极致策略优化
  4. 监控完善,防劣化

  5. 组件化建设,头条Lite双端复用

指标统计和分析能力建设

核心是**建立用户视角的全链路数据统计和监控,每次搜索统计以用户点击搜索词为起点,结果页渲染出首屏为终点,中途取消均不视为成功。(**原有的统计口径是利用前端performance api统计的首屏耗时)

主要遇到的挑战是:

  1. 整个项目团队已经沿用旧口径一段时间了,口径迁移有较大成本,包括开发和信息对齐的成本;
  2. 另外由于系统webview内部实现纯黑盒,很多环节是没办法进一步分析的;

通过沟通协调,最终整个项目团队达成一致:以用户视角口径为目标、以全流程性能分析为驱动来做优化,并且与内核团队共建完成webview内核全流程打点,为后续更细致的性能分析打下基础。

指标的口径迁移和建设从最终的效果来看是非常值得的,新的口径帮助我们发现了很多被忽略的问题,而这些原本被忽略的点也是我们后续的优化空间所在:

  • 耗时方面。比如端上逻辑耗时、webview初始化耗时、js模版加载耗时、jsb通信耗时等在以往是被忽略的,但其实存在很大的优化空间;
  • 稳定性方面。旧的成功率以网络请求为口径,遗漏了用户主动取消的场景以及webview异常等;

渲染架构迭代

主要围绕前端架构和端上加载流程来做优化。通过前端页面做SSR服务端渲染改造,再结合端上预创建webview、预链接、内核PLZ优化等,在速度和成功率上取得大幅提升。

【整体的演进过程】

离线化,由模版请求数据

离线化,native代替前端请求

离线化,单卡ssr

完整ssr

基于ssr的模版预加载

【核心优化点】

  • 减少js执行
  • 并行/预执行
  • 服务端渲染,减轻端上工作
  • 流式传输,加快首屏渲染
  • 分机型策略

搜索网络链路优化

网络是整个链路的重要一环,搜索经SSR服务端渲染改造后会走两种网络

  • Webview网络:通过预链接&链接保活,请求的链接复用率提升到90%+,80分位DNS+TCP建链耗时降为0;
  • TTNet:前期和Feed共用域名,已经有较高的复用率,后期切独立域名后通过链接保活做优化,保持90%+链接复用率;

另外经过对长尾badcase分析,挖掘出了dns过长、H2链接黑洞等问题,提出并推动网络库实现兜底IP、链接保活、旁路重试等策略,对搜索网络性能/可用性有明显提升。另外在网络协议层面也推进了quic、br压缩等优化策略。

异常case治理

异常case是整个优化过程里的一大难点,也是痛点。由于webview本身的复杂性,结合业务复杂性,webview版本兼容性,厂商兼容性等,衍生出很多异常。这类问题也很难用系统性的方案来解决,各自原因不同,更多是一个不断踩坑填坑的经验积累~下面挑选2个印象深的case具体介绍下:

  • Vivo9系统异常cancel

在搜索做完SSR+内核Plz优化之后,虽然大盘性能提升了,但是仍然有不少搜不出的用户反馈。经过初步分析发现,用户反馈和异常统计在vivo9系统上都有明显的聚集性,这个现象一度让我们非常费解,因为ttwebview的存在按理说已经能规避webview厂商兼容问题。

怀着深深的疑惑逐步排查定位,最终发现是:

vivo9系统为了适配深色模式,会在webview的onPageStart/ onPageFinished/ onLoadResource/ doUpdateVisitedHistory几个时机,从framework层直接调用webview.loadUrl执行一段js来实现深色模式。而在KITKAT以上的版本,loadUrl里面执行js会导致当前正在加载的页面被cancel掉,从而导致搜索搜不出,plz特性会明显加剧这个问题。

这个故事告诉我们在Android开发里不管是任何场景,都不要忽略出现厂商兼容问题的可能性...

  • onRenderProcessGone自动恢复

原生webview在8.0系统后默认是多进程,Render进程和主进程隔离,而ttwebview在低版本也能开启多进程。在多进程webview下一个常见问题是主进程正常运行但Render进程被杀了,此时webview会通过webViewClient.onRenderProcessGone回调通知应用(所有存活的webview)做相应处理。

override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail?): Boolean {   }

此时如果webvew在onRenderProcessGone返回false,则app进程也会发生崩溃,但是如果返回true,则当前的WebView会不可用,再次使用该实例加载网页会出现白屏。为避免主进程崩溃显然需要返回true,但在搜索的业务里由于二次搜索、预创建等场景的存在,白屏的问题也很影响用户。

因此需要增加恢复的措施,在render进程发生崩溃时,使用新的WebView来代替有问题的WebView,不过由于SearchActivity内部维护着一些相关的状态,如果直接替换掉当前的Activity里的WebView的话,还需要处理这些状态,比较麻烦。因此解决方案是,启动一个新的Activity,发起一次新的搜索,这样就可以使用新的WebView实例。

极致策略优化

整个搜索链路上,经过SSR服务端渲染+网络优化,网络侧和前端侧很难进一步优化了,客户端上也经过一些常规手段优化(无用逻辑精简、异步、预加载),正常流程中耗时在10几ms内。但受限于服务端耗时较长(80分位近1s),整体搜索速度还是落后于竞品。针对于此我们提出了一些更极致的优化策略:

【请求提前】

打破常规加载流程,把搜索请求的发起时机尽可能提前,拿到了不错的收益。具体的两个场景:

  • 被动搜索场景(含页面跳转),把请求发起时机提前到Intent构造时,节省Activity生命周期执行的系统耗时~100ms;
  • 主动搜索场景(无页面跳转),把请求时机提前到手指Touch Down的时机,节省点击判定的~30ms;

【搜索预取】

针对服务端处理高耗时的问题,设计和推动实现了预取的方案,可以大幅减少后端+网络耗时。方案最大的难点在于推荐侧流量压力过大,很难支撑大批量的预取请求,因此需要通过各种策略提升预取的命中率,在命中率和覆盖率间取一个平衡,另外在流控方面也需要做好详细设计。

目前方案已经上线,小量实验收益符合预期,但受限于推荐侧压力未能扩量。目前推动推荐侧在做Cache能力,完成后预期能大幅减轻流量压力,支持预取扩量。

后续规划

经过一段时间的治理整个搜索链路已经逐步接近理想态,再往后的主要空间在于后端&推荐侧的处理耗时上(后端Cache+预取),而预取的命中率调优是一个复杂的课题,需要投入更多精力做用户的行为分析与预测,以及结合网络环境、机型性能等因素做精细化的策略;另一方面是在做更细致的性能分析(机型、系统、设备性能)等,把现有策略做的更加深入和精细,拿到更大的收益,比如独立Render进程在低内存机型上完全有可能因内存不足而负向。


本文地址:https://www.6aiq.com/article/1640455266251
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出