阅读 100

LOFTER App离线包方案及相关性能分析

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 ReadydomContentLoadedEventEnd - fetchStart
页面完全加载时间loadEventStart - fetchStart

统计结果整理如下:

市集主要页面使用离线包的性能指标统计数据,包含统计的版本,样本数量,结果比对等

图形化处理后得到百分比堆积统计图:

市集主要页面的性能指标百分比堆积统计图

可以看到,DNS耗时和TCP耗时在两端都有明显降低。Dom Ready和加载时间也有所降低,Android整体达到了30%的平均降幅。需要说明的是,首页HTML是部分SSR的,目前走的是在线资源,所以即使数据降低,比起其他页面前两项指标并没有趋近于0。

对于通过异步接口获取主要数据的大部分页面,其实使用WebVitals 指标更能展示用户侧真实体验(WebVitals介绍传送门)。通过对数据收集分析,离线包能有效降低FCP(首次内容渲染)、LCP(最大内容渲染)的耗时,对CLS(累计布局位移)、FID(首次输入时延)的影响不大。由于目前主要是基于 Chromium 内核的浏览器的浏览器才能采集到到WebVitals 指标,这里只对Android端的数据进行展示。

Android端使用离线包的WebVitals指标统计数据,按照数据区间进行统计和结果比对

图形化处理后得到条形统计图:

Android端市集主要页面使用离线包的WebVitals指标条形统计图

可以看到,离线包对FCP和LCP有明显优化。表格中可以看到离线包在P95的统计中绝对值的优化大部分要高于P75的数据,说明可以降低整体数据的波动,对一些加载较慢用户的体验优化也会更加有效。

4. 离线包加载分析

根据统计结果,WebView从本地加载静态资源可以提升多项性能指标,将主要静态资源切换到从本地加载,可以大幅降低DNS和TCP耗时,这是符合预期的。DomReady和页面加载完成时间的提升效果比较出乎我们的意料,这里通过调试模式来分析离线包应用加载时页面的执行情况,对比非离线包模式,探寻一下时间是怎么节省出来的。

因为安卓端数据比较稳定,这次先着重分析安卓WebView的离线应用,并选取了代表性比较强的首页和商详页作为示例。

设备为一台华为荣耀9,Android 9,运行内存6GB,处理器为Hisilicon Kirin 960,网络环境:联通4G,连接手机使用performance模块进行加载分析。

4.1 市集首页分析

市集首页的首屏数据是预填到HTML中的,没有存储为离线资源,目前对主要JS,CSS资源进行了离线存储。

这是页面的整体加载时间线:

市集首页使用离线包加载过程的Performance时间线

市集首页使用离线包加载时各资源加载情况,离线资源不会产生网络传输耗时

市集首页主JS耗时分布

可以看到,从本地加载的资源,只有等待时间和文件读取解析的时间。

红线之间的部分为资源加载完成,执行页面渲染的阶段

homepage.js加载好后,浏览器执行了一个构件页面的长任务,执行完后DomReady。离线包场景下,这个长任务的耗时约为371.7ms。在homepage.js加载时间通过离线包降低了的情况下,DCL的时间点会被提前。

假设用户侧网络环境一般,以Fast 3G网络环境来模拟:

渲染任务由于高优先级资源阻塞,开始时间推后,但是耗时与正常网络环境相近

优先级较高的外部JS阻塞了渲染进程,尽管homepage.js已加载好,但是渲染任务迟迟未开始,这时这个长任务的耗时为387.9ms左右,如果后续将优先级较高的JS,css也加入离线包,那么预期用户侧平均DCL的时间点会更加提前。

对比非离线包情况下,页面的加载情况:

不启用离线包时,体积最大的资源的加载耗时直接影响页面渲染时机

Fast 3G模拟环境下:

弱网场景下,离线包资源快速加载,可以提前DCL的时间点

不同网络下资源加载分析示意图

对比多种情形,在构建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进行交流。

文章分类
前端
文章标签