本文是笔者参与UC浏览器新一代Web容器架构方案的设计、建设、业务落地过程的一些总结和思考。
在移动端项目的落地过程中,有很多技术方案可供选择,如Native、Flutter、H5……但在业务中选择哪一种技术方案,当然是需要结合业务和技术的现状和历史沉淀来看。
就历史沉淀而言,UC是做浏览器的,在对Webview优化上的积累自然也是最多。由于UC有对浏览器内核有定制优化的能力,很多时候对Web的优化和问题从前端侧可能是很难找原因,但从内核的“上帝视角”却很容易找到思路和解法。在浏览器里面做业务,只要没有超过Web容器的能力范围,我们一般会优先考虑用Web技术来满足业务的诉求。
当然,要想准确把握Webview的能力范围并不是件容易的事,而不同人或团队对于Webview能力边界的理解也是不太一样的。当我们在讨论一项技术的边界时,关键的不是了解该技术能做好什么,而是知道它做不好什么。
注意,说的是做不好,不是做不了。很多时候,我们所说的“做不好”指的是,达不到最优秀的用户体验,通常与使用Native并经过优化后的效果进行对比。在用户体验竞争越来越激烈的情况下,每个产品都期望能用最好的天花板最高的技术来落地。
但在现实的技术方案设计中,除了考虑技术所能达到的最优效果外,还需要综合考虑开发成本、开发周期、维护效率、线上风险等因素,以及根据业务发展、人力资源配置情况,综合考虑后最终选择一个ROI最高的方案。
好了,方案选择的路径不是这里讨论的重点,还是来聊聊动态容器吧。下面笔者将列举一些典型的用Webview技术的Cover起来有难度的场景。
1 . 视频播控
并不是说Webview(H5)、小程序处理不了视频,而是对于视频的细节处理不够好。我们都知道web原生的video播放控件功能单一,没有快进/退、倍速、音量调节、亮度调节、对缓冲无感知等问题。此外,动态容器处理视频还有以下常见问题:
包含多视频的长列表滚动到可视范围内自动播放,技术以native/flutter为主
当然,webview处理video带来的细节体验问题还有不少,有一些问题通过native托管可以有效解决。在各大App自有业务内,对Webview的video播放器的优化基本都走native托管的模式,在技术实现上叫混合渲染或同层渲染。
音频媒体资源的播控问题与video是类似的,webview内的audio标签也存在功能单一,没有快进/退、倍速、音量调节、对缓冲无感知等问题。
在我们的业务中涉及的音频播控场景比较少,目前为止包含音频播控的场景主要是图文的语音播报功能,由于需要对音频播控在App保活期间全局生效,音频播控的落地页面自然不能使用h5的audio标签来处理TTS音频,而是对接Native自定义的语音控制事件体系,页面则绘制一个播控音频的UI面板。
目前,针对音频播控的诉求行业主要方案是Native base,Flutter实现应该也没问题,Web的实现相对较少。目前,我们已完成了此场景Web化,这里需要解决的边界问题是在后台模式下的页面保活和生命周期拓展。
动图包括gif、apng、webp等,在动态容器只有img标签一种图片处理方式,在一些动图很多的场景,就很难实现对动图的播放有效控制。例如:
实际上,业务需要动图播放可控的功能,要解决的实际问题是动图可以逐帧暂停/播放、播放开始和结束都有对应的事件,也就是需要一个功能完备的动图播放器,而不只是用封面替换动图来模拟实现暂停而又不知道最后一帧啥时候完成的半成品。
目前针对动图进行有序播控的诉求,我们采用的是Flutter方案,本质是Native的实现,而在Webview容器内可以通过类似视频播放器一样,通过同层渲染技术实现对动图的逐帧播控。
1 . 长列表的性能问题
在Web容器下,性能是长列表面临的最棘手问题。问题会表现为:
对于这些问题,前端业界有很多解决方案,解决思路大致两个方向:
图片过多,这是引起性能问题的最常见因素,因此在有大量图片的页面中,我们都会使用 lazyload 的方式按需加载图片,长列表的场景也是一样的。
业界普遍的解决方案是虚拟长列表,根据列表容器的可视范围,动态计算出在可视范围内的列表节点 item,然后只渲染视野边界内容的 item,通过控制页面节点数避免内存线性增加。
在具体的实现方案中,目前行业方案有:
当然,还有很多,这里就不再一一列举。
以上的几种方案有所差异,但设计理念基本类似。在实际的业务场景中,当容器内的每个 item 高度是动态的(等高的计算逻辑相对没那么复杂,这里不讨论),在虚拟列表页面节点数量增多的过程,背后存在大量的js逻辑计算:
由于获取Dom元素的真实高度需渲染完成后才能获得(相对Native或Flutter可以在元素layout的过程,通过layoutBuilder的回调就可以获取其高度,无需等待元素渲染上屏),导致js计算列表元素高度需要等待Dom渲染,进而到带来不可避免的时间差。
因此,在页面快速滚动的过程中,虚拟长列表在回收节点计算的过程,由于高度计算的处理逻辑需要等到Dom上屏之后,如果页面滚动速度越快,计算量也就越大,等待Dom上屏的时间间隔就越大,一旦页面的滚动速度超过一定阈值,必然出现可视区域内UI的变化速度 > 渲染速度的问题,就会表现为快速滚动的页面闪白。
实际上,滚动的白屏问题是虚拟列表的节点被回收后引入的新问题,是一个用户的体感问题。在这里笔者用可视区域内的UI变化速度 > 渲染速度只是技术用语上的表述,尴尬的是从纯技术的角度这是一个很难用数据进行量化的问题。why?
首先,这是一个新问题。
如果我们没有对页面节点进行回收,那么就不存在滚动路径上页面没内容的情况,也就没有所谓的闪白问题。但不做节点回收就意味着在一个超长或无限下拉的列表中,DOM 节点会线性增大,必然导致页面占用越来越多的内存,增加更多的排版耗时,进而影响页面性能和用户操控体感。
其次,为何无法数据量化?
因为在 Webview 内,前端并不知道当前页面滚动速度是多少(或者在前端不能准确地用数据的方式表达),滚动曲线和滚动加速度在不同的手机和平台上也是不尽相同的,因此在不同手机上发生白屏的滚动速度阈值是不一样的。
那么,为了平衡快速滚动的闪白问题,可以让容器可以对页面在滚动速度的上限进行限制,这个需要客户端容器侧来处理。
很多前端开发者应该都知道,在老旧 iOS 系统上,如果 WebView 采用的 UIView 架构,由于 UIView 和 js 运行在同一个线程,导致在 UIView 滚动时会阻塞 js 执行,因此在 UIView 的容器内虚拟长列表快速滚动带来的白屏问题是不可避免的,好在现在的 iOS 系统基本上已升级为异步线程模式的 WKWebView 了。
目前,WKWebView 容器滚动的惯性速度和加速度的上限默认是比安卓的要低一些的(这也只是笔者对双平台的滚动对比的体感,没有量化的数据),而且iPhone手机性能通常比较好,因此虚拟长列表页面在快速滚动中,iOS闪白体感不那么明显。
由于安卓的 WebView 容器在快速滚动情况下,页面会拥有很高的惯性速度和加速度。在极端滚动操控下,比如直接触控拖拽滚动条(参考以上的视频)或通过window.scrollTo 快速定位到某个位置,JS 逻辑还来不及计算当前滚动到的可视区域所需展示的 item 内容时,页面就已滚过了该区域,闪白问题几乎是不可避免的。
而针对极端操控的闪白问题,可以在安卓的Web容器侧禁用滚动条的拖拽功能来规避,这并没有在根本上解决问题,但是webview这种机制不一定全是缺点,在大多数场景下,普通速度滑动h5的列表滑动也是很顺畅的,基本也感觉不到白屏,而技术角度看webview内存占用会比flutter低,这其实也是一种优势。
如果业务上,不得不使用长列表,在前端的优化技巧层也是有一些方式方法的。
比如,让列表渲染的 item 不只是可视范围内的 item,而是会在上下边界部分预留足够的buffer,这样可以缓解问题。在双端的具体优化策略上,双端冗余buffer也可以做差异处理。在上下边界的冗余 item 数量,iOS 可以冗余少一些(性能好,但内存少),而安卓则多一些(设备内存大,就多占内存)。
或者,在需要动态计算列表高度的时候将过程简化,比如原来需要每一个参与布局的列表item都需要计算一次,是否可以采用“分组计算”的策略,例如10个item分为1组,回收节点也是按组进行,这样回收算法的复杂度就可能只有原来的1/10。
不管对长列表采用怎样的dom节点回收技术,都必定会面临在用户极端操控下 UI 的变化速度 > 渲染速度问题,在现有的浏览器JS执行必然阻塞Dom渲染的模型下,而且Webview内核层面定义没有透露更多的动态渲染处理API之前,此问题暂时没有彻底的解法。在对用户体验较高的长列表场景,我们倾向于选择Flutter,如果是一级核心场景,主流方案还是Native。
当然,问题虽如此,这并不意味在h5长列表中对非可视区域的节点进行回收是个不必要的设计,毕竟极速滚动的闪白还是一个比较极端场景,很多时候产品对于用户的体验并不没有那么特别苛刻。或许也不应该那么苛刻,特别是人力有限的情况下,毕竟也要综合考虑ROI,但开发者最好要知道技术边界在哪里,避免掉坑里。
视差互动(Parallax Effect)或滚动(Parallax Scrolling)指操控网页滚动过程中,同时实现多个元素以不同的速度移动,形成立体的运动效果以提供出色的视觉体验。
先来看看两个真实业务场景的栗子(视频):
以上视频的栗子是Flutter实现的视差效果,含有两种视差:
如果用H5的方式来做这个需求,这两个效果是做不到滑动操控和UI变换的那种顺滑体感。这个本质原因是js是单线程的,当js在执行时DOM渲染会被阻塞,一帧内要做多件事情就可能会出现掉帧,所以做不到丝滑体感。
以上的栗子是基于Flutter实现的视差,是目前比较常见的视频播控落地页的交互模式。在这个业务场景中是一个可以横向切换的多页容器,同时每个播放页面又是一个可上下滚动的嵌套容器:
这个栗子里面包含的边界问题是复杂嵌套滚动的顺滑切换,目前业界实现类似效果的技术方案主要是Nativa或Flutter,没有见过H5实现的效果。
再看一个相同业务的Flutter与H5差异对比栗子。
以上是相同页面的两种技术实现对比,头部的视差互动在滑动操控时,H5有明显的UI抖动,Flutter则不会。可以看到,在我们的业务中,Flutter实现了title随着容器上推渐显和下拉而渐隐,H5做不到顺滑的渐隐渐显过渡,降级的分享页只能取消效果。
当我们用Webview来实现复杂的视差交互时,为何会触及Web的边界?
究其原因是复杂的视差互动大多需要通过js计算受控目标的Dom实时位置,不断循环“读取dom位置→计算dom位置→改变dom位置”,如果受控目标过多(视差效果通常是2个或以上受控目标,且每个目标采用不同的运动曲线),必然会带来js计算耗时>16.67ms进而导致UI的抖动,就会给人一种互动动画不顺畅的体感。
在前端的业务场景中,复杂一些的动画可以采用css动画来实现,顺畅度会比js实现好很多。这是因为css动画本质是由渲染内核提供的动画组件能力,它和js是异步的,不会因为js运行而阻塞。但有一个问题,css动画的运行状态在js侧没有感知的机制,如果用js和css混合来处理视差、动画会存在两者衔接不顺的问题,这就违背了采用视差效果的初衷。
在现在App业务场景中,多tab的页面是非常常见的UI交互设计,多tab页面设计将相同类型的信息聚合到相同的tab内,不同的分类则按tab横向拓展,这样可以在有限的屏幕范围内尽可能多的容纳更多信息。
图片转自https://developer.aliyun.com/article/791254
可以看到很多大型App的首页都是类似的设计,实现的技术栈是客户端,用Web实现案例会比较少见。当然,在某些App极速版中确实有基于H5实现的,但也会在用户安装App后通过动态加载等方式将Native版本下载回来。为什么Web实现类似的多tab长列表存在困难呢?
其实在上述的边界问题中也谈到了其中的原因,在这个场景中用web实现的话,有以下的难点:
从技术方案上有很多实现的方法和思路,在UC的业务场景中也有很多类似的业务,在这个场景是包含了长列表、视差滚动等边界问题的综合,由于Web容器对于这边边界问题缺少原生组件能力支持,业务落地的技术成本往往比较高,而且对比效果native或flutter的效果差别也比较明显。
因此,针对多Tab列表的场景,在业界的技术选型中往往以native或flutter技术为主。在部分业务场景则通过native拓展多Tab容器的方式,每个tab则是内嵌Webview,而native处理tab与tab之间的页面切换效果和事件派发,这样通过容器封装也可以实现Web的场景拓展,这就是Web容器的多page模式(也叫swiper容器)。
在同一个套业务代码里面提供一个局部弹窗,在技术上应该是比较简单的。那么,弹窗在什么情况下会存在边界问题呢?让我们先来看看几个业务场景的诉求。
上面是相机万物识别的结果呈现的页面流程。当拍照完成后,在拍照结果的等待页面中再从弹出一个局部弹窗,用于展示云端的搜索结果内容,承载内容是一个第三方页面(不是当前相机业务负责,而是另外的业务团队)。页面的容器顶部bar拖拽放大结果内容页面,支持分段式触控,也就是弹窗容器高度是可变的,内部支持局部滚动。
以上两个视频是分别在两个不同的内容消费场景打开用评论或评论详情,其中一个是图文H5打开一个子弹窗,另一个是沉浸式视频播放流中打开一个半屏弹窗。
因为都是评论是通用的功能,会在很多业务场景中被调用,在技术上我们采用的是H5实现,同行大多采用Native,例如今日头条、腾讯新闻、网易新闻等均如此,它在代码上不属于当前的业务,是一个独立演进的业务模块。
这是UC带货直播的业务。在我们的直播容器中,上层可见的UI是由Webview实现的互动层来承载,它包含很多功能实现,例如点赞、礼物互动、弹幕列表、商品卡片、福利活动、作者关注等,所有动态运营能力基本都基于互动层来实现,行业的主要方案是native为主,而采用Webview来实现在技术上是一种新的尝试。
在底部的购物车按钮打开的是当前直播中的商品列表,在原有的产品设计中不属于当前互动业务团队,而是由直播电商团队负责,所以是一个二方页面的局部页面。后来改成了我们通过接口获取,但考虑到互动层的复杂度,我们沿用了独立页面的技术实现。
以上,所有的弹窗功能有一个共同特点,就是不属于当前业务,也就是由A业务调用B业务,实现一个局部的功能界面效果。在业务划分上,可能是不同的业务团队负责,都是H5技术栈的话,业务实现所采用的的前端框架大概率是不一样的。
那么,这里的边界问题是——在两个相互隔离的js代码仓库中,如何实现一个局部的弹窗,而且该弹窗是一个二方提供的具体实现?
技术上,被调用的局部弹窗可以采用js-sdk的方式提供对外服务,业务调用方通过动态注入JS到当前页面的页面中来实现。但这就会带来以下的问题:
以上这些问题其实纯前端的方式都是不好解决的,因此,这样的局部弹窗更合理的设计是一个独立Web页面,独立页面的好处是避免了业务耦合和版本迭代两个问题,只需要解决加载性能问题就可以了。但在具体的技术实现上,难道用iframe的方式设计这个弹窗吗?
很显然,大多数前端开发者都不太愿意采用这样的设计,因为iframe不好用。因此,这样的局部弹窗页面需要从客户端角度提供定制化的扩展,这就是Web容器的半屏模式,也叫局部容器模式。
以上举例了5个典型的Web边界问题,哪些是可以在容器范围内进行突破的呢?当然这里所说的边界突破,指的是原来做不了或做得不够好的,通过对容器的封装而获得解决的。
实际上,容器边界能力的突破还是需要Native同学通过容器进行组件/API定制,给前端提供私有组件的方式来解决标准Web容器的能力局限。从Web浏览器的渲染流程上,只要不涉及需要挑战浏览器渲染线程模型的,从原理上可以通过容器封装来解决。
例如媒体播控、局部弹窗,通过Web容器扩展私有组件或API能力是可以比较好的实现边界拓展的,但长列表、视差和复杂多tab等场景所面临的本质问题都是一致的,都需要在足够小的时间片段内完成“读取Dom位置(大小)→计算Dom位置(大小)→改变Dom位置(大小)”,如果这个时间片段超过16.67ms(一帧)都会面临体验不够好的问题。
如果想将时间耗时压缩在足够小的范围内,而JS作为动态解析型语言的执行效率在计算下一帧Dom位置的耗时就可能会成为瓶颈,而Web标准没有在layou阶段提供获取Dom位置大小信息的能力,则进一步加剧了需要动态计算目标Dom位置的等待时长。
因此,此类问题在现有的Web范围内是不太好解决的,而解决它可能需要调整浏览器的渲染流程和架构,这样的技术成本还不如用其他技术来得更加实在,技术方案又不是只有Web一种选择,不是吗?
以上笔者列举了许多Web的边界问题,好像都是说动态方案做不好的或做不了的,对前端而言是不是太过悲观了呢?显然,这并不是笔者的初衷,而是期望通过了解技术实现的边界,才能更好的推动Web容器进行完善和向前发展。
笔者认为,对于Web容器的封装目标是——在容器范围内解决原有Web标准容器解决不了的边界问题,并通过容器的标准化封装提升对复杂问题的解决效率,让Web容器能够覆盖更多的业务场景,进而能够拓展前端的业务空间。这里的关键点有:
这是一个很重要的出发点,但凡标准能够解决的问题,其实就不需要一个自定义的容器(轮子)来加持。边界的问题其实跟业务类型和场景有很大关系,不同的业务触及的边界是不一样的,在不同的发展阶段业务对于边界的体感或认知也不尽相同。
不过,是不是所有的边界问题都能通过容器来解决呢?显然不是,有些边界可能是在现有Web容器范围内就是无法突破的,该Native的就Native,因此需要开发者对于边界问题的有更精准和更深刻的认识。
很多时候一个App应用需要在不同的平台上提供服务,不同平台的实现或UI展现本来就存在差异,而这种差异对产品而言可能是不可接受的,那么可以将这种不一致的问题在容器层面进行封装或拓展。
这样的能力差异挺多的,例如输入面板、分享面板、日夜间适配、web容器样式(如前进、后退、关闭、收藏等)、跨页通讯、容器push/pop的动画、worker调度和通讯,等等……这些应该是容器封装要解决的基本内容,有一部分是web标准的延伸或拓展(渐进增强),也有部分是针对私域业务的定制优化。
复杂问题有很多,举个通俗易懂的栗子。例如性能优化,这对于每个业务都是一个复杂的问题。
做过移动H5性能优化的同学都应该知道,纯前端视角是很难将性能优化到极致的,最多就是通过服务端实现SSR、ER边缘渲染,但这并不是最极致的性能优化手段。在App里面做性能优化,极致的优化手段还包括离线包、数据预取、图片预取、NSR预渲染、页面预执行、端智能调度等优化手段。
当然,复杂问题的解决往往也是一个技术成本高的事情,如何将复杂的、成本高的事情简单化、标准化、动态化,这也是容器设计的重要目的。
这应该是Web容器封装的最终目标。当然,一个新的Web容器方案,除了性能、交互体验能够满足业务需求外,前端的开发体验也需要得到保障,配套的工程效率工具方案也应该是一个Web容器建设的重要内容,而这往往容易被容器封装的Owner所忽略的。
因此,Web容器框架应该以业务为起点,前端开发者需要深度参与其中,这样才能提出和推动更符合前端视角的诉求落地,而不只是被动接受客户端的封装实现。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8