作为一个前端工程师,我们都知道页面的速度对产品业务的发展至关重要,在日常工作中我们除了完成业务逻辑之外,也在持续的优化前端页面的速度。但我们往往从加载速度和渲染速度这两个方面着手,而忽略了感知速度的提升,本文我们就来探讨一下如何从感知速度着手提升前端页面的感知速度。

1.UI预览

对于常见的前后端分离项目,一般的工作流程是:

1. 页面JS请求后端接口
2. 后端接口返回JSON数据
3. 前端模板引擎根据JSON数据和预设模板输出最终的HTML片段
4. 前端模板引擎将HTML片段插入DOM中,浏览器重新局部渲染,显示最终结果

而从第1步到第4步之间,我们往往使用一个加载条,来提示用户继续等待。本质上,在这期间UI对用户不可见(UI是阻塞的),给用户的感觉就是页面较慢,而UI预览就可以从这里介入从而提升用户感知速度。

在使用一些知名网站时,我们经常会看到UI预览,比如新浪微博的文章,在文章主体加载完成之前提前给出页面的基本布局:

weibo-ui-preview
facebook在信息流加载完成之前,也会给出UI预览:
facebook-interface-preview

2.UI预览的实现方式

UI预览的实现方式主要有两种,一种就是伪UI预览,另一种是真实UI预览。

(1).伪UI预览

伪UI预览的应用场景较为有限,需要提前人工写好需要预览的UI的布局和样式,因此其只能适用于UI较为简单的场景。facbook的每一条feed的布局都非常相似,它使用的就是伪UI预览,在真实feed加载完成之前,先显示UI预览,当真实feed加载完成后,隐藏UI预览,显示真实feed内容。比如下图是一条feed的UI预览:

facebook-feed-preview

(2).真实UI预览

真实UI预览实现起来较为复杂,但是其适用场景较为也更为广泛。一种实现真实UI预览的方案是,通过真实测试数据生成真实UI预览,当然这里对测试数据有一定的要求:

(1)文本用空格代替
(2)图像用默认图像代替

真实UI预览的生成可以通过构建工具完成,比如grunt、gulp、fis等。当真实数据获取成功时,即可隐藏UI预览,显示真实用户页面。

UI预览可以极大的提升用户的感知速度,使网站留住更多用户。虽然在网速较好的情况下,其作用有限,但却不失为锦上添花之举。


参考资料

https://dribbble.com/shots/2138907-Non-Blocking-Settings-Page-UI

http://www.callumhart.com/blog/non-blocking-uis-with-interface-previews

HTML5提供了一种离线应用缓存机制,使得网页应用可以离线使用,这种机制在移动端浏览器上支持度非常广,所有版本的android和ios浏览器都能很好的支持。我们可以放心的使用该特性来加速移动端页面的访问速度。

开启离线缓存的步骤也非常简单:

(1) 准备缓存清单文件(menifest text/cache-manifest),用于描述页面需要缓存的资源列表
(2) 在需要离线使用的页面中添加menifest属性,用于指定缓存清单文件的路径

让我们首先理解浏览器实现离线缓存的详细步骤,然后探讨使用离线缓存加速移动端网页访问速度的方案。

1. 下载/更新缓存的详细步骤

(1)当浏览器访问一个包含 manifest 特性的文档时,如果应用缓存不存在,浏览器会加载文档,然后获取所有在清单文件中列出的文件,生成应用缓存的第一个版本。

(2)对该文档的后续访问会使浏览器直接从应用缓存(而不是服务器)中加载文档与其他在清单文件中列出的资源。此外,浏览器还会向 window.applicationCache 对象发送一个 checking 事件,在遵循合适的 HTTP 缓存规则前提下,获取清单文件。

(3)如果当前缓存的清单副本是最新的,浏览器将向 applicationCache 对象发送一个 noupdate 事件,到此,更新过程结束。注意,如果你在服务器修改了任何缓存资源,同时也应该修改清单文件,这样浏览器才能知道它需要重新获取资源。

(4)如果清单文件已经改变,文件中列出的所有文件—也包括通过调用 applicationCache.add() 方法添加到缓存中的那些文件—会被获取并放到一个临时缓存中,遵循适当的 HTTP 缓存规则。对于每个加入到临时缓存中的文件,浏览器会向 applicationCache 对象发送一个 progress 事件。如果出现任何错误,浏览器会发送一个 error 事件,并暂停更新。

(5)一旦所有文件都获取成功,它们会自动移送到真正的离线缓存中,并向 applicationCache 对象发送一个 cached 事件。鉴于文档早已经被从缓存加载到浏览器中,所以更新后的文档不会重新渲染,直到页面重新加载(可以手动或通过程序).

以上只是一些详细步骤,具体也有一些值得注意的细节,比如menifest文件中列出的资源url必须和menifest本身使用同样的网络协议,如果menifest文件使用的是http协议,则列表中https协议的文件就会被忽略。总之,每当在使用Application Cache的过程中遇到奇怪的问题时,随时查阅W3C标准文档。

2.离线缓存事件流

浏览器在解析HTML文档的过程中,遇到HTML标记的menifest属性时,就立即在后台开启一个新的进程下载需要离线缓存的资源。在下载离线缓存的过程中,会在ApplicationCache上触发一系列事件。

事件名称 解释 后续事件
checking checking事件是整个事件序列中的第一个,表示浏览器在首次下载缓存,或者在检查缓存是否有更新 noupdate, downloading, obsolete, error
noupdate noupdate事件是整个事件序列的最后一个,用来表示缓存没有更新
downloading 浏览器正在首次下载menifest中列出的离线资源,或者浏览器发现缓存有更新,并且正在下载该更新 progress, error, cached, updateready
progress 浏览器正在下载menifest中的离线资源,其中progressEvent对象中total表示需要下载的总资源的数量,loaded表示已经处理过的资源数量 progress, error, cached, updateready
cached 浏览器已经成功下载了menifest中的离线资源,应用已经被缓存
updateready 缓存有更新并且更新已经下载完成,这是可以使用ApplicationCache.swapCache()来将应用切换到最新缓存
obsolete 应用已经有离线缓存了,但menifest文件无法加载成功,则删除现有离线缓存
error 发生错误的情况比较多:可能是首次视图缓存离线文件时,发现menifest文件返回404;也可能是下载缓存过程中发生了fatal错误,比如超出了最大可用存储空间的限制,发现了header为Cache-Control: no-store的资源;还有可能是缓存在更新过程中发现menifest文件发生了变化

关于离线缓存中要触发的事件,有一个很有趣的特性。从离线缓存W3C标准中,我们经常看到如下描述:'queue a post-load task to fire a simple event named (checking|noupdate|downloading)',每一个HTML文档都有一个离线缓存事件队列(queue),离线缓存下载过程中的事件都存放在这个队列里,用于在文档的onload事件触发后执行。也就是说,所有的ApplicationCache事件都在html的onload事件触发后才触发。

3.使用离线缓存加速移动端网页开发

上文提到过,Application Cache在移动端支持的很好,几乎所有的android浏览器和ios浏览器都能很好的支持。事实上使用Application Cache加速移动端网页访问速度是行业类普遍采用的优化方案,在包括新浪微博和QQ浏览器等大型产品中都有非常广泛的使用。但是在使用离线缓存时,我们需要留意一些问题。

(1).二次更新的问题

我们知道每次使用离线应用时,在有网络连接的情况下,浏览器都会逐字节的检查menifest文件是否有更新,而当menifest文件有更新时,就会重新下载menifest文件中列出的所有资源,资源下载成功后会触发updateready事件。这时离线应用本身并不会立即更新,而会在下次访问时才更新,这就是我们所说的二次更新。我们在开发web程序时,一般都是前端页面和后端接口同步更新,但是二次更新问题会导致页面更新不受控制,无法和后端接口同步更新,因此要做好后端接口的向前兼容,这迫使我们抛弃传统的web开发思路而采取native开发思路来管理离线应用。我们可以通过检测updateready事件,在新的缓存可用时通知用户更新。

window.addEventListener('load', function(e) {  
    window.applicationCache.addEventListener('updateready', function(e) { 
        //缓存更新完毕 
        if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {  
            //切换为最新缓存
            window.applicationCache.swapCache();  
            if (confirm('新版本已经更新完成,是否重新加载?')) {  
                window.location.reload();  
            }  
        }  
    }, false);  
}, false);  

(2).超出大小限制的问题

Application Cache的W3C标准中并没有对大小限制做出详细的描述。因此浏览器实现起来也是参差不齐,为了最大化的使用离线缓存,我们应该清楚自己业务的目标浏览器的离线缓存大小限制,使用chrome://appcache-internals/可以轻易的管理chrome浏览器的离线缓存。我们可以写一个DEMO,采用二分搜索法,不断的测试浏览器的Application Cache大小限制,直到触发相应的error事件。这个链接可以测试Application Cache的大小限制。通过监听Application的error事件来能够处理超出离线缓存大小限制的情况:

window.applicationCache.addEventListener('error',function(e){
    if(e.reason == 'quota'){
        //超出离线缓存大小限制
    }
});

离线缓存的是一套网页加速方案,超出Application Cache的大小限制后,会对我们的应用有不同的影响,具体表现就是:

首次下载缓存时超出大小,所有资源都不会缓存,而是请求网络,应用功能正常。
更新资源后超出大小,缓存不会更新,应用无法更新。

(3).webview中的问题

在标准的html5浏览器中,我们可以放心的使用Application Cache,并且不需要任何设置。但是在webview中,则可能需要显示的设置,比如android系统的webview默认是不支持Application Cache的,因此需要显示设置:

webView.getSettings().setAppCacheEnabled(true);        //默认是关闭的
webView.getSettings().setAppCacheMaxSize(1024*1024*5); //缓存大小
webView.getSettings().setAppCachePath("路径");          //缓存路径

参考资料

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Using_the_application_cache

https://www.w3.org/TR/html5/