开篇以前先介绍一下场景。信息流是一个基于用户兴趣使用算法将用户感兴趣的新闻内容推荐给用户的一种业务。这种业务带有很是特点的场景就是用户有一个“永远”都刷不完的推荐流列表,点击列表中的新闻以后能够跳转到其详情页中查看新闻的正文内容。列表通常都是由客户端原生去实现的,而详情页这块因为新闻内容结构的复杂性,通常仍是会使用 h5 来实现。这样就对咱们 h5 的性能提出了要求,咱们必须在用户切换的时候将切换的白屏时间尽可能减小,这样才能提升用户的阅读体验。html
本文就将为你们讲述一下咱们是如何实现性能优化达到“闪开”的效果的。咱们能够先看看效果
https://v.qq.com/x/page/j0900...,下图左边是正常版本,而右边的是优化后的版本。对比之下能够发现即便我已经悄咪咪的先点击左边的手机,同一篇新闻右边的打开速度明显比左边的要快不少。接下来就让咱们看看这个是如何作到的吧!前端
众所周知,网页中内容渲染每每根据渲染方式能够分为后渲染和前端渲染两种方式,最近几年由前端渲染又演化出了同构渲染,也就是你们常常说的 SSR。这几种渲染方式的主要优缺点大概整理了主要有以下几个方面。java
后端渲染:android
前端渲染:web
同构渲染:算法
固然本篇文章不是来说各类渲染方式的优缺点的,主要是说由于种种缘由咱们的项目最后使用了前端 JS 渲染的方式。而 JS 渲染带来的性能问题主要是因为数据接口请求返回以及前端 JS 资源获取所带来的网络问题。为了解决这两个问题,一方面咱们采用了服务端将数据注入到页面全局变量中的方式避免了数据请求,另外一方面咱们使用了 localStorage 缓存的方式将前端资源作了 LS 缓存避免了二次打开以后的前端资源请求,从而提升了前端渲染的首屏性能。npm
虽然咱们避免了前端渲染的一些问题对首屏的性能作了优化,但还远远不够。那目前还有哪些点能够进行优化呢?简单的整理了下能够有以下两个方面:后端
从上面两个优化点咱们能够看到全部的优化仍是网络的优化,主要仍是在移动端网络对性能的影响是远远大于其余方面的。那么是否有什么方案可以让咱们免去这些网络请求呢,最终咱们给的答案就是详情页本地化。经过本地化方案,咱们将平均 820ms 的首屏渲染时间优化到了 260ms,整整提升了三倍多!跨域
详情页本地化就是客户端不走网络请求打开新闻的方案,解决上文中列举的全部网络请求相关的优化点。它除了能为咱们带来首屏性能的进一步提高以外,因为它不走网络请求的特性,也为咱们解决了复杂网络环境下页面劫持致使的详情页白页打不开的问题。同时还为咱们带来了无网络环境下的离线阅读新闻的能力。缓存
因为咱们的这面是纯 JS 渲染的,因此咱们一个最终的详情页主要是由新闻数据
和静态页面
二者构成的。
鉴于对服务端的依赖很是的少,和大部分的 SPA 页面同样,本质上只要在客户端将咱们的前端页面提早下载下来就能正常打开了。
详情页 = 静态页面 + 新闻数据
而如何在用户尚未打开新闻以前客户端就能把咱们的页面资源下载下来呢?这里就不得不提一下咱们的场景,由于在咱们的信息流场景中,用户永远是经过流点击进入到详情页中。而在客户端的流中是须要加载服务端数据的,因此在这个时候其实咱们就能够告知客户端让其提早下载好模板。固然你们不要忘记,除了页面以外咱们还要有新闻数据,为了实现纯离线化同时也避免新闻数据接口的请求,在列表中还会将每条新闻的详细数据下发下去,保证必备要素的本地化。
如图所示,在列表请求的接口中,服务端会将须要缓存的静态页面地址以及每条新闻对应的新闻数据所有下发给客户端,客户端接收到请求以后会进行模板的下载。
须要的东西下发下去以后剩下的就是客户端进行渲染了。正常来讲除了模板页面以外,服务端还须要下载其余相关的静态资源,而后启动一个 HTTP 服务将页面和资源文件进行关联,关联以后将数据注入到页面以后打开页面。但这对客户端的要求就很是多了,为了将客户端的工做量下降,咱们将全部须要使用的静态资源经过编译内联到 HTML 文件内,客户端经过字符串拼接的形式将数据注入到页面的全局变量中。
如图所示全部静态资源都被标记了 inline
属性,咱们的编译工具在读取到这个属性后会将当前资源给内联到 HTML 中。同时你们注意到该模板不是以 <html>
开头的,而是有一些截断。这是为了给客户端提供注入数据空间,客户端经过模板字符串拼接的形式将新闻数据注入到全局变量中最终完成整个新闻页面的获取。前端代码中则直接使用 __INJECT_DATA_FROM_CLIENT_DONT_MODIFY__
全局变量获取注入的数据。
上面就是一套完整的本地化下发并打开的流程了,总的来说就分为四步:
可是只要有资源的分发就会涉及到资源的同步更新问题,咱们的本地化模板也是同样。在咱们的线上更新的时候如何让客户端知晓并触发更新行为,也是咱们须要去考虑的问题。实际上你们在前两张截图中能够看到,为了解决这个问题,咱们是在服务端下发的接口中还增长了一个 version
字段,用来标记当前 HTML 的版本。而当前端进行代码发布的时候,咱们的发布系统会有一个相似 npm 的 postpublish
的钩子,利用这个钩子咱们告诉服务端发布成功更新版本号。最后,当客户端接收到新的版本号的时候则会从新下载新的模板,完成一次本地模板的更新。
在前端页面中,Cookie 和 LocalStorage 等大量的特性是和域名相关的,而不巧的是咱们的页面中都有使用,因此跨域也是咱们须要考虑到的问题。咱们知道,本质上此种方案下客户端至关于使用 WebView 打开了一个本地页面,而在 Android 系统中 WebView 打开本地页面的话有三种方法:
file:///temp.html
的形式打开一个本地文件 URLloadUrl
类型,好的地方在于不须要写成文件,能够直接加载页面字符串,不过此时加载完以后页面的 URL 是 about:blank
loadData
相似,好的地方在于提供了参数可以设置当前 URL 地址从描述中能够看到,很明显最后一种 loadDataWithBaseURL
才是咱们须要的。客户端经过这个方法加载,设置当前页面的 URL 为真实线上 URL,对于前端来讲基本上就和线上环境无异了,本地化和线上 Cookie 和 LocalStorage 的共享都没有问题。不过这里须要注意,第一个参数 baseUrl 仅能管住当前页面,若是页面作了 history.pushState()
等前进后退操做的话当前页面地址又会变成 about:blank
,此时须要再设置最后一个参数 historyUrl
才行。
本文给你们讲述了实现本地化离线阅读的方案。除了以上列举的问题,咱们还碰到了一些细微的问题。例如咱们发如今网络很差的状况下客户端可能会下载模板失败缓存了不完整的代码,因此咱们增长了模板的 md5 值一并下发给客户端用来校验模板是否下载彻底。又如上文说了模板的更新,实际上内容也会有更新,特别是一些新闻的实时性会有比较高的要求,为了解决这个问题,咱们会在页面打开后再次去检查一下文章的状态,若是发生变量会切换至线上版本用来规避这个问题。除了这些以外咱们还作了完备的云控后退方案,能在方案出问题的时候完美回退到普通版本。
其实你们能够看到,本地化只是咱们在特定场景下决绝性能问题的一种特定思路。它并非使用于全部的场景,因此我在文章开头也特别强调了一下咱们的应用场景方便你们去理解。可是咱们只要理解这种方案的精髓,我相信在其它的一些特定场合总能发挥它的威力。