Webview加载H5优化小记

10,193 阅读13分钟

原文链接

一、概述

1、背景
  • 鉴于H5的优势,客户端的很多业务都由H5来实现,Webview成了App中H5业务的主要载体。
  • WebView组件是iOS组件体系中非常重要的一个,之前的UIWebView 存在严重的性能和内存消耗问题,iOS 8之后推出WKWebView,旨在代替UIWebView;
  • WKWebView在性能、稳定性、内存占用上有很大的提升,支持更多的HTML5特性,高达60fps的滚动刷新率以及内置手势;可以通过KVO监控网络加载的进度,获取网页title;
  • 实践中,大部分App的H5业务将由WKWebview承载。
2、H5页面的体验问题

从用户角度,相比Native页面,H5页面的体验问题主要有两点:

  • 页面打开时间慢:打开一个 H5 页面需要做一系列处理,会有一段白屏时间,体验糟糕。
  • 响应流畅度较差:由于 WebKit 的渲染机制,单线程,历史包袱等原因,页面刷新/交互的性能体验不如原生。

这里讨论的是:第一点,怎样减少白屏时间。

二、Webview打开H5

通过Webview打开H5页面,请求并得到 HTML、CSS 和 JavaScript 等资源并对其进行处理从而渲染出 Web 页面。

1、加载流程
  • 初始化Webview -> 请求页面 -> 下载数据 -> 解析HTML -> 请求 js/css 资源 ->DOM 渲染 -> 解析 JS 执行 -> JS 请求数据 -> 解析渲染 -> 下载渲染图片-> 页面完整展示

H5页面加载流程.png

  • DOM渲染之前耗时主要在两部分:初始化Webview数据请求,一般Webview首次初始化在400ms这个量级,二次加载能少一个量级。
  • 数据请求依赖网络,网络请求一般经过:DNS查询、TCP 连接、HTTP 请求和响应。数据包括HTML、JS和CSS资源,这些都是在webview在loadRequest:之后做的,这一阶段,用户所见到的都是白屏。(虽然4G已经成为主流,但是4G延迟明显高于Wifi)。
2、H5页面渲染

对H5页面的渲染,主要包括:渲染树构建、布局及绘制,具体可分为:

  • 处理 HTML 标记并构建 DOM 树。

  • 处理 CSS 标记并构建 CSSOM(CSS Object Model) 树。

  • 将 DOM 与 CSSOM 合并成一个渲染树。

  • 根据渲染树来布局,以计算每个节点的几何信息。

  • 将各个节点绘制到屏幕上。

说明:这五个步骤并不一定一次性顺序完成。如果 DOM 或 CSSOM 被修改,以上过程需要重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。实际页面中,CSS 与 JavaScript 往往会多次修改 DOM 和 CSSOM。具体参考:DOM渲染机制与常见性能优化

3、H5页面加载度量

H5页面性能通过通过如下度量指标获取:

  • 白屏时间(first paint time)—— 地址栏输入网址后回车 到 浏览器出现第一个元素(网络、服务端性能,前端页面结构设计);

  • 可操作时间(dom Interactive) —— 地址栏输入网址后回车 到 用户可以进行正常的点击、输入等操作,默认可以统计domready时间,因为通常会在这时候绑定事件操作

  • 首屏时间—— 地址栏输入网址后回车 - 浏览器第一屏渲染完成(白屏时间,资源下载执行时间等);

  • 总下载时间——页面所有资源都加载完成并呈现出来所花的时间,即页面 onload 的时间;

  • 加载时间:页面所有元素加载完成;

说明:可以使用 前端的性能监控框架window.performance 是实现这些指标数据的获取。当然,在App中打开H5页面,还需要统计WebView的初始化时间。

4、总结
  • 分析Webview打开H5打开的过程,我们发现,在H5优化中,前端重任在肩;
降低请求量:合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。
加快请求速度:预解析DNS,减少域名数,并行加载,CDN 分发。
缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存localStorage。
渲染:JS/CSS优化,加载顺序,服务端渲染,pipeline。
  • 但是客户端也很重要,主要优化DOM渲染之前这些事情,可以做有:减少DNS时间预初始化WebView 以及 HTML、JS、CSS等资源离线下载
  • 列举在某业务中笔者实践过的比较trick的优化方案,然后再引出笔者认为理想的方案。

二、WebView的客户端优化(trick版)

由于是接入第三方的H5页面,接入离线包方案,需要比较繁杂的商务沟通和技术挑战(业务逻辑和代码超级诡异),临时采用如下优化方案

1、预加载资源
  • 将首页面需要的JS文件CSS文件等资源放在一个URL地址(和业务url同域名);
  • 启动App后,间隔X秒去加载;加载的策略是,检查当前和上一次间隔时间,超时则加载,有效期忽略预加载请求。
2、预初始化Webview
  • 首次初始化Webview,需要初始化浏览器内核,需要的时间在400ms这个量级;二次初始化时间在几十ms这个量级;

  • 根据此特征:选择在APP 启动后X秒,预创建(初始化)一个 Webview 然后释放,这样等使用到 H5 模块,再加载 Webview时,加载时间也少了不少。

  • 结合步骤一中预加载公共资源,也需要Webview,所以选择在加载公共资源包时候,首次初始化Webview,加载资源,然后释放。

3、最终方案(迫不得已)

​ 由于第三方业务H5很多问题,和人力上不足;不得不需要客户端强行配合优化,在产品的要求下,不得不采用如下方案,方案的前提是:业务H5尽可能少修改,甚至不修改,客户端还要保证首屏加载快;

  • 预加载资源
  • 预创建Webview并加载首页H5,驻留在内存中,需要的时候,立刻显示。
4、方案的后遗症
  • 我不建议这种trick做法,因为自从开了这个口子,后续很多H5需求不走之前既定的离线包方案,在内存中预创建多个Webview (最多4个),加载H5时候不用新建Webview,从Webview池中获取;
  • 此种Webview池方案带来诸多隐患:内存压力、诡异的白屏、JS造成的内存泄露,页面的清空等等问题(填坑填到掉头发)。

三、离线包方案

1、概述
  • 离线包方案才是业务主流的H5加载优化方案,非常建议在客户端团队和前端团队推广,类似预创建Webview加载H5不应该成为主流。

  • 将每个独立的H5功能模块,相关HTML、Javascript、CSS 等页面内静态资源打包到一个压缩包内,客户端可以下载该离线包到本地,然后打开Webview,直接从本地加载离线包,从而最大程度地摆脱网络环境对 H5 页面的影响。

  • 离线包可以提升用户体验(页面加载更快),还可以实现动态更新(在推出新版本或是紧急发布的时候,可以把修改的资源放入离线包,通过更新配置让应用自动下载更新)

2、方案描述

引用bang的离线包方案,简单描述如下

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

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

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

  • 拦截网络请求,对于离线包已经有的文件,直接读取离线包数据返回,否则走 HTTP 协议缓存逻辑。

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

说明:目前WKWebView已经能成为主流,但是WKWebView在实现离线包方案时,拦截网络请求有坑。

3、WKWebView拦截网络请求的坑
  • 虽然NSURLProtocol可以拦截监听每一个URL Loading System中发出request请求,记住是URL Loading System中那些类发出的请求,也支持AFNetwoking,UIWebView发出的request,NSURLProtocol都可以拦截和监听。
  • 因为WKWebView 在独立进程里执行网络请求。一旦注册 http(s) scheme 后,网络请求将从 Network Process 发送到 App Process,这样 NSURLProtocol 才能拦截网络请求。
  • 但是在 WebKit2 的设计里使用 MessageQueue 进行进程之间的通信,Network Process 会将请求 encode 成一个 Message,然后通过 IPC(进程间通信) 发送给 App Process。出于性能的原因,encode 的时候 将HTTPBody 和 HTTPBodyStream 这两个字段丢弃掉()
  • 因此,如果通过 registerSchemeForCustomProtocol 注册了 http(s) scheme, 那么由 WKWebView 发起的所有 http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理,导致 post 请求 body 被清空
//苹果开源的 WebKit2 源码暴露了私有API:
+ [WKBrowsingContextController registerSchemeForCustomProtocol:]

//通过注册 http(s) scheme 后 WKWebView 将可以使用 NSURLProtocol 拦截 http(s) 请求:
Class cls = NSClassFromString(@"WKBrowsingContextController”); 
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); 
if ([(id)cls respondsToSelector:sel]) { 
   // 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理 
   [(id)cls performSelector:sel withObject:@"http"]; 
   [(id)cls performSelector:sel withObject:@"https"]; 
}

**说明1:**名目张胆使用私有API,是过不了AppStore审核的,具体使用什么办法,想来你也懂(hun xiao)。

说明2:一旦打开ATS开关:Allow Arbitrary Loads 选项设置为NO,通过 registerSchemeForCustomProtocol 注册了 http(s) scheme,WKWebView 发起的所有 http(s) 网络请求将被阻塞(即便将Allow Arbitrary Loads in Web Content 选项设置为YES);

说明3:iOS11之后可以通过WKURLSchemeHandler去完成对WKWebView的请求拦截,不需要再调用私有API解决上述问题了。

4、WKWebView自定义资源scheme
  • 向WKWebView 注册 customScheme, 比如 dynamic://, 而不是https或http,避免对https或http请求的影响
  • 保证使用离线包功能的请求,没有post方式,遇到customScheme请求,比如dynamic://www.dynamicalbumlocalimage.com/,通过 NSURLProtocol 拦截这个请求并加载离线数据。
  • iOS 11上, WebKit 提供的WKURLSchemeHandler可实现拦截,需要注意的只允许开发者拦截自定义 Scheme 的请求,不允许拦截 “http”、“https”、“ftp”、“file” 等的请求,否则会crash。

四、浏览器相关知识

1、移动浏览器分类
  • 系统内置浏览器:搭载在系统固件中,一般由系统产商开发,如 iOS 的 Safari,Android 的 Android WebKit 或 Chrome。
  • Webview组件:系统提供给原生应用的内置浏览器组件。比如 iOS 上的WKWebview、UIWebview(iOS 12后废弃)、Android的Webview。使用内置浏览器的底层组件(如渲染引擎、JavaScript 引擎),但某些方面可能会有所不同,如性能上会有所限制。
  • 第三方浏览器:Firefox、Opera、UC、QQ,360
2、浏览器内核(Rendering Engine)
  • 浏览器的内核是分为两个部分的,一是渲染引擎,另一个是JS引擎。现在JS引擎比较独立,内核更加倾向于说渲染引擎。
  • 常见的内核主要分四种:
    • Trident(IE):代表作品:IE,不开源。
    • Gecko(火狐):代表作Firefox,开源。
    • Blink/Chromium(Chrome、Opera):由Google和Opera Software开发的浏览器排版引擎,2013年4月发布。现在Chrome内核是Blink。谷歌还开发了自己的JS引擎,V8,使JS运行速度极大地提高了
    • Webkit(Safari)。代表作品是Safari、曾经的Chrome,是开源的项目。主要包括:WebCore排版引擎、JavaScriptCore解析引擎
  • 四大内核之外,还有Presto内核,代表作品是Opera,但是13年之后,Opera宣布加入谷歌阵营,弃用了 Presto内核。
3、浏览器内核主要组成
  • HTML解释器:解释HTML文本的解释器,主要作用是将HTML文本解释成DOM树,DOM是一种文档表示方法。
  • CSS解释器:级联样式表的解释器,它的作用是为DOM中的各个元素对象计算出样式信息,从而为计算最后网页的布局提供基础设施。
  • 布局:在DOM创建之后,webkit需要将其中的元素对象同样式信息结合起来,计算它们的大小位置等布局信息,形成一个能够表示这所有信息的内部比偶表示模型。
  • JavaScript引擎:使用JavaScript代码可以修改网页的内容,也能修改CSS的信息,JavaScript引擎能过解释JavaScript代码并通过DOM接口和CSSOM接口来修改网页内容和样式信息,从而改变渲染结果。
  • 绘图:使用图形库将布局计算后的各个网页的节点绘制成图像结果。
  • 以上这些模块依赖许多其他基础模块,其中包括网络、存储、2D/3D图形、音频视频和图片解码器等。

五、其他

1、LocalWebServer
  • 离线包方案中,除了拦截请求加载资源的方式,还有种在项目中搭建local web server,用以获得本地资源。市面有比较完善的框架
CocoaHttpServer (支持iOS、macOS及多种网络场景)
GCDWebServer (基于iOS,不支持 https 及 webSocket)
Telegraph (Swift实现,功能较上面两类更完善)
2、静态资源预加载技术
  • 基于Webview的link prefetching(链接预取)机制,利用webview的空闲时间,提前下载好用户在后续可能使用到的静态资源文件,当用户访问预取文档时,便可以快速从浏览器缓存中读取。
  • 此方案可显著提升网页启动速度(缩短页面加载时间30%-50%以上)、提高网页加载成功率。相比于传统的离线化技术方案,本方案具有差异性优势,且能规避iOS WKWebview中无法拦截请求的问题。具体可参考:App内网页启动加速实践:静态资源预加载视角
3、WKWebView loadRequest 问题
  • 在 WKWebView 上通过 loadRequest 发起的 post 请求 body 数据会丢失:
//同样是由于进程间通信性能问题,HTTPBody字段被丢弃
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];
[wkwebview loadRequest: request];

解决:假如想通过-[WKWebView loadRequest:]加载 post 请求 (原始请求)request1: h5.nanhua.com/order/list,可以通过以下步骤实现:

  • 替换请求 scheme,生成新的 post 请求 request2: post://h5.nanhua.com/order/list, 同时将 request1 的 body 字段复制到 request2 的 header 中(WebKit 不会丢弃 header 字段);
  • 通过-[WKWebView loadRequest:] 加载新的 post 请求 request2;
  • 并且通过 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注册 scheme: post://;
  • 注册 NSURLProtocol 拦截请求 post://h5.nanhua.com/order/list ,替换请求 scheme, 生成新的请求 request3: h5.nanhua.com/order/list,将 request2 header的body 字段复制到 request3 的 body 中,并使用 NSURLSession 加载 request3,最后将加载结果返回 WKWebView;
4、推荐资料