这里首屏优化是指用户安装应用后,首次打开应用消耗时间的优化。 其中比较耗时的一点是首次打开webview应用,加载静态资源。 优化思路是,首次打开应用,使用客户端拦截请求,返回本地文件。以后的请求优化由ServiceWorker接管。
webview请求过程
一个静态资源的请求,分为以下4个过程按顺序执行
- ServiceWorker
- http缓存
- App应用拦截
- 网络服务
注意:sw.js文件的规则不在此列,sw.js文件好像始终请求网络
有以下几点需要注意:
第一步 ServiceWorker 必须是安装好的
第一次进入应用,ServiceWorker并未安装,所以会被直接跳过,进入http缓存
ServiceWorker的请求,也会先查看http缓存
就是说,从ServiceWorker发出的请求,也会遵循http缓存规范,http缓存中有的文件,会被直接返回给ServiceWorker
只有ServiceWoker和http缓存都未命中,才会被App应用拦截
App应用拦截的限制
App应用拦截webview请求,是通过复写shouldInterceptRequest方法实现
@TargetApi(VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view,
WebResourceRequest request) {
经过测试,他有限制:
只会拦截html请求和写在html中的资源请求(即通过<link><script>等直接加载的资源。通过js请求的资源都不会被拦截,例如ajax请求、main.js文件中请求的sw.js、ServiceWorker中发起的请求)。没有深入研究测试,理解也可能有误。
优化思路
App应用安装时候,将首屏所需静态资源下载到本地文件中
首屏
首次打开应用,ServiceWorker未安装,也没有任何http缓存,所以所有请求会直接被App应用拦截,在这一步返回本地文件,达到秒开效果。
同时不拦截sw.js文件(ServiceWorker的注册文件),去网络服务器请求sw.js文件进行注册安装。
第二次打开
因为在首屏打开应用同时,sw.js也同时注册安装好了,同时通过cacheAll,也在ServiceWorker中缓存了最新的网络服务器中文件。
所以第二次打开,所有的请求,都可以被ServiceWorker进行拦截处理,根据上面的注意事项(通过js请求的资源都不会被App应用拦截),所以第二次及之后的请求,基本就和App应用拦截无关了。
ServiceWoker缓存的更新
- 在每次请求时候,返回ServiceWorker缓存同时,请求最新文件(副作用,在每次页面版本发生改变时候,第二次进入应用会卡,因为第一次返回ServiceWorker缓存,不会卡,但是请求到了新的index.html,第二次进入应用,根据新的index.html,发生变化的静态资源,都会从网络请求等待,第三次进入应用才不会卡)
- 更新sw.js文件时候,更新所有静态资源文件(未实践,应该可行,且无上面的副作用)
ServiceWoker文件sw.js自身的更新
对sw.js不设置缓存,每次都到网络请求最新sw.js文件即可(对于如何安装更新sw.js,本文不探讨)
具体实现
首屏加载
1. App缓存文件存放位置
2. 声明拦截列表
private void initData() {
// 主页
mMap.put(BaseUrl, "index.html");
mMap.put(BaseUrl+"/", "index.html");
mMap.put(BaseUrl + "/index.html", "index.html");
// js文件
mMap.put(BaseUrl + "/main-v1.js", "main.js");
// css文件
mMap.put(BaseUrl + "/static/css/main-v1.css", "static/css/main-v1.css");
// 图片
mMap.put(BaseUrl + "/favicon.ico", "favicon.ico");
mMap.put(BaseUrl + "/images/log.png", "images/log.png");
}
3. App应用拦截
@TargetApi(VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view,
WebResourceRequest request) {
String url = request.getUrl().toString();
Log.d(TAG, "shouldInterceptRequest>5.0: url = " + url);
if (!mIsLoadLocal) {
return super.shouldInterceptRequest(view, request);
}
if (mDataHelper.hasLocalResource(url)) {
Log.d(TAG, "shouldInterceptRequest>5.0: 资源命中:url="+url);
// super.shouldInterceptRequest(view, request); // 同时去请求网络
WebResourceResponse response =
mDataHelper.getReplacedWebResourceResponse(getApplicationContext(),
url);
if (response != null) {
return response;
}
}
return super.shouldInterceptRequest(view, request);
}
第二次加载
之后的处理流程都是类似的
说明
1. 和http缓存结合使用
ServiceWorker中的请求,也会去http缓存中抓取的,所以可以结合使用,缓存策略:
- index.html设定no-cache
- 静态资源文件缓存1年(通过每次打包后新版本号更新)
以Nginx为例:
location / {
# root html;
root /Users/frru/nginxServers;
index index.html index.htm;
autoindex on; ##显示索引
autoindex_exact_size on; ##显示大小
autoindex_localtime on; ##显示时间
if ($request_uri ~* ".html|htm") {
add_header Cache-Control "no-cache";
}
if ($request_uri ~* ".css|js|png") {
expires 360d;
# add_header Cache-Control "public, max-age=2592000, s-maxage=2592000";
add_header wall "hey!guys!give me a star.";
}
}
2. 每次项目打包时候,请同步更新sw.js文件,主要是更新其中cacheAll部分文件列表
例如版本变为v2之后,sw.js中cacheAll部分应该为
//Service Worker安装事件,其中可以预缓存资源
this.addEventListener('install', function(event) {
console.log('install');
//需要缓存的页面资源
var urlsToPrefetch = [
'./index.html',
'./main-v2.js',
'./static/css/main-v2.css',
'./static/image/log.png',
];
event.waitUntil(
caches.open(OFFLINE_CACHE_NAME).then(function(cache) {
return cache.addAll(urlsToPrefetch);
})
);
});
3. 注意:并未实践
以上部分,关于sw.js部分的主动更新文件,我并未实现调试,理论上可行。之前我是采取边返还ServiceWorker缓存,边请求最新数据的方案,副作用上面已经说明了。
4. sw.js主动更新文件和边返回缓存,边加载新资源,可以结合使用
5. 关于缓存文件列表
现在打包工具一般都会生成静态资源列表,例如Create React App打包项目会生成一个asset-manifest.json文件:
{
"main.css": "./static/css/main.484bfb43.chunk.css",
"main.js": "./static/js/main.a763f74c.chunk.js",
"main.js.map": "./static/js/main.a763f74c.chunk.js.map",
"runtime~main.js": "./static/js/runtime~main.9eb600ee.js",
"runtime~main.js.map": "./static/js/runtime~main.9eb600ee.js.map",
"static/css/2.81f174d0.chunk.css": "./static/css/2.81f174d0.chunk.css",
"static/js/2.3caf1636.chunk.js": "./static/js/2.3caf1636.chunk.js",
"static/js/2.3caf1636.chunk.js.map": "./static/js/2.3caf1636.chunk.js.map",
"index.html": "./index.html",
"precache-manifest.95764df349dcc4f784ad6dbed9254278.js": "./precache-manifest.95764df349dcc4f784ad6dbed9254278.js",
"service-worker.js": "./service-worker.js",
"static/css/2.81f174d0.chunk.css.map": "./static/css/2.81f174d0.chunk.css.map",
"static/css/main.484bfb43.chunk.css.map": "./static/css/main.484bfb43.chunk.css.map"
}
App应用可以利用,拉取静态文件。
也可以用来生成ServiceWorker主动获取文件列表
腾讯VasSonic框架
简介:腾讯SNG增值产品部技术团队研发的轻量级高性能Hybrid框架VasSonic
官方介绍文档
业务场景
主要是应对手Q的业务,一些重点常用业务,例如游戏分发中心、会员特权中心、个性装扮商场等,是H5页面,首屏加载时候,会比较慢。
随着业务发展,有些业务形态发生了变化,例如个性推荐是动态内容。
解决方案
VasSonic的前身
-
终端优化
一些常见的首页优化:懒加载、X5内核预加载、WebView对象复用 - 静态直出
-
离线预推(即熟悉的离线包)
VasSonic做了功能增强,即离线增量包,大大减少了包大小。 -
动态直出功能
因为业务形态发生了变化(个性推荐),做了对应的功能。
实时拉取用户数据并在服务端渲染后返回给客户端,也就是动态直出的方案。 - webso的尝试
QQ空间技术团队在解决动态直出方面的wns+html解决方案。不太适合手Q业务场景。有一定的借鉴参考。
综上,不管改进下,最后诞生了VasSonic。
VasSonic的诞生
- 并行加载
就是把WebView初始化和请求资源由原来的串行改成并行,减少等待时间。 - 动态缓存
把页面拆分成静态部分和动态部分,动态部分更新后去主动更新界面。 - 页面分离
对上面动态缓存的具体实现
后面的小章节都是具体使用VasSonic的规范。
???
这两种方式感觉都是针对手Q业务场景,在用户打开手Q时候,预加载一些重要页面和预判页面。
腾讯浏览服务 Service Worker最佳实践
ServiceWorker的不用讲了,这里关于首屏的加载,比较狠,直接打成压缩文件,让x5内核注册成ServiceWorker,省去了用户需要打开页面才能注册ServiceWorker的过程。
X5内核Service Worker功能扩展
首次访问解决方案
首次访问解决方案旨在用户访问业务前实现业务的资源缓存,让用户在第一次真正访问业务时能够让业务页面以最快的速度展示出来。针对该主旨,X5内核实现了三套具体实现方案:
- 离线包方式
- X5内核后台云下发指令
- X5内核扩展接口