Webview秒开探索:让你的H5“快人一步”

3,777 阅读7分钟

本文为原创文章,引用请注明出处,欢迎大家收藏和分享💐💐

背景

如今的前端技术层出不穷,无论是reactvue等框架还是跨端解决方案,为使用场景和开发效率做了不少的提升,但作为前端技术的重要衡量指标之一,首屏渲染效率无疑前端老生常谈的话题了。这篇文章就来聊下如何在常见的H5环境下,做到页面秒开。

业务场景

这里也是引用笔者之前做过的一个业务来举例:有一个模拟用户朋友圈记录的H5页面,用户能通过管理端来编辑一条朋友圈消息「图文|视频」,并展示在这个H5页面上。如下图: image.png

在这个场景下,如何快速打开页面并把朋友圈动态展示出来就显得尤为重要了,因为首屏速度越快,有效曝光率就越高、挽留率也就越高,如何提高页面的访问速度可以说是这个业务的硬性指标

另外,这个业务还有另外的技术要求:

  • 实时更新,与后台管理系统操作同步(后台新增1条消息前端就要有新记录);
  • 秒开,最好能达到native页面的体验感。

秒开的技术探讨

网页请求流程

在确定方案前,我们先回顾下网页的请求全流程:

part1:浏览器发起document请求

  1. app cache:检查域名缓存,如果有缓存就不需要DNS解析域名;
  2. DNS解析:把域名解析成IP;
  3. TCP连接:浏览器发起TCP连接请求。经过标准的TCP握手流程,建立TCP连接;
  4. HTTP请求:按照HTTP协议标准发送一个索要网页的请求;
  5. API网关转发:一般服务配备业务转发能力,根据不同路径转发到不同服务;
  6. 负载均衡:计算负载,转发到一台后端的真实Web服务器,Web服务器收到请求,产生响应,并将网页返回。

part2:document到页面渲染

  1. 根据 HTML 结构生成 DOM Tree
  2. 根据 CSS 生成 CSSOM
  3. 将 DOM 和 CSSOM 整合形成 RenderTree
  4. 根据 RenderTree 开始渲染和展示;
  5. 遇到script标签时,会执行并阻塞渲染,因为 Javascript 代码有权利改变DOM树;
  6. 异步请求触发,完善页面数据,最终得到一个最终页面。

由此看来,对于首屏的常规优化,我们可以采取资源压缩&合并、cdn加速、骨架图等一系列措施,这都是老生常谈的优化方案了;

其实,对于动态页面,往往需要在onload后发起额外的异步请求(上述第6步),在这个过程中,会有或多或少的等待时间,降低用户体验。


思考:有没有办法让这类页面提前渲染出最终形态??

没错,我们可以采用ssr渲染方案(即是在part1过程进行数据提前处理),在请求html的时候在网关层进行拦截,转发到后台服务把数据写入html,把最终带有数据的页面返回给前端,流程图如下: image.png

这是常规的SSR渲染方案,只是异步数据拉取时机由前端调用改为服务端调用。按理说,这时候的:首屏时长=服务请求时长+服务获取异步数据时长+浏览器渲染页面时长

虽然说服务器拉数据比前端更稳定和快速,但带来了额外的问题:

  • 拉取数据服务宕机,导致html请求阻塞,前端页面一直处于空白等待状态,需要服务端做额外逻辑兼容;
  • 拉取数据耗时较长时,前端页面的白屏时间也相应增加,不但没有实现秒开效果,反而拖慢页面加载速度;

思考plus:有没有办法在实现SSR情况下又能保证页面秒开?

image.png

这样,我们再想想在哪个流程点可以优化下:

  1. 放弃ssr,从优化前端资源入手
  2. ssr+本地存储
  3. 设置ssr数据拉取接口超时,前端页面onload后加上ajax请求补偿
  4. node服务+redis数据存储,代替额外的数据请求

方案对比

放弃ssr,从优化前端资源入手

  • 在 HTML 内实现 Loading 态或者骨架屏;
  • 去掉外联 css;
  • 使用动态 polyfill;
  • 使用 SplitChunksPlugin 拆分公共代码;
  • 正确地使用 Webpack 4.0 的 Tree Shaking;
  • 使用动态 import,切分页面代码,减小首屏 JS 体积;
  • 编译到 ES2015+,提高代码运行效率,减小体积;
  • ...

缺点:无法抹平异步数据加载带来的页面抖动,但可以快速给用户呈现页面雏形,综合考虑无法满足需求,舍弃。

ssr+前端本地存储

使用localstorage对首次请求得到的数据缓存,并设置有效时间,在有效期内直接读取本地数据...

缺点:无法保证数据实时性,无法满足需求,舍弃。

设置ssr数据拉取api超时,前端页面onload后加上ajax请求补偿

这个就是在服务器拉取数据时加上短暂的时间判断,在接口超时情况下直接返回没有ssr渲染的页面,前端在首屏完成后再异步请求数据。

分析:服务器之间的请求相对比较稳定而高效,ssr成功率也相对比较高,可以采取。

改后的流程如下: image.png

node服务+redis数据存储,代替额外的数据请求「推荐」

这方案大致思路:admin在管理后台新增朋友圈记录时,顺便拉取该用户最近20条记录,并把它们写进redis中。之后在H5请求数据时,先进redis检查是否有用户记录,有就直接写ssr并返回document,达到极速渲染效果。

一般对于数据量不是很大的请求,http在跨服务上的请求平均耗时100+ms起,而redis能达到10+ms的级别,在这2种方式,效率差别尤为明显。

技术栈:nodejs、react、redis、ReactDOMServer

整理后的流程如下: image.png

redis具备高性能的特点,参考资料《redis高性能原理》
ReactDOMServer可以参考官方描述,主要作用是在服务端将react函数实例化成一个dom

Ajax vs Redis 效率

Ajax

对于异步获取数据的http请求开销: image.png

Redis

使用nodejs+redis ssr处理耗时:

{"msg":"开始处理demo_user的ssr渲染...","time":"2021-04-11T03:36:45.842Z","v":0}
{"msg":"redis数据读取成功,共20条数据","time":"2021-04-11T03:36:45.858Z","v":0}
{"msg":"ssr渲染处理成功","time":"2021-04-11T03:36:45.869Z","v":0}

浏览器视角,获取html文档流开销: image.png

整个数据获取+处理过程大约只需要27ms,而首屏完全加载时间也保证在329ms,对比起来,我们在请求html文档时,在服务器直接对redis读取数据并写入ssr,效率提高了不止一个档次。

那如何保证redis数据是最新的?其实也很简单,在对用户数据进行数据库操作同时,更新一份到redis就可以了,而且ssr用于首屏渲染只需要前20条数据,固redis保存的数据量是可控的。当然,redis也不是绝对可靠的,所以我们还需要做些补偿方案,例如在redis获取数据失败时,改调用接口获取数据等。

效果展示

ssr秒开方案

111.gif

普通异步加载

222.gif

写在最后

欢迎大家关注本人公众号「似马非马」,一起玩耍起来!🌹🌹