怎样让H5页面启动更快是很多人在探索的技术点,

2019-09-16 12:14栏目:前端开发
TAG:

关于Web静态资源缓存自动更新的思考与实践

2016/04/06 · 基础技术 · 静态资源

本文作者: 伯乐在线 - Natumsol 。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

前言

对于前端工程化而言,静态资源的缓存与更新一直是一个比较大的问题,各大公司也推出了各自的解决方案,如百度的FIS工具集。如果没有解决好这个问题,不仅会给用户造成糟糕的用户体验,而且还会给开发和调试带了很多不必要的麻烦。关于如何自动实现缓存更新,以下是自己的一点心得和体会。

今年二月份,Google 宣布将在 16 年初放弃对 SPDY 的支持,随后 Google 自家支持 SPDY 协议的服务都切到了 HTTP/2。今年 5 月 14 日,HTTP/2 以 RFC 7540 正式发布。目前,浏览器方面,Chrome 40+ 和 Firefox 36+ 都正式支持了 HTTP/2;服务器方面,著名的 Nginx 表示会在今年底正式支持 HTTP/2。

本文出处链接: https://mp.weixin.qq.com/s/ye1CeIjlfs9VSUab3gQI5g
作者:蚂蚁金服高级无线开发专家 bang

静态资源发布的痛点

我们知道,缓存对于前端性能的优化是十分重要的,在正式发布系统的时候,对于那些不经常变动的静态资源比如各种JS工具库、CSS文件、背景图片等等我们会设置一个比较大的缓存过期时间(max-age),当用户再次访问这个页面的时候就可以直接利用缓存而不是重新从服务器获取,这样不仅可以减轻服务端的压力,还可以节约网络传输的流量,同时用户体验也更好(用户打开页面更快了)。这样看起来很完美,你好我好大家都好,but,理想是美好的,现实是残酷的,假设存在这样一个浏览器,强制缓存静态资源还不给你清除缓存的机会(微信,说的就是你!),该怎么办?即使你的服务端已更新,文件的Etag值已变化,但是微信就是不给你更新文件…请允许我做一个悲伤的表情…

对于这个问题,我们很自然的想法是在每次发布新版本的时候给所有静态资源的请求后面加上一个版本参数或时间戳,类似于/js/indx.js?ver=1.0.1,但是这样存在两个问题:

  1. 微信对于加参数的静态资源还是优先使用缓存版本(实际测试的情况是这样的)。
  2. 假如这样是可行的,那么对于没有变更的静态资源也会重新从服务器获取而不是读取缓存,没有充分利用缓存。

那么有没有一种方法可以自动分辨出哪个文件发生了变化并让客户端主动更新呢?答案是肯定的。我们知道一个文件的MD5可以唯一标识一个文件。若文件发生了变化,文件的指纹值MD5也随之变化。利用这个特性我们就可以标识出哪个静态资源发生了变化,并让客户端主动更新。

不得不说这几年 WEB 技术一直在突飞猛进,爆炸式发展。昨天还觉得 HTTP/2 很遥远,今天已经遍地都是了。对于新鲜事物,有些人不愿意接受,觉得好端端为什么又要折腾;有些人会盲目崇拜,认为它是能拯救一切的救世主。HTTP/2 究竟会给前端带来什么,什么都不是?还是像某些人说的「让前端那些优化小伎俩直接退休」?我打算通过写一系列文章来尝试回答这个问题,今天是第一篇。

阿里妹导读: 越来越多的APP内业务使用H5的方式实现,怎样让H5页面启动更快是很多人在探索的技术点,本文梳理了启动过程中的各个点,分别从前端和客户端角度去探讨有哪些优化方案,供大家参考。

如何解决?

经过前文的介绍,我们知道了可以利用文件的指纹值来标识需要客户端主动更新的文件,但是如何实现呢?经过自己的思考和调研后,大致思路为:

  1. 在每次发布之前,利用Gulp对所有的静态资源进行预处理,重命名为原文件名 + 文件MD5值 + 文件后缀名的形式。比如index.js重命名为index-c6c9492ce6.js
  2. 生成一份manifest,标明了预处理前后文件之间的对应关系.manifest文件的样子为:
JavaScript

{ "index.js": "index-c6c9492ce6.js", "lib/jQuery/jQuery.js":
"lib/jQuery/jQuery-683c73084c.js", "require.js":
"require-c8e8015f8d.js", "style.css": "style-125d3a3f82.css",
"tools.js": "tools-5666ee48e9.js" }

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f4b6669294327058473-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f4b6669294327058473-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f4b6669294327058473-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f4b6669294327058473-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f4b6669294327058473-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f4b6669294327058473-6">
6
</div>
<div class="crayon-num" data-line="crayon-5b8f4b6669294327058473-7">
7
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f4b6669294327058473-1" class="crayon-line">
{
</div>
<div id="crayon-5b8f4b6669294327058473-2" class="crayon-line crayon-striped-line">
  &quot;index.js&quot;: &quot;index-c6c9492ce6.js&quot;,
</div>
<div id="crayon-5b8f4b6669294327058473-3" class="crayon-line">
  &quot;lib/jQuery/jQuery.js&quot;: &quot;lib/jQuery/jQuery-683c73084c.js&quot;,
</div>
<div id="crayon-5b8f4b6669294327058473-4" class="crayon-line crayon-striped-line">
  &quot;require.js&quot;: &quot;require-c8e8015f8d.js&quot;,
</div>
<div id="crayon-5b8f4b6669294327058473-5" class="crayon-line">
  &quot;style.css&quot;: &quot;style-125d3a3f82.css&quot;,
</div>
<div id="crayon-5b8f4b6669294327058473-6" class="crayon-line crayon-striped-line">
  &quot;tools.js&quot;: &quot;tools-5666ee48e9.js&quot;
</div>
<div id="crayon-5b8f4b6669294327058473-7" class="crayon-line">
}
</div>
</div></td>
</tr>
</tbody>
</table>
  1. 在渲染视图模版的时候,根据manifest,将预处理前的静态资置换为预处理后的静态资源。
  2. 如果在浏览器端用到了模块加载器(这里以实现了AMD标准的requireJS为例),在每次发布的时候需要根据manifest对模块进行mapping,将配置文件以内联JS的形式写入到模版页面里面,类似于:
JavaScript

&lt;script&gt; requirejs.config({ "baseUrl": "/js", "map": { "*": {
"index": "index-c6c9492ce6", "jquery":
"lib/jQuery/jQuery-683c73084c", "require": "require-c8e8015f8d",
"tools": "tools-5666ee48e9" } } }); &lt;/script&gt;

<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<tbody>
<tr class="odd">
<td><div class="crayon-nums-content" style="font-size: 13px !important; line-height: 15px !important;">
<div class="crayon-num" data-line="crayon-5b8f4b666929d715705975-1">
1
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f4b666929d715705975-2">
2
</div>
<div class="crayon-num" data-line="crayon-5b8f4b666929d715705975-3">
3
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f4b666929d715705975-4">
4
</div>
<div class="crayon-num" data-line="crayon-5b8f4b666929d715705975-5">
5
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f4b666929d715705975-6">
6
</div>
<div class="crayon-num" data-line="crayon-5b8f4b666929d715705975-7">
7
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f4b666929d715705975-8">
8
</div>
<div class="crayon-num" data-line="crayon-5b8f4b666929d715705975-9">
9
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f4b666929d715705975-10">
10
</div>
<div class="crayon-num" data-line="crayon-5b8f4b666929d715705975-11">
11
</div>
<div class="crayon-num crayon-striped-num" data-line="crayon-5b8f4b666929d715705975-12">
12
</div>
<div class="crayon-num" data-line="crayon-5b8f4b666929d715705975-13">
13
</div>
</div></td>
<td><div class="crayon-pre" style="font-size: 13px !important; line-height: 15px !important; -moz-tab-size:4; -o-tab-size:4; -webkit-tab-size:4; tab-size:4;">
<div id="crayon-5b8f4b666929d715705975-1" class="crayon-line">
&lt;script&gt;
</div>
<div id="crayon-5b8f4b666929d715705975-2" class="crayon-line crayon-striped-line">
requirejs.config({
</div>
<div id="crayon-5b8f4b666929d715705975-3" class="crayon-line">
    &quot;baseUrl&quot;: &quot;/js&quot;,
</div>
<div id="crayon-5b8f4b666929d715705975-4" class="crayon-line crayon-striped-line">
    &quot;map&quot;: {
</div>
<div id="crayon-5b8f4b666929d715705975-5" class="crayon-line">
        &quot;*&quot;: {
</div>
<div id="crayon-5b8f4b666929d715705975-6" class="crayon-line crayon-striped-line">
            &quot;index&quot;: &quot;index-c6c9492ce6&quot;,
</div>
<div id="crayon-5b8f4b666929d715705975-7" class="crayon-line">
            &quot;jquery&quot;: &quot;lib/jQuery/jQuery-683c73084c&quot;,
</div>
<div id="crayon-5b8f4b666929d715705975-8" class="crayon-line crayon-striped-line">
            &quot;require&quot;: &quot;require-c8e8015f8d&quot;,
</div>
<div id="crayon-5b8f4b666929d715705975-9" class="crayon-line">
            &quot;tools&quot;: &quot;tools-5666ee48e9&quot;
</div>
<div id="crayon-5b8f4b666929d715705975-10" class="crayon-line crayon-striped-line">
        }
</div>
<div id="crayon-5b8f4b666929d715705975-11" class="crayon-line">
    }
</div>
<div id="crayon-5b8f4b666929d715705975-12" class="crayon-line crayon-striped-line">
});
</div>
<div id="crayon-5b8f4b666929d715705975-13" class="crayon-line">
&lt;/script&gt;
</div>
</div></td>
</tr>
</tbody>
</table>

提出问题

随着移动设备性能不断增强,web页面的性能体验逐渐变得可以接受,又因为 web 开发模式的诸多好处(跨平台,动态更新,减体积,无限扩展),APP 客户端里出现越来越多内嵌 web 页面(为了配上当前流行的说法,以下把所有网页都称为 H5 页面,虽然可能跟 H5 没关系),很多 APP 把一些功能模块改成用 H5 实现。

测试

为了验证可行性,自己做了个demo,代码托管在Github。经测试,可以完美的解决之前提出的问题。

  1. 首次载入页面
    图片 1
  2. 更改index.js, 刷新页面
    图片 2

我们发现,只有index.js在更改后被主动更新了,其余的静态资源均是直接利用的缓存!。

我们知道,一个页面通常由一个 HTML 文档和多个资源组成。有一些很重要的资源,例如头部的 CSS、关键的 JS,如果迟迟没有加载完,会阻塞页面渲染或导致用户无法交互,体验很差。如何让重要的资源更快加载完是我本文要讨论的问题。

虽然说 H5 页面性能变好了,但如果没针对性地做一些优化,体验还是很糟糕的,主要两部分体验:
1.页面启动白屏时间:打开一个 H5 页面需要做一系列处理,会有一段白屏时间,体验糟糕。
2.响应流畅度:由于 webkit 的渲染机制,单线程,历史包袱等原因,页面刷新/交互的性能体验不如原生。

后记

关于前端性能优化,缓存一直是浓墨重彩的一笔。如果利用好缓存控制,不仅能提高用户体验,减少服务端流量压力,而且对于前端工程化的推进也是很有帮助的。随着web系统的业务和功能的扩大,维护前端的任务将变得越来越繁重,按照历史规律,当一件事变得越来越繁重的时候,工程化是其唯一的出路。现在的前端还很年轻,工程化的概念提出来不久,但我相信,在各大互联网公司的前端们积极推动下,前端工程化必将成为业界标配。

打赏支持我写出更多好文章,谢谢!

打赏作者

HTTP/1

本文先不讨论第二点,只讨论第一点,怎样减少白屏时间。对 APP 里的一些使用 H5 实现的功能模块,怎样加快它们的启动速度,让它们启动的体验接近原生。

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

图片 3 图片 4

1 赞 4 收藏 评论

分析

过程

关于作者:Natumsol

图片 5

阿里巴巴 前端工程师 个人主页 · 我的文章 · 5 ·    

图片 6

我们先来考虑资源外链的情况。通常,外链资源都会部署在 CDN 上,这样用户就可以从离自己最近的节点上获取数据。一般文本文件都会采用 gzip 压缩,实际传输大小是文件大小的几分之一。服务端托管静态资源的效率通常非常高,服务端处理时间几乎可以忽略。在忽略网络因素、传输大小以及服务端处理时间之后,用户何时能加载完外链资源,很大程度上取决于请求何时能发出去,这主要受下面三个因素影响:

为什么打开一个 H5 页面会有一长段白屏时间?因为它做了很多事情,大概是:

浏览器阻塞(Stalled):浏览器会因为一些原因阻塞请求。例如在 rfc2616 中规定浏览器对于一个域名,同时只能有 2 个连接(HTTP/1.1 的修订版中去掉了这个限制,详见 rfc7230,因为后来浏览器实际上都放宽了限制),超过浏览器最大连接数限制,后续请求就会被阻塞。再例如现代浏览器在加载同一域名多个 HTTPS 资源时,会有意等第一个 TLS 连接建立完成再请求其他资源;

初始化 webview -> 请求页面 -> 下载数据 -> 解析HTML -> 请求 js/css 资源 -> dom 渲染 -> 解析 JS 执行 -> JS 请求数据 -> 解析渲染 -> 下载渲染图片

DNS 查询(DNS Lookup):浏览器需要知道目标服务器的 IP 才能建立连接。将域名解析为 IP 的这个系统就是 DNS。DNS 查询结果通常会被缓存一段时间,但第一次访问或者缓存失效时,还是可能耗费几十到几百毫秒;

一些简单的页面可能没有 JS 请求数据 这一步,但大部分功能模块应该是有的,根据当前用户信息,JS 向后台请求相关数据再渲染,是常规开发方式。

建立连接(Initial connection):HTTP 是基于 TCP 协议的,浏览器最快也要在第三次握手时才能捎带 HTTP 请求报文。这个过程通常也要耗费几百毫秒;

一般页面在 dom 渲染后能显示雏形,在这之前用户看到的都是白屏,等到下载渲染图片后整个页面才完整显示,首屏秒开优化就是要减少这个过程的耗时。

当然我们一般都会给静态资源设置一个很长时间的缓存头。只要用户不清除浏览器缓存也不刷新,第二次访问我们网页时,静态资源会直接从本地缓存获取,并不产生网络请求;如果用户只是普通刷新而不是强刷,浏览器会在请求头带上协商字段 If-Modified-Since 或 If-None-Match,服务端对没有变化的资源会响应 304 状态码,告知浏览器从本地缓存获取资源。304 请求没有正文,非常小。

前端优化

也就是说资源外链的特点是,第一次慢,第二次快。

上述打开一个页面的过程有很多优化点,包括前端和客户端,常规的前端和后端的性能优化在 PC 时代已经有最佳实践,主要的是:

再来看看资源内联的情况。把 CSS、JS 文件内容直接内联在 HTML 中的方案,毫无疑问会在用户第一次访问时有速度优势。但通常我们很少缓存 HTML 页面,这种方案会导致内联的资源没办法利用浏览器缓存,后续每次访问都是一种浪费。

1.降低请求量:合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。

解决

2.加快请求速度:预解析DNS,减少域名数,并行加载,CDN 分发。

很早之前,就有网站开始针对第一次访问的用户将资源内联,并在页面加载完之后异步加载这些资源的外链版本,同时记录一个 Cookie 标记表示用户来过。用户再次访问这个页面时,服务端就可以输出只有外链版本的页面,减小体积。

3.缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存localStorage。

这个方案除了有点浪费流量之外(一份资源,内联外链加载了两次),基本上能达到更快加载重要资源的效果。但是在流量更加宝贵的移动端,我们需要继续改进这个方案。

4.渲染:JS/CSS优化,加载顺序,服务端渲染,pipeline。

考虑到移动端浏览器都支持 localStorage,可以将第一次内联引入的资源缓存起来后续使用。缓存更新机制可以通过在 Cookie 中存放版本号来实现。这样,服务端收到请求后,首先要检查 Cookie 头中的版本标记:

其中对首屏启动速度影响最大的就是网络请求,所以优化的重点就是缓存,这里着重说一下前端对请求的缓存策略。我们再细分一下,分成 HTML 的缓存,JS/CSS/image 资源的缓存,以及 json 数据的缓存。

如果标记不存在或者版本不匹配,就将资源内联输出,并提供当前版本标记。页面执行时,会把内联资源存入 localStorage,并将资源版本标记存入 Cookie;

HTML 和 JS/CSS/image 资源都属于静态文件,HTTP 本身提供了缓存协议,浏览器实现了这些协议,可以做到静态文件的缓存。

如果标记匹配,就输出 JavaScript 片段,用来从 localStorage 读取并使用资源;

总的来说,就是两种缓存:

由于 Cookie 内容需要尽可能的少,所以一般只存总的版本号。这会导致页面任何一处资源变动,都会改变总版本号,进而忽略客户端所有 localStorage 缓存。要解决这个问题可以继续改进我们的方案:Cookie 中只存放用户唯一标识,用户和资源对应关系存在服务端。服务端收到请求后根据用户标识,计算出哪些资源需要更新,从而输出更有针对性的 HTML 文档。

1.询问是否有更新:根据 If-Modified-Since / ETag 等协议向后端请求询问是否有更新,没有更新返回304,浏览器使用本地缓存。

这套方案要投入实际使用,要处理一系列异常情况,例如 JS / Cookie / localStorage 被禁用;localStorage 被写满;localStorage 内容损坏或丢失等等。考虑成本和实际收益,推荐只在移动项目中使用这种方案。

2.直接使用本地缓存:根据协议里的 Cache-Control / Expires 字段去确定多长时间内可以不去发请求询问更新,直接使用本地缓存。

HTTP/2

前端能做的最大限度的缓存策略是:HTML 文件每次都向服务器询问是否有更新,JS/CSS/Image资源文件则不请求更新,直接使用本地缓存。那 JS/CSS 资源文件如何更新?常见做法是在在构建过程中给每个资源文件一个版本号或hash值,若资源文件有更新,版本号和 hash 值变化,这个资源请求的 URL 就变化了,同时对应的 HTML 页面更新,变成请求新的资源URL,资源也就更新了。

对于 HTTP/2 来说,要解决前面这个问题简直就太容易了,开启「Server Push」即可。HTTP/2 的多路复用特性,使得可以在一个连接上同时打开多个流,双向传输数据。Server Push,意味着服务端可以在发送页面 HTML 时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。另外,服务端主动推送的资源不是被内联在页面里,它们有自己独立的 URL,可以被浏览器缓存,当然也可以给其他页面使用。

json 数据的缓存可以用 localStorage 缓存请求下来的数据,可以在首次显示时先用本地数据,再请求更新,这都由前端 JS 控制。

服务端可以主动推送,客户端也有权利选择接收与否。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送 RST_STREAM 帧来拒收。

这些缓存策略可以实现 JS/CSS 等资源文件以及用户数据的缓存的全缓存,可以做到每次都直接使用本地缓存数据,不用等待网络请求。但 HTML 文件的缓存做不到,对于 HTML 文件,如果把 Expires / max-age 时间设长了,长时间只使用本地缓存,那更新就不及时,如果设短了,每次打开页面都要发网络请求询问是否有更新,再确定是否使用本地资源,一般前端在这里的策略是每次都请求,这在弱网情况下用户感受到的白屏时间仍然会很长。所以 HTML 文件的“缓存”和跟“更新”间存在矛盾。

可以看到,HTTP/2 的 Server Push 能够很好地解决「如何让重要资源尽快加载」这个问题,一旦普及开来,可以取代前面介绍过的 HTTP/1 时代优化方案。

客户端优化

【编辑推荐】

接着轮到客户端出场了,桌面时代受限于浏览器,H5 页面无法做更多的优化,现在 H5 页面是内嵌在客户端 APP 上,客户端有更多的权限,于是客户端上可以超出浏览器的范围,做更多的优化。

HTML 缓存

先接着缓存说,在客户端有更自由的缓存策略,客户端可以拦截 H5 页面的所有请求,由自己管理缓存,针对上述 HTML 文件的“缓存”和“更新”之间的矛盾,我们可以用这样的策略解决:

1.在客户端拦截请求,首次请求 HTML 文件后缓存数据,第二次不发请求,直接使用缓存数据。

2.什么时候去请求更新?这个更新请求可以客户端自由控制策略,可以在使用本地缓存打开本地页面后再在后台发起请求询问更新缓存,下次打开时生效;也可以在 APP 启动时或某个时机在后台去发起请求预更新,提升用户访问最新代码的几率。

这样看起来已经比较完美了,HTML 文件在用客户端的策略缓存,其余资源和数据沿用上述前端的缓存方式,这样一个 H5 页面第二次访问从 HTML 到 JS/CSS/Image 资源,再到数据,都可以直接从本地读取,无需等待网络请求,同时又能保持尽可能的实时更新,解决了缓存问题,大大提升 H5 页面首屏启动速度。

问题

上述方案似乎已完整解决缓存问题,但实际上还有很多问题:

1.没有预加载:第一次打开的体验很差,所有数据都要从网络请求。

2.缓存不可控:缓存的存取由系统 webview 控制,无法控制它的缓存逻辑,带来的问题包括:

清理逻辑不可控,缓存空间有限,可能缓存几张大图片后,重要的 HTML/JS/CSS 缓存就被清除了。

磁盘 IO 无法控制,无法从磁盘预加载数据到内存。

更新体验差:后台 HTML/JS/CSS 更新时全量下载,数据量大,弱网下载耗时长。

无法防劫持:若 HTML 页面被运营商或其他第三方劫持,将长时间缓存劫持的页面。

这些问题在客户端上都是可以被解决的,只不过有点麻烦,简单描述下:

1.可以配置一个预加载列表,在APP启动或某些时机时提前去请求,这个预加载列表需要包含所需 H5 模块的页面和资源,还需要考虑到一个H5模块有多个页面的情况,这个列表可能会很大,也需要工具生成和管理这个预加载列表。

2.客户端可以接管所有请求的缓存,不走 webview 默认缓存逻辑,自行实现缓存机制,可以分缓存优先级以及缓存预加载。

3.可以针对每个 HTML 和资源文件做增量更新,只是实现和管理起来比较麻烦。

4.在客户端使用 httpdns + https 防劫持。

上面的解决方案实现起来十分繁琐,原因就是各个 HTML 和资源文件很多很分散,管理困难,有个较好的方案可以解决这些问题,就是离线包。

离线包

既然很多问题都是文件分散管理困难引起,而我们这里的使用场景是使用 H5 开发功能模块,那很容易想到把一个个功能模块的所有相关页面和资源打包下发,这个压缩包可以称为功能模块的离线包。使用离线包的方案,可以相对较简单地解决上述几个问题:

1.可以预先下载整个离线包,只需要按业务模块配置,不需要按文件配置,离线包包含业务模块相关的所有页面,可以一次性预加载。

2.离线包核心文件和页面动态的图片资源文件缓存分离,可以更方便地管理缓存,离线包也可以整体提前加载进内存,减少磁盘 IO 耗时。

3.离线包可以很方便地根据版本做增量更新。

4.离线包以压缩包的方式下发,同时会经过加密和校验,运营商和第三方无法对其劫持篡改。

到这里,对于使用 H5 开发功能模块,离线包是一个挺不错的方案了,简单复述一下离线包的方案:

1.后端使用构建工具把同一个业务模块相关的页面和资源打包成一个文件,同时对文件加密/签名。

2.客户端根据配置表,在自定义时机去把离线包拉下来,做解压/解密/校验等工作。

3.根据配置表,打开某个业务时转接到打开离线包的入口页面。

4.拦截网络请求,对于离线包已经有的文件,直接读取

5.离线包数据返回,否则走 HTTP 协议缓存逻辑。

离线包更新时,根据版本号后台下发两个版本间的 diff 数据,客户端合并,增量更新。

更多优化

离线包方案在缓存上已经做得差不多了,还可以再配上一些细节优化:

公共资源包

每个包都会使用相同的 JS 框架和 CSS 全局样式,这些资源重复在每一个离线包出现太浪费,可以做一个公共资源包提供这些全局文件。

预加载 webview

无论是 iOS 还是 Android,本地 webview 初始化都要不少时间,可以预先初始化好 webview。这里分两种预加载:

1.首次预加载:在一个进程内首次初始化 webview 与第二次初始化不同,首次会比第二次慢很多。原因预计是 webview 首次初始化后,即使 webview 已经释放,但一些多 webview 共用的全局服务或资源对象仍没有释放,第二次初始化时不需要再生成这些对象从而变快。我们可以在 APP 启动时预先初始化一个 webview 然后释放,这样等用户真正走到 H5 模块去加载 webview时就变快了。

2.webview 池:可以用两个或多个 webview 重复使用,而不是每次打开 H5 都新建 webview。不过这种方式要解决页面跳转时清空上一个页面,另外若一个 H5 页面上 JS 出现内存泄漏,就影响到其他页面,在 APP 运行期间都无法释放了。

预加载数据

理想情况下离线包的方案第一次打开时所有 HTML/JS/CSS 都使用本地缓存,无需等待网络请求,但页面上的用户数据还是需要实时拉,这里可以做个优化,在 webview 初始化的同时并行去请求数据,webview 初始化是需要一些时间的,这段时间没有任何网络请求,在这个时机并行请求可以节省不少时间。

具体实现上,首先可以在配置表注明某个离线包需要预加载的 URL,客户端在 webview 初始化同时发起请求,请求由一个管理器管理,请求完成时缓存结果,然后 webview 在初始化完毕后开始请求刚才预加载的 URL,客户端拦截到请求,转接到刚才提到的请求管理器,若预加载已完成就直接返回内容,若未完成则等待。

Fallback

如果用户访问某个离线包模块时,这个离线包还没有下载,或配置表检测到已有新版本但本地是旧版本的情况如何处理?几种方案:

1.简单的方案是如果本地离线包没有或不是最新,就同步阻塞等待下载最新离线包。这种用户打开的体验更差了,因为离线包体积相对较大。

2.也可以是如果本地有旧包,用户本次就直接使用旧包,如果没有再同步阻塞等待,这种会导致更新不及时,无法确保用户使用最新版本。

3.还可以对离线包做一个线上版本,离线包里的文件在服务端有一一对应的访问地址,在本地没有离线包时,直接访问对应的线上地址,跟传统打开一个在线页面一样,这种体验相对等待下载整个离线包较好,也能保证用户访问到最新。

第三种 Fallback 的方式还带来兜底的好处,在一些意外情况离线包出错的时候可以直接访问线上版本,功能不受影响,此外像公共资源包更新不及时导致版本没有对应上时也可以直接访问线上版本,是个不错的兜底方案。

上述几种方案策略也可以混着使用,看业务需求。

使用客户端接口

网路和存储接口如果使用 webkit 的 ajax 和 localStorage 会有不少限制,难以优化,可以在客户端提供这些接口给 JS,客户端可以在网络请求上做像 DNS 预解析/IP直连/长连接/并行请求等更细致的优化,存储也使用客户端接口也能做读写并发/用户隔离等针对性优化。

服务端渲染

早期 web 页面里,JS 只是负责交互,所有内容都是直接在 HTML 里,到现代 H5 页面,很多内容已经依赖 JS 逻辑去决定渲染什么,例如等待 JS 请求 JSON 数据,再拼接成 HTML 生成 DOM 渲染到页面上,于是页面的渲染展现就要等待这一整个过程,这里有一个耗时,减少这里的耗时也是白屏优化的范围之内。

优化方法可以是人为减少 JS 渲染逻辑,也可以是更彻底地,回归到原始,所有内容都由服务端返回的 HTML 决定,无需等待 JS 逻辑,称之为服务端渲染。是否做这种优化视业务情况而定,毕竟这种会带来开发模式变化/流量增大/服务端开销增大这些负面影响。手Q的部分页面就是使用服务端渲染的方式,称为动态直出。

最后

从前端优化,到客户端缓存,到离线包,到更多的细节优化,做到上述这些点,H5 页面在启动上差不多可以媲美原生的体验了。

总结起来,大体优化思路就是:缓存/预加载/并行,缓存一切网络请求,尽量在用户打开之前就加载好所有内容,能并行做的事不串行做。这里有些优化手段需要做好一整套工具和流程支持,需要跟开发效率权衡,视实际需求优化。

另外上述讨论的是针对功能模块类的 H5 页面秒开的优化方案,客户端 APP 上除了功能模块,其他一些像营销活动/外部接入的 H5 页面可能有些优化点就不适用,还需要视实际情况和需求而定。另外微信小程序就是属于功能模块的类别,差不多是这个套路。

这里讨论了 H5 页面首屏启动时间的优化,上述优化过后,基本上耗时只剩 webview 本身的启动/渲染机制问题了,这个问题跟后续的响应流畅度的问题一起属于另一个优化范围,就是类 RN / Weex 这样的方案,有机会再探讨。

版权声明:本文由大奖888-www.88pt88.com-大奖888官网登录发布于前端开发,转载请注明出处:怎样让H5页面启动更快是很多人在探索的技术点,