Web 端如何低成本打造 Native 体验?

简介: Web 应用在实际体验上和 Native 应用仍然存在很是明显的差距,那么如何低成本地把一个现有的网站改形成类 Native 的体验呢?本文分享一种让网站低成本渐进式实现 Native 化体验的方式——同屏渲染。(文末推荐:2020 阿里云线上峰会)前端

image.png

Web 端体验

在有了 PWA(Progressive web apps) 以后,Web Application 也具有了添加到桌面和离线访问等能力,可是实际体验上却老是和 Native 应用存在很是明显的差距。git

咱们能够看一下 Alibaba 的 M 站和 iOS 应用的录屏(左边为 WEB,右边为 iOS APP):github

640 (2).gif.gif")web

640 (3).gif.gif")json

咱们能够看到,对于 Web Applicaiton 来讲,在页面中来回跳转时访问的老是割裂的,从上一个页面到下一个页面须要等 loading ,返回时不少内存状态又都不在了,致使没法正肯定位回以前的列表位置(这一点其实和不一样的浏览器以及列表自己的实现方式有关,也有一些方案能够规避这个问题,在这里只是其中一个 case)。跨域

这样对于用户的体验伤害很是明显,他能明确感受到本身在用的并不是一个 Application 而是一个 Website,并且在进行复杂的操做时整个链路也很是容易被中断。浏览器

而其实这种体验差别的根源,在于 B/S(Browser/Server)和 C/S(Client/Server)的差别。ServiceWorker 虽然提供了一些方案(例如 App Shell)让咱们较低成本的加强原有的体验,但仍然难以解决页面之间的割裂问题,不少相同的代码在不一样页面间重复执行,每一次访问内存状态就会丢失。安全

渲染性能

当咱们在说体验的时候会显得有点主观,性能相比之下就容易衡量的多,而页面割裂带来的最为直观的体验差距其实就来自于渲染性能的差别。闭包

在 Web 端一个典型的 CSR(Client Side Rendering)要通过的流程大体以下:app

image.png

这其中有不少不符合咱们预期的地方:

  • HTML/JS 都等到点击后才开始加载(这点能够经过预加载的手段解决,其中 HTML 的预加载能够经过 ServiceWorker 进行)。
  • framework 等 JS 在不一样页面间老是重复执行的。
  • 加载 API 的时机很是晚,而加载 API 通常是耗时很长并且能够并行的部分,理论上加载时机越早越好。

因此理想中的渲染流程应该是下图这样:

image.png

其实对于 Native 应用也是如此,用户点击时基本就会开始加载 API 而且执行下个页面的逻辑。其实一个优化的比较好(作了 preload 等)的 SPA 也是相似的效果,咱们提早加载好下个页面的 vendor ,点击时直接只执行下个页面的逻辑便可。

然而实际上对于一个较大的现存站点来讲(例如 m.alibaba.com ),把整个网站做为一个 SPA 来维护是不太现实的,一方面不能适应当前多人协做的现状,另一方面稳定性上也不能接受修改一个页面整个网站都要发布的方案。

那么,如何低成本的把一个现有的网站改形成类 Native 的体验呢?

同屏渲染

在有了上面的思考后,咱们就在想,有没有一个方案在不作改造的前提下,在用户点击后,当即开始数据的并行加载,同时把下个页面动态的加载进来,选择性的保留上个页面的一些内容(例如正在加载中的数据, jsonp , framework 层的对象等)而隔绝其余部分的干扰。

因而针对咱们的场景产出了一个同屏渲染的方案:LightHub,所谓同屏渲染,即渲染过程当中页面不须要被卸载,全部的渲染行为都在一个上下文中发生。

640 (1).gif.gif")

这里咱们须要几个东西:

  • 能直接附着到现有页面上的沙箱,用于把页面还原到初始状态(同时容许保留部分共享的部分)
  • 过渡动画
  • API 并行加载
  • 按照浏览器行为渲染 HTML
  • 按照浏览器行为触发事件

image.png

沙箱

咱们须要一个低成本把页面还原会初始状态、而且容许保留部分对象的沙箱机制,并且最好这个机制是能够直接低成本部署到现有页面上的。其实这里的诉求和微前端碰到的问题相似,咱们受 qiankun 的沙箱机制启发,只须要在页面的

中插入一小段内联 JS 记录:

  • window 上的全局变量
  • window/document 的 eventListener
  • 定时器:setInterval/setTimeout/requestAnimationFrame/requestIdleCallback
  • MutationObserver

在咱们须要时咱们只须要清空页面的 DOM,还原变化的全局变量(这里和 qiankun 同样采用的浅拷贝),eventListener,定时器和 MutationObserver,就能把页面还原到初始状态。

同时,记录的状态也能封存到一个对象中,当用户从下个页面 back 到上个页面时,咱们能够直接把状态还原到页面上。

这里就须要在清空页面状态时选择性的保留一些须要保留的对象:例如公共的 Framework,JSONP 请求的标签等。

过渡动画

这一点其实就没有多复杂了,在页面不须要被卸载和从新加载后,咱们能够在用户点击后当即展现一个动画。目前采用的只是一个简单的从右侧 slide-in 的动画。

须要注意的是,因为在绘制动画的过程当中咱们每每正在执行下个页面的逻辑,咱们须要注意使用 GPU 来绘制动画,从而确保动画不会被 JS 执行阻塞。这一点对于低端机尤其关键。

API 并行加载

其实在有了上面的沙箱机制后,API 的并行加载就不是难事了,须要注意的是咱们须要保护 API 并行加载自己的过程当中产生的状态(例如 setTimeout ),咱们须要实现一个 runInSharedContext 确保这其中的定时器不会在页面切换时被卸载。

runInSharedContext(() => {
  // 这里的 setTimeout 不能是被记录 & 清除的 setTimeout
    setTimeout(() => window.sharedfetchDataPromise = fetch(res));
});

而在下个页面消费的只须要 window.sharedfetchDataPromise || fetch(url) 就能直接复用并行加载的 API 请求。

在咱们的场景下为了让这个问题更加开发者无感,封装了一个叫作 redfox 的工具库,在同一个页面环境执行屡次相同配置的请求会自动复用,不须要开发者手动判断。

按照浏览器行为渲染 HTML

这多是其中最复杂的部分了,在咱们抓到下个页面的 HTML 后,不能只是简单的 document.innerHTML = nextHTML ,这样会致使和普通的浏览器行为彻底不一致,样式加载会致使闪屏,脚本的执行顺序不符合预期等等。

image.png

因此咱们须要本身实现一个 renderHTML ,将抓到的 HTML 解析后模拟浏览器的行为进行渲染。

  • 播放动画
  • 先经过样式隐藏 body
  • 异步将 CSS append 到页面,等到 head 中的 CSS 加载完成而且动画播放完成后取消 body 的隐藏
  • 把 JS 按照类型和顺序 append 到页面
  1. inline & 正常的阻塞后续 DOM 和 js 的 append
  2. defer 丢到 defer 队列中
  3. async 异步执行,不阻塞后续
  • 按次序执行 defer 队列

这个部分的行为比较复杂,须要在较多的场景进行测试,以及有相应的单元测试保障逻辑的正确性。

image.png

按浏览器行为触发事件

其实和上面渲染 HTML 类似,在渲染的过程当中须要按照浏览器的行为触发相应的事件。

例如上个页面卸载时依次触发 beforeunload => pagehide => unload ,在下个页面加载时先把 readyState 重置,而后按照次序触发 domInteractive defer 的执行和 DOMContentLoaded 。

一样的,单元测试在这个环节是必须的。

分析

Timeline 分析

从 Chrome 最后的 Timeline 看执行逻辑基本是符合咱们预期的,点击后的瞬间 API 开始加载而且基本上就开始全力执行下个页面的渲染逻辑。

Framework 层的代码基本也不须要再重复执行。

image.png

内存压力

对于这种不卸载页面的方案来讲最容易引发担心的可能就是内存泄露问题,其实按照上面的沙箱机制,只要咱们确保 DOM、全局变量、定时器、时间监听等可以被正确清除,与之相关的闭包等就不会赖在内存中不走。

从咱们本地屡次频繁点击切换页面的反应看,内存随着页面的切换返回也会一次次回到初始状态,从理论上不存在直接致使内存泄露的缺陷。

image.png

然而,因为咱们容许在页面间保留一部分的公共区域(上面称之为 Service Layer),另外沙箱自己是一个约定沙箱而非安全沙箱(例如往 Element.prototype.xxx 属性写东西就没法被拦截),对于一些不规范的写法仍然存在内存泄露的风险。

这一点可能须要和 Native 端相似的内存压力监控等方式来长期观察。

分阶段打点

因为整个 HTML 渲染过程都是咱们本身实现的,因此整个渲染的各个阶段能够本身打点记录一些时间,下面就是一个例子:API 从 JS 请求到拿到耗时 124ms ,而实际上整个取数据(提早并行取的)花了 350ms 。每个 script 开始执行和执行耗时也能够经过这种方式打上来。

image.png

这也能够为咱们的页面优化提供一些指导,例如 JS 的执行时间是否是过晚,某段 JS 的执行时间是否是过长。

效果

最终的对比效果以下,左为同屏渲染,右为正常跳转,从线上的数据看性能提高大约从 2.8s => 1.8s 。

640 (4).gif.gif")

除了异步渲染的页面外,咱们针对一些原先是 SSR 的页面也作了很是低成本的接入(不须要改造页面,可是享受到的受益相对也更有限)。

640 (5).gif.gif")

但仅仅是上面这种跳转体验和返回体验的改善,就让咱们的 Just For U 模块的曝光屏数有稳定 3% 的增加。

总结

总结一下:

  • 相似 SPA 体验的客户端渲染可让 Web 的体验更接近 Native。
  • 同屏渲染是一种让网站低成本渐进式实现 Native 化体验的方式。
  • 更加沉浸的体验确实会让用户有意愿进行更多地浏览。

局限

上面的方案仍然存在一些局限性,例如前面提到的须要开发者防范内存泄露的问题,同时由于 History API 的限制,页面必须是同域的,不然跳转的 URL 没法知足预期。

将来

关注 Chrome 动态的同窗也会了解到 Chrome 最近也退出了一个新的提案:Portal API,就是旨在解决咱们上面提到的 Web 体验割裂的问题。

可以提供一个相似 iframe 的沙箱,以较低的成本实现页面间的跳转过渡等。在将来 Protal 普及后(至少 Chrome 发布, Safari 跟进后),咱们就能够在新版本的浏览器中抛弃如今使用 JS 实现的沙箱机制,使用更加安全(且炫酷)的 Portal API 来实现同屏渲染。

在 Protal API 的支持下,咱们也能够克服没法跨域的问题,按照目前的草案,Portal 是支持跨域跳转的。

拓展阅读

[1]qiankun ( https://github.com/umijs/qiankun))

[2]Hands-on with Portals: seamless navigation on the Web
(https://web.dev/hands-on-portals/))

重磅推荐 | 2020 阿里云线上峰会

就在今天!1 场主旨论坛,27 个产品技术及战略发布,30 场行业及产品技术专题演讲以及技术大咖脱口秀,思想的盛宴,有料,自助,可续杯。点击”阅读原文“当即参会!

相关文章
相关标签/搜索