叶航 字节跳动技术质量 稿

导览

为了提升头条端上的搜索性能体验,我们从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特性