1. 背景
H5页面在APP中通常寄宿在WebView中,页面表现十分依赖网络环境。通过离线包方案旨在以较低的兼容成本弱化网络对页面表现的影响,同时保留H5自身的优点。
离线包通过将页面资源,包括HTML,JS,CSS打包为压缩包,APP预先下载到本地,WebView直接从本地加载静态资源,减少网络环境对页面加载的影响。离线包重在解决建立链接 -> 接受页面/样式/脚本的白屏过程。
前端工程可以通过离线包插件将页面资源打成压缩包,上传到离线包配置平台,通过配置平台管理关联应用,进行部署,发布,查看等操作。
LOFTER APP中有很多H5应用,比如市集,达人认证,客服消息等等,我们希望通过离线包方案将页面更快的展示给用户,提升用户体验,这些应用已经整体比较稳定,需要比较合适的方式来接入离线包的能力,我们的方案也以此为目的进行设计和实现。
2. 离线包整体方案
整体方案设计如下:
前端应用(Web)保持原有的开发部署流程,仅通过webpack plugin修改打包流程,建立与配置平台的联系,并生成压缩文件,上传到NOS。这就让现有的H5应用方便的具备离线包的能力,后续新开发的离线应用也可以同时具备静态发布的能力。
离线包配置平台(Configuration Platform)用于管理各个离线包应用,包括对应用的增删改查,版本管理,配置查看,设置检查更新的url等功能,由webpack plugin生成的配置会通过接口入库。每次版本更新都会存储下来,通过上线操作生效。
APP后端(APP Server)主要提供APP侧离线包配置查询的接口,也包含ios和android离线包开启与否的开关,在配置平台设置生效的离线包应用会以列表的形式提供给APP使用。
客户端(APP)通过接口获取离线包清单,如果某个应用版本需要更新,先将本地包资源删除,再进行更新。通过整体考量,目前我们没有做增量更新和差量包的维护,为了减少应用体积过大带来的更新问题,我们提供分包的配置,可以分步实现离线化。在开启离线包功能的WebView中,对所有资源请求进行反向代理,如果资源在本地有缓存,走本地缓存,没有则请求在线资源。客户端检查配置更新的时机主要是:1. 启动时拉取全量离线包应用配置;2. 命中平台配置的检查更新的url时。
2.1 离线包应用配置清单
清单示例如下:
[
{
"appId": "离线包应用appId",
"name": "离线包应用名",
"version": "离线包应用版本",
"updateTime":"离线包应用配置项更新时间,可用于判断是否需要覆盖配置项",
"refreshUrls":["www.lofter.com/mp/lofter878789/html/page1.html"],
"packages": [
{
"packageName":"package_1",
"loadingQuietly":true,
"dependencies":["common trunk包"],
"urls": [
"//www.lofter.com/market/page1.html",
"//www.lofter/market/page2.html"
],
"zipUrl": "https://xxx.nosdn.127.net/offline/xxxx.zip",
"md5":"xxxxxxxxxxxxxxxx"
},{
"packageName":" package_2",
"dependencies":["common trunk包"],
"urls": [
"//www.lofter.com/market/page3.html",
"//www.lofter/market/page4.html"
],
"zipUrl": "https://xxx.nosdn.127.net/offline/xxxx.zip",
"md5":"xxxxxxxxxxxxxxxx"
},{
"packageName":"common trunk包",
"loadingQuietly":true,
"zipUrl": "https://xxx.nosdn.127.net/offline/xxxx.zip",
"md5":"xxxxxxxxxxxxxxxx"
}
]
}
]
主要配置项说明:
- refreshUrls 定义命中则需要进行版本更新检查的url。
- urls 定义了需要下载离线包的链接,非静默加载时用以触发离线包下载。
- loadingQuietly 定义当前包是否需要静默加载,APP获取配置后优先加载需要静默加载的包。
- dependencies 定义分包时各包之间的依赖关系,客户端需要优先下载被依赖的包。
- zipUrl 是资源文件与映射文件集合的压缩包,也会生成对应的MD5供客户端校验。
2.2 离线包插件
离线包插件工作流如下:
在webpack生成输出文件之前,根据资源信息生成映射文件manifest.json,上传资源包到NOS供APP下载,提交离线包应用相关的信息program.json到离线包配置平台。分包也是通过配置的形式提供,未配置的资源会自动分为一个包。这里还提供了额外静态资源的配置,如果页面需要以高优先级引入第三方资源时,可以保存一份本地副本,并将访问链接与资源关联起来供插件读取,这样可以避免这类资源请求耗时对页面加载造成影响。
使用方式为:
const OfflinePackage = require('lofter-mp-webpack-plugin');
const offlineEntry = getEntry();
module.exports = {
...
plugins: [
new HtmlWebpackPlugin(),
new OfflinePackage({
packageNameValue: 'test',
appId: 'lofter1',
uploadConfigUrl: 'http://www.lofter.com/mp/api/appVersion/add',
baseUrl: `//www.lofter.com/mp/lofter1/`,
cdnUrl: `//xx.cdn.com/mp/lofter1/`,
nosConfig: nosConfig,
ignoreFileTypes: ['txt', 'js.map'],
excludeFileName: ['page2'],
entry: offlineEntry,
extraManifest: {
"static/jquery.min.js": "//cdn.bootcss.com/jquery/2.2.4/jquery.min.js",
},
dividePackageConfig: [{
loadingQuietly: true,
entryFile: ['index'],
},{
loadingQuietly: false,
entryFile: ['page1'],
}]
})
]
}
需要配置在HtmlWebpackPlugin之后,便于直接取到处理过后的资源。
2.3 客户端实现
Android形成了离线包SDK,WebView注册SDK来具备拦截资源的能力,通过对shouldInterceptRequest 方法的复写来拦截WebView资源地址,与已存在的离线包需要拦截的url作匹配,如果未匹配上则不做拦截,直接访问远端资源,否则代理WebView的资源获取方式。从远端CDN下载包含静态资源的压缩包后,解压并不是必须的,可以直接读取包内的文件。
IOS目前使用NSURLProtocol 拦截WKWebView网络请求,但是会导致POST请求body丢失的问题,通过注入hook js代码,对fetch和XMLHttpRequest做一些处理,在实际联调中,我们还对Headers做了补全,以及实现了一套简单的同源策略。这里我们还在探索更好的实现方案,否则随着业务复杂度的提高,难免会在客户端实现了一个复杂的定制浏览器。
在用App验证离线包功能的时候,仅靠抓包是无法有效判断资源加载情况的,这里Android同学提供了日志筛选工具,连接手机后可以在电脑上查看到离线包拦截的日志,iOS的调试浮层也加入了离线包资源匹配的日志。此外我们约定了收集web日志的JSBridge,由web通过主动调用的方式,添加到app的日志文件中,以便进行问题排查。
3. 离线包效果统计
这里我们对乐乎市集应用进行效果分析,乐乎市集是站内用户量比较大的多页H5应用,在离线包上线前就已经接入了云音乐的wapm平台进行性能监控,配合离线包功能上线可以观察到页面性能指标的变化。客户端WebView容器在6.11.1版本上线了离线包功能,我们就近选取了6.10.0到6.10.3的所有版本作为对照。先从平均DNS耗时、平均TCP耗时、平均DomReady时间、平均加载时间这四个指标进行统计。
这是指标的计算方式,由wapm的sdk进行收集上报。
| 指标 | 计算方式 |
|---|---|
| DNS耗时 | domainLookupEnd - domainLookupStart |
| TCP耗时(包含SSL耗时) | connectEnd - connectStart |
| DOM Ready | domContentLoadedEventEnd - fetchStart |
| 页面完全加载时间 | loadEventStart - fetchStart |
统计结果整理如下:
图形化处理后得到百分比堆积统计图:
可以看到,DNS耗时和TCP耗时在两端都有明显降低。Dom Ready和加载时间也有所降低,Android整体达到了30%的平均降幅。需要说明的是,首页HTML是部分SSR的,目前走的是在线资源,所以即使数据降低,比起其他页面前两项指标并没有趋近于0。
对于通过异步接口获取主要数据的大部分页面,其实使用WebVitals 指标更能展示用户侧真实体验(WebVitals介绍传送门)。通过对数据收集分析,离线包能有效降低FCP(首次内容渲染)、LCP(最大内容渲染)的耗时,对CLS(累计布局位移)、FID(首次输入时延)的影响不大。由于目前主要是基于 Chromium 内核的浏览器的浏览器才能采集到到WebVitals 指标,这里只对Android端的数据进行展示。
图形化处理后得到条形统计图:
可以看到,离线包对FCP和LCP有明显优化。表格中可以看到离线包在P95的统计中绝对值的优化大部分要高于P75的数据,说明可以降低整体数据的波动,对一些加载较慢用户的体验优化也会更加有效。
4. 离线包加载分析
根据统计结果,WebView从本地加载静态资源可以提升多项性能指标,将主要静态资源切换到从本地加载,可以大幅降低DNS和TCP耗时,这是符合预期的。DomReady和页面加载完成时间的提升效果比较出乎我们的意料,这里通过调试模式来分析离线包应用加载时页面的执行情况,对比非离线包模式,探寻一下时间是怎么节省出来的。
因为安卓端数据比较稳定,这次先着重分析安卓WebView的离线应用,并选取了代表性比较强的首页和商详页作为示例。
设备为一台华为荣耀9,Android 9,运行内存6GB,处理器为Hisilicon Kirin 960,网络环境:联通4G,连接手机使用performance模块进行加载分析。
4.1 市集首页分析
市集首页的首屏数据是预填到HTML中的,没有存储为离线资源,目前对主要JS,CSS资源进行了离线存储。
这是页面的整体加载时间线:
可以看到,从本地加载的资源,只有等待时间和文件读取解析的时间。
homepage.js加载好后,浏览器执行了一个构件页面的长任务,执行完后DomReady。离线包场景下,这个长任务的耗时约为371.7ms。在homepage.js加载时间通过离线包降低了的情况下,DCL的时间点会被提前。
假设用户侧网络环境一般,以Fast 3G网络环境来模拟:
优先级较高的外部JS阻塞了渲染进程,尽管homepage.js已加载好,但是渲染任务迟迟未开始,这时这个长任务的耗时为387.9ms左右,如果后续将优先级较高的JS,css也加入离线包,那么预期用户侧平均DCL的时间点会更加提前。
对比非离线包情况下,页面的加载情况:
Fast 3G模拟环境下:
对比多种情形,在构建dom的长任务时间差不多的情况下,几个高优先级的资源请求耗时直接决定了DCL的时间点。
对首页来说离线包对页面加载的时间优化,主要是homepage.js自身加载减少的时间或与高优先级的外部资源(index.js或index.css)加载的时间差,即图中红框圈住的部分,约为几百毫秒,这与统计数据是吻合的。
4.2 市集商详页分析
挑选了畅销榜的商详页作为测试页。
因为有骨架屏,可以看到FP和FCP的时间点都很靠前。
模拟Fast 3G的网络环境:
各离线资源在不同网络环境下的加载时间相近。类似首页的加载效果,DCL的时间点因为优先级高的资源阻塞页面渲染的原因被明显延后了。
从图中可以看到,DCL和Onload的时间点很多时候是在主要接口返回前就形成了,此时页面信息展示不完全,LCP指标更能反映页面的真实渲染情况,统计数据也展示了离线包对LCP指标的优化。
5. 总结
离线包场景下,应该将高优先级的外部资源全部作为离线资源加载,避免阻塞主渲染进程。离线包方案能有效降低首次访问页面时的DNS和TCP耗时。在用户侧网络环境极其复杂的情况下,提高主要资源加载的稳定性,让容器能更早执行渲染任务。
本文发布自网易元气事业部前端团队,文章未经授权禁止任何形式的转载。欢迎与我们交流前端相关的技术问题和经验,同时,团队以及部门正在招聘前端、服务端以及客户端各岗位的开发人员,以上都可以联系LofterFrontendTeam@corp.netease.com进行交流。