优化APP中H5页面加载速度

H5侧加载优化

先了解下webview加载一个页面干了哪些事情:

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


页面从初始化webview开始到DOM渲染页面过程就是用户加载等待的过程,DOM渲染是将CSS/HTML转化成用户可见的页面,如果提高这个速度就能提升页面的加载速度。

H5 页面从发起请求到最终渲染完成的完整流程可分为 网络请求、HTML 解析、资源加载、DOM 构建、渲染 几个阶段


所以从H5段分析来看,页面显示需要满足HTML 文档完全解析(DOM 树构建完成)、所有同步的 <script> 执行完毕(除非标记为 async 或 defer)才能正常渲染页面,所以我们可以采用以下措施:

1、iframe加载

2、异步加载的 JS(<script async> 或 <script defer>)。

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

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

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

6、渲染:JS/CSS优化,加载顺序,服务端渲染模板直出。

实际项目中前端能做的很有限,其实H5的主要消耗就在HTML、CSS、IMage、JS等资源的下载,如果我们将这些资源本地化到客户端就能解决资源加载的问题,这种离线包方案网上有很多现成的方案可以直接使用。


客户端加载方案:

这里我们介绍下自己实现离线包方案


离线包的存放和更新机制

可以使用CDN存放静态资源,同时包含资源信息的配置json文件

//配置json文件内容

"version":"********", 

"files": [

 "https://xxx/index.html",

 "https://xxx.png",

"https://xxx.js", 

"https://xxx.css",

 "https://xxx.json" 

]

 }

当APP冷启动是请求CDN的json文件, 对比json中间中version和本地不一致时需要重新请求files中的资源,下载完成后替换本地资源。

本地包更新完成后需要拦截并将本地包内容显示给H5

拦截并加载本地资源包

NSURLProtocol

NSURLProtocol是Foundation框架提供的网络请求拦截抽象类,可以拦截大多数基于URL Loading System的请求

拦截能力取决于具体使用的网络框架和连接方式

某些特殊请求和连接方式无法被拦截

使用方式:使用时要创建一个继承NSURLProtocol的子类,不应该直接实例化一个NSURLProtocol。


+ (BOOL)canInitWithRequest:(NSURLRequest *)request

判断当前protocol是否要对这个request进行处理(所有的网络请求都会走到这里)。

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request

可选方法,对于需要修改请求头的请求在该方法中修改,一般直接返回request即可。

- (void)startLoading

重点是这个方法,拦截请求后在此处理加载本地的资源并返回给webview。

- (void)stopLoading

对于拦截的请求,NSURLProtocol对象在停止加载时调用该方法


- (void)startLoading {

 //标示该request已经处理过了,防止无限循环

 [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:self.request]; 

NSData *data = [NSData dataWithContentsOfFile:filePath];

//硬编码 开始嵌入本地资源到web中

 [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];

 [[self client] URLProtocol:self didLoadData:data];

 [[self client] URLProtocolDidFinishLoading:self];

}





但是在使用时我们发现WKWebView中需要网络请求并没有被拦截到,原因是WKWebView 使用独立的网络进程(iOS 11+),与传统的 URL Loading System 分离,还有WKWebView使用ajax请求页是无法被拦截。

当然在调试是我们也可以通过拦截苹果私有API方法WKBrowsingContextController和registerSchemeForCustomProtocol通过反射的方式拿到了私有的 class/selector。通过把注册把 http 和 https 请求交给 NSURLProtocol 处理,不过这种方式是不允许上架到苹果商店的。

Class cls = NSClassFromString(@"WKBrowsingContextController"); 

SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");

if ([(id)cls respondsToSelector:sel]) {

// 把 http 和 https 请求交给 NSURLProtocol 处理

[(id)cls performSelector:sel withObject:@"http"];

[(id)cls performSelector:sel withObject:@"https"];

}

// 这下 NSURLProtocolCustom 就可以用啦

[NSURLProtocol registerClass:[NSURLProtocolCustom class]];

通过以上处理,可以正常拦截处理,但是又发现拦截不了post请求(拦截到的post请求body体为空),即使在canInitWithRequest:方法中设置对于POST请求的request不处理也不能解决问题。内流。。。

经了解,算是 WebKit 的一个缺陷吧。首先 WebKit 进程是独立于 app 进程之外的,两个进程之间使用消息队列的方式进行进程间通信。比如 app 想使用 WKWebView 加载一个请求,就要把请求的参数打包成一个 Message,然后通过 IPC 把 Message 交给 WebKit 去加载,反过来 WebKit 的请求想传到 app 进程的话(比如 URLProtocol ),也要打包成 Message 走 IPC。出于性能的原因,打包的时候 HTTPBody 和 HTTPBodyStream 这两个字段被丢弃掉了,这个可以参考 WebKit 的源码,这就导致 -[WKWebView loadRequest:] 传出的 HTTPBody 和 NSURLProtocol 传回的 HTTPBody 全都被丢弃掉了。 所以如果通过 NSURLProtocol 注册拦截 http scheme,那么由 WebKit 发起的所有 http POST 请求就全都无效了,这个从原理上就是无解的。

当然网上也出现一些解决方案,但是本人尝试没有成功。同时拦截后对ATS支持不好。再结合又使用了苹果私有API有被拒风险,最终决定弃用NSURLProtocol拦截的方案。

WKURLSchemeHandler

iOS11以后苹果推出了WKURLSchemeHandler来拦截资源请求。

使用前要与前端统一URL-Scheme,如:myScheme,资源定义成myScheme://xxx/path/xxxx.css。native端使用时,先注册myScheme,WKWebView请求加载网页,遇到myScheme的资源,就会被hock住,然后使用本地已下载好的资源进行加载。

//本例中WKWebView将把URLScheme为customScheme的请求交由CustomURLSchemeHandler类的实例处理

//在WKWebview发起loadRequest之前注册

[configuration setURLSchemeHandler:[MySchemeHandler new] forURLScheme: @"myScheme"];

注意:

setURLSchemeHandler注册时机只能在WKWebView创建WKWebViewConfiguration时注册。

WKWebView 只允许开发者拦截自定义 Scheme 的请求,不允许拦截 “http”、“https”、“ftp”、“file” 等的请求,否则会crash。

【补充】WKWebView加载网页前,要在user-agent添加个标志,H5遇到这个标识就使用customScheme,否则就是用原来的http或https。

MySchemeHandler中将拦截到的请求改成本地资源放回给H5,如果没有请求到本地资源就转成http、https发送给H5.



在联调本地 H5 页面过程中,发现首次加载页面时间比后续打开时间都慢很多,原因预计是 webView 首次初始化时候需要启动资源和服务较多,于是尝试客户端在预先初始化 webView 方案,果然这样第一次打开页面时候就变快了。同时为了 H5 在第一次打开时能直接展示数据,客户端在页面打开前就预拉取数据并缓存,这样来减少请求数据时间导致的白屏。详细教程请参考:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容