0

    浅谈Web容器设计的边界和目标

    2023.07.26 | admin | 142次围观

    本文是笔者参与UC浏览器新一代Web容器架构方案的设计、建设、业务落地过程的一些总结和思考。

    前言

    在移动端项目的落地过程中,有很多技术方案可供选择,如Native、Flutter、H5……但在业务中选择哪一种技术方案,当然是需要结合业务和技术的现状和历史沉淀来看。

    就历史沉淀而言,UC是做浏览器的,在对Webview优化上的积累自然也是最多。由于UC有对浏览器内核有定制优化的能力,很多时候对Web的优化和问题从前端侧可能是很难找原因,但从内核的“上帝视角”却很容易找到思路和解法。在浏览器里面做业务,只要没有超过Web容器的能力范围,我们一般会优先考虑用Web技术来满足业务的诉求。

    当然,要想准确把握Webview的能力范围并不是件容易的事,而不同人或团队对于Webview能力边界的理解也是不太一样的。当我们在讨论一项技术的边界时,关键的不是了解该技术能做好什么,而是知道它做不好什么。

    注意,说的是做不好,不是做不了。很多时候,我们所说的“做不好”指的是,达不到最优秀的用户体验,通常与使用Native并经过优化后的效果进行对比。在用户体验竞争越来越激烈的情况下,每个产品都期望能用最好的天花板最高的技术来落地。

    但在现实的技术方案设计中,除了考虑技术所能达到的最优效果外,还需要综合考虑开发成本、开发周期、维护效率、线上风险等因素,以及根据业务发展、人力资源配置情况,综合考虑后最终选择一个ROI最高的方案。

    好了,方案选择的路径不是这里讨论的重点,还是来聊聊动态容器吧。下面笔者将列举一些典型的用Webview技术的Cover起来有难度的场景。

    Web 容器的边界场景

    (一)复杂的媒体播控

    1. 视频播控

    并不是说Webview(H5)、小程序处理不了视频,而是对于视频的细节处理不够好。我们都知道web原生的video播放控件功能单一,没有快进/退、倍速、音量调节、亮度调节、对缓冲无感知等问题。此外,动态容器处理视频还有以下常见问题:

    包含多视频的长列表滚动到可视范围内自动播放,技术以native/flutter为主

    当然,webview处理video带来的细节体验问题还有不少,有一些问题通过native托管可以有效解决。在各大App自有业务内,对Webview的video播放器的优化基本都走native托管的模式,在技术实现上叫混合渲染或同层渲染。

    2. 音频播控

    音频媒体资源的播控问题与video是类似的,webview内的audio标签也存在功能单一,没有快进/退、倍速、音量调节、对缓冲无感知等问题。

    在我们的业务中涉及的音频播控场景比较少,目前为止包含音频播控的场景主要是图文的语音播报功能,由于需要对音频播控在App保活期间全局生效,音频播控的落地页面自然不能使用h5的audio标签来处理TTS音频,而是对接Native自定义的语音控制事件体系,页面则绘制一个播控音频的UI面板。

    目前,针对音频播控的诉求行业主要方案是Native base,Flutter实现应该也没问题,Web的实现相对较少。目前,我们已完成了此场景Web化,这里需要解决的边界问题是在后台模式下的页面保活和生命周期拓展。

    3. 动图播控

    动图包括gif、apng、webp等,在动态容器只有img标签一种图片处理方式,在一些动图很多的场景,就很难实现对动图的播放有效控制。例如:

    实际上,业务需要动图播放可控的功能,要解决的实际问题是动图可以逐帧暂停/播放、播放开始和结束都有对应的事件,也就是需要一个功能完备的动图播放器,而不只是用封面替换动图来模拟实现暂停而又不知道最后一帧啥时候完成的半成品。

    目前针对动图进行有序播控的诉求,我们采用的是Flutter方案,本质是Native的实现,而在Webview容器内可以通过类似视频播放器一样,通过同层渲染技术实现对动图的逐帧播控。

    (二)高性能的长列表

    1. 长列表的性能问题

    在Web容器下,性能是长列表面临的最棘手问题。问题会表现为:

    2. 行业方案分析

    对于这些问题,前端业界有很多解决方案,解决思路大致两个方向:

    图片过多,这是引起性能问题的最常见因素,因此在有大量图片的页面中,我们都会使用 lazyload 的方式按需加载图片,长列表的场景也是一样的。

    业界普遍的解决方案是虚拟长列表,根据列表容器的可视范围,动态计算出在可视范围内的列表节点 item,然后只渲染视野边界内容的 item,通过控制页面节点数避免内存线性增加。

    在具体的实现方案中,目前行业方案有:

    当然,还有很多,这里就不再一一列举。

    以上的几种方案有所差异,但设计理念基本类似。在实际的业务场景中,当容器内的每个 item 高度是动态的(等高的计算逻辑相对没那么复杂,这里不讨论),在虚拟列表页面节点数量增多的过程,背后存在大量的js逻辑计算:

    由于获取Dom元素的真实高度需渲染完成后才能获得(相对Native或Flutter可以在元素layout的过程,通过layoutBuilder的回调就可以获取其高度,无需等待元素渲染上屏),导致js计算列表元素高度需要等待Dom渲染,进而到带来不可避免的时间差。

    因此微信小程序view滚动条,在页面快速滚动的过程中,虚拟长列表在回收节点计算的过程,由于高度计算的处理逻辑需要等到Dom上屏之后,如果页面滚动速度越快,计算量也就越大,等待Dom上屏的时间间隔就越大,一旦页面的滚动速度超过一定阈值,必然出现可视区域内UI的变化速度 > 渲染速度的问题,就会表现为快速滚动的页面闪白。

    3. 闪白无法量化

    实际上,滚动的白屏问题是虚拟列表的节点被回收后引入的新问题,是一个用户的体感问题。在这里笔者用可视区域内的UI变化速度 > 渲染速度只是技术用语上的表述,尴尬的是从纯技术的角度这是一个很难用数据进行量化的问题。why?

    首先,这是一个新问题。

    如果我们没有对页面节点进行回收,那么就不存在滚动路径上页面没内容的情况,也就没有所谓的闪白问题。但不做节点回收就意味着在一个超长或无限下拉的列表中,DOM 节点会线性增大,必然导致页面占用越来越多的内存,增加更多的排版耗时,进而影响页面性能和用户操控体感。

    其次,为何无法数据量化?

    因为在 Webview 内,前端并不知道当前页面滚动速度是多少(或者在前端不能准确地用数据的方式表达),滚动曲线和滚动加速度在不同的手机和平台上也是不尽相同的,因此在不同手机上发生白屏的滚动速度阈值是不一样的。

    4. 规避快滑闪白

    那么,为了平衡快速滚动的闪白问题,可以让容器可以对页面在滚动速度的上限进行限制,这个需要客户端容器侧来处理。

    很多前端开发者应该都知道,在老旧 iOS 系统上,如果 WebView 采用的 UIView 架构,由于 UIView 和 js 运行在同一个线程,导致在 UIView 滚动时会阻塞 js 执行,因此在 UIView 的容器内虚拟长列表快速滚动带来的白屏问题是不可避免的,好在现在的 iOS 系统基本上已升级为异步线程模式的 WKWebView 了。

    目前,WKWebView 容器滚动的惯性速度和加速度的上限默认是比安卓的要低一些的(这也只是笔者对双平台的滚动对比的体感,没有量化的数据),而且iPhone手机性能通常比较好,因此虚拟长列表页面在快速滚动中,iOS闪白体感不那么明显。

    由于安卓的 WebView 容器在快速滚动情况下,页面会拥有很高的惯性速度和加速度。在极端滚动操控下,比如直接触控拖拽滚动条(参考以上的视频)或通过window.scrollTo 快速定位到某个位置,JS 逻辑还来不及计算当前滚动到的可视区域所需展示的 item 内容时,页面就已滚过了该区域,闪白问题几乎是不可避免的。

    而针对极端操控的闪白问题,可以在安卓的Web容器侧禁用滚动条的拖拽功能来规避,这并没有在根本上解决问题,但是webview这种机制不一定全是缺点,在大多数场景下,普通速度滑动h5的列表滑动也是很顺畅的,基本也感觉不到白屏,而技术角度看webview内存占用会比flutter低,这其实也是一种优势。

    5. 长列表的优化思路

    如果业务上,不得不使用长列表,在前端的优化技巧层也是有一些方式方法的。

    比如,让列表渲染的 item 不只是可视范围内的 item,而是会在上下边界部分预留足够的buffer,这样可以缓解问题。在双端的具体优化策略上,双端冗余buffer也可以做差异处理。在上下边界的冗余 item 数量,iOS 可以冗余少一些(性能好,但内存少),而安卓则多一些(设备内存大,就多占内存)。

    或者,在需要动态计算列表高度的时候将过程简化,比如原来需要每一个参与布局的列表item都需要计算一次,是否可以采用“分组计算”的策略,例如10个item分为1组,回收节点也是按组进行,这样回收算法的复杂度就可能只有原来的1/10。

    6. 边界的选择

    不管对长列表采用怎样的dom节点回收技术,都必定会面临在用户极端操控下 UI 的变化速度 > 渲染速度问题,在现有的浏览器JS执行必然阻塞Dom渲染的模型下,而且Webview内核层面定义没有透露更多的动态渲染处理API之前,此问题暂时没有彻底的解法。在对用户体验较高的长列表场景,我们倾向于选择Flutter,如果是一级核心场景,主流方案还是Native。

    当然,问题虽如此,这并不意味在h5长列表中对非可视区域的节点进行回收是个不必要的设计,毕竟极速滚动的闪白还是一个比较极端场景,很多时候产品对于用户的体验并不没有那么特别苛刻。或许也不应该那么苛刻,特别是人力有限的情况下,毕竟也要综合考虑ROI,但开发者最好要知道技术边界在哪里,避免掉坑里。

    (三)复杂的视差互动

    视差互动(Parallax Effect)或滚动(Parallax Scrolling)指操控网页滚动过程中,同时实现多个元素以不同的速度移动,形成立体的运动效果以提供出色的视觉体验。

    1. 视差互动的案例分析

    先来看看两个真实业务场景的栗子(视频):

    以上视频的栗子是Flutter实现的视差效果,含有两种视差:

    如果用H5的方式来做这个需求,这两个效果是做不到滑动操控和UI变换的那种顺滑体感。这个本质原因是js是单线程的,当js在执行时DOM渲染会被阻塞,一帧内要做多件事情就可能会出现掉帧,所以做不到丝滑体感。

    以上的栗子是基于Flutter实现的视差,是目前比较常见的视频播控落地页的交互模式。在这个业务场景中是一个可以横向切换的多页容器,同时每个播放页面又是一个可上下滚动的嵌套容器:

    这个栗子里面包含的边界问题是复杂嵌套滚动的顺滑切换,目前业界实现类似效果的技术方案主要是Nativa或Flutter,没有见过H5实现的效果。

    再看一个相同业务的Flutter与H5差异对比栗子。

    以上是相同页面的两种技术实现对比,头部的视差互动在滑动操控时,H5有明显的UI抖动,Flutter则不会。可以看到,在我们的业务中,Flutter实现了title随着容器上推渐显和下拉而渐隐,H5做不到顺滑的渐隐渐显过渡,降级的分享页只能取消效果。

    2. 为什么Web实现视差有边界

    当我们用Webview来实现复杂的视差交互时,为何会触及Web的边界?

    究其原因是复杂的视差互动大多需要通过js计算受控目标的Dom实时位置,不断循环“读取dom位置→计算dom位置→改变dom位置”,如果受控目标过多(视差效果通常是2个或以上受控目标微信小程序view滚动条,且每个目标采用不同的运动曲线),必然会带来js计算耗时>16.67ms进而导致UI的抖动,就会给人一种互动动画不顺畅的体感。

    在前端的业务场景中,复杂一些的动画可以采用css动画来实现,顺畅度会比js实现好很多。这是因为css动画本质是由渲染内核提供的动画组件能力,它和js是异步的,不会因为js运行而阻塞。但有一个问题,css动画的运行状态在js侧没有感知的机制,如果用js和css混合来处理视差、动画会存在两者衔接不顺的问题,这就违背了采用视差效果的初衷。

    (四)复杂的多tab页面

    在现在App业务场景中,多tab的页面是非常常见的UI交互设计,多tab页面设计将相同类型的信息聚合到相同的tab内,不同的分类则按tab横向拓展,这样可以在有限的屏幕范围内尽可能多的容纳更多信息。

    1. 多Tab页面的技术难点

    版权声明

    本文仅代表作者观点。
    本文系作者授权发表,未经许可,不得转载。

    发表评论