Feb 20

NSR头脑风暴

Lrdcq , 2020/02/20 18:23 , 程序 , 閱讀(2088) , Via 本站原創
之前UC提出了一个webview容器增强方式,NSR(https://www.infoq.cn/article/9UKos4Xh_6wL4Fh1FOGL)即Native Side Rendering。听着很玄幻但是其实就是将常见的webview本地优化方式的一个极限做法,如果要用人话描述的话,就是本地SSR+请求预加载+同构的集合体。为什么要请求预加载:肯定要做请求预加载的;为什么要本地SSR而不是资源离线化:其实可以,但是离线化无法解决spa应用在webview中初始化与渲染的耗时;为什么要同构而不是用rn或者weex之类的看起来离线化根彻底性能能达到相同目的的方案而是死折腾web:技术栈收敛并且业务场景受限。

按UC给的图,大概这个意思:
點擊在新視窗中瀏覽此圖片

不过这里涉及到一个姿势汇总,即对于Webview渲染优化,我们从Native的角度到底可以做些什么?

加载耗时背景

我们首先需要关注一个常见的spa的web应用,在我们的一个native客户端中,从Activity/Controller打开到页面渲染完成即FMP的事件到底经历了什么。

1. Native界面创建,包括页面即Activity/Controller本身的创建与其中的核心View即Webview的创建。
2. Webview搞定后即开始加载网页document本身与相关资源了。如今SPA应用为主的业界js资源应该挺头大的。
3. SPA的app.js运行起来,即可开始加载业务流程,会有一定的接口和包括定位/设备信息等一系列流程。

界面创建阶段优化

Webview复用

- 使用Webview的时候,我们经常会提到Webview创建容易卡顿/成本高,在早期Android开发的过程中特别明显,创建一个Webview至少要白屏1s以上。现在设备性能好了不少,但是在cpu使用的时候创建webview还是能感受到明显的卡顿的,因此从上古以来最场景的优化方案就是webview复用。

- 启动客户端后即提前准备一个webview,开启web页面的时候直接attach到页面中并使用。
- 页面销毁后把它放入重用池。

- 重用池一般有多个策略来保障整体使用过程平滑有效:
    a. 始终保证有一个空闲的webview,即使用第一个的时候创建第二个webview;使用第二个时创建第三个webview,关闭第二个页面时即可销毁一个webview。
    b. dirty标记法,即然要销毁,为了防止内存泄漏,还是优先销毁dirty的webview,并且销毁有一定延迟。
    c. 如果web页面较多,ab重用方案也会导致大量的重建与销毁工作——虽然用户感受不到,但是也会引起性能销毁。因此重用池会有一个最低持有数量的概念,如果大量发生创建销毁,会动态提升重用池持有webview数量,来提升重用率。实际做法中另一个做法是为每一个dirty的webview设置销毁倒计时,随着创建频率提升销毁延迟变高,也能解决这个问题。

页面复用

- Webview进一步,就是Web容器所承载的页面(Activity/Controller)进行复用了,结合webview复用了。因为是web容器,web页面所承载的逻辑并不多,保持复用状态当然未尝不可,特别是有较多的容器增强功能时,页面创建确实也是一个会导致卡顿的开销。
- 对于Android来说,通过服用页面的主元素来做到页面持有的目的,同时也能比较作弊的实现Activity后台销毁后恢复网页。

网页加载阶段

这个阶段可以做的事情相当多,并且都是围绕着“请求”与“资源”两个词进行的处理。

请求加速

经常会遇到webview打开的时候賊慢,但是后面使用的时候倒是挺正常的。用常理分析,肯定是document和资源请求出现了问题,开一下charles抓包,一下子就清晰了:第一次打开webview肯定会遇到大量之前未遇到过的资源,dns解析和连接时间太长了。因此请求加速本质上是打通快速通信的通道。

- 长链接:现在大长和各个第三方应用框架均提供了长连接进行http通信的框架,将webview中的请求代理到长链接通道上进行内网访问,当然可以提速了。

- HTTPDNS:长链接通道并不是总是稳定可用,并且对于图片,音视频等大型资源不可能走长链接,因此对于还需要走http的资源,我们应该拦截并进行HTTPDNS动作来避免dns解析问题。使用HTTPDNS需要注意一点的是web场景涉及的域名多,HTTPDNS服务本身可靠性也是问题,因此从app维度需要对HTTPDNS进行预热,提前下发并打通web中可能访问到的域名。

资源离线化

当然对于网页资源来说,更快捷的方式便是完全离线化,以Map的形式提前下发并解压网页资源,在发生网络请求的时候,进行拦截并根据url返回本地资源,即可。
这个方法也是业界最最最常用的方法了。

文档预加载

对于无法离线化的资源,网页能够进行预加载当然是最好的。因此在特定的场景里,基于之前所的webview复用,我们可以进行综合性质的网页预加载。

- 即在特定的操作路径下,直接触发一个网页加载,通过一个复用池中的webview提前加载网页的一部分。这里尽量通过代码逻辑做一定限制,最合理的情况是:a. 90%命中率,预开启的页面能尽量命中用户的意图。b. 做不到准确预测,就卡90%的有效流量消耗,预加载消耗的流量理应受限,在用户真实进入的时候再进行大流量api请求。这样既可以做到webview的页面即document提前准备完成,也能保证用户流量不瞎跑。在document本身有缓存拆解的机制下,其实并不难做到。

- 如果不提前启webview,让webview的document启动运行更快也有一些别的方法。对于目前web页面大js的背景,在Android上v8引擎中,我们可以使用v8引擎的Code Caching技术为特定网页的js部分做缓存与加速(参考https://nativescript.org/blog/using-v8-code-caching-to-minimize-app-load-time-on-android/)。当然,配合上SSR的思路,避免首屏大量元算来实现提速其实是最有效的。这个就靠近UCNSR的思路了。

资源压缩

理论上资源加载过程中都应启动了gzip,数据size还是很小的,不过对于非字符资源,还是有一些特定的压缩方式可以做的,只是需要对应cdn支持。

- 拦截图片请求,判断为支持压缩的cdn,则改变图片请求到压缩格式,比如webp上,虽然可能有画质损失,但是请求量确实减小了。这个思路类似于现在手机浏览器提供的代理模式。
- js资源和其他常见的业界公共资源,也可以通过一定的服务端判断重定向到公共的cdn上从而提升加载速度。

简单来说就是两个思路,非cdn改成cdn,cdn支持极限压缩。这样的话远端加载的资源问题也解决了。

核心实现方案

资源/图片/document/api请求拦截的方式看起来很复杂,但是其实都是一致的,对于iOS来说:

- UIWebview可以通过NSURLProtocol直接进行拦截。
- WKWebview稍微麻烦一点,需要通过registerSchemeForCustomProtocol这个私有API进行拦截,网上到处都是example

对于Android来说,事情更麻烦了

- 一般来说是通过WebViewClient的shouldInterceptRequest方法进行拦截,但是这个拦截出来的方法取不到httpbody,是安卓webview设计上的硬伤。因此涉及到post请求带body的场景,需要通过特殊的js操作,将body通过header或者其他方式带过来。
- 一般可以让web主动引入一个aop的js即可(拦截form表单,拦截xhr,拦截fetch即可),对于第三方也可以使用native注入js的方式。

业务流程阶段

到业务流程阶段,还有一些可以做的事情。

业务请求预加载

和文档预加载部分一致,如果我们一定程度上能判断用户的操作路径,我们的文档和业务数据都可以预加载,甚至可以直接用前一个native页面的数据拼装准备好即可。这也是UCNSR在新闻列表页的核心逻辑之一,不用多说。

业务逻辑与渲染分离

- 如果我通过一个worker独立出业务逻辑,那它就可以在更合理的时间进行加载并执行,并不依赖webview的文档资源部分——这就是pwa的逻辑。

- 如果我把业务逻辑放到一个独立的webcore,这样它初始化的时间可以继续提前,并且还可以做到长期持有/复用,更极端的场景下,一个独立的webcore执行业务逻辑可以去操作多个webview。同时如果我们长期持有这个业务逻辑core的话,Android的页面销毁/恢复也可以尽可能的保留场景了——这就是微信小程序的思路。

进行业务逻辑与渲染分离算是比较大刀阔斧的改动,但是在重业务逻辑请渲染的场景下,无论是pwa思路还是小程序思路都是会有明显优化的。

Native容器增强

在业务流程阶段有一些js做的东西可能会晚了或者相对比较卡,这类的小元素可以交给native完成。可以想到的包括:

- 加载屏或者骨架屏,js里启动fc已经过去的一段时间了,所以通过容器native布局去做明显会体验更好。

- 重组件,如video,map。这类js中比较吃资源的组件,一般的应用内native会有明确性能更优越的native组件,直接用native组件替换js组件即可,利用同层渲染技术,相关组件几乎和普通js组件用起来没区别。

- 定位/陀螺仪等,一些调用系统能力的js方法——其实是浏览器实现的jni方法,我们可以提供更快速更精简的实现,甚至作弊的方法。比如大量网页业务流程启动时需要定位信息——那我们新增内部使用的jni定位桥,直接返回缓存结果就好,更夸张的是直接url参数上向容器声明needlocation=1,网页实际加载时就通过query或者cookie之类的场景把经纬度带给web。

收口

以上我们提到了目前业界有见过的绝大部分native角度进行webview中web页面优化的思路,但是和UC的NSR相比,还有一部分没提到:

- 同构:对于web应用,当然期望同时运行在多种容器上,如果我们加入了大量容器特性,那也需要做一定容器磨平工作,至少对web开发者无感最好。因此同构框架,以上涉及到这么多可配置项目的脚手架等等都必不可少。

除去UCNSR,其他部分看起来其实更像是微信公众号做的或者微信小程序做的——其实微信小程序的开发初衷很大一部分也是webview增强

选取其中一部分组合起来,结合web侧常用的优化方案(cdn,ssr,优化缓存策略),理应我们能得到一个不输于业界各个大厂的webview容器。
关键词:webview , nsr
logo