离线包方案参考思考过程-总结了几篇文章

时间:2024-01-30 22:20:40

总结了下几篇文章

网易(资源离线/JsBridge通信/接口预请求)

一. 资源离线

  • 静态资源加载耗时, 资源离线到本地, 能很好解决.

  • web页面把静态资源生成zip包, 客户端在合适的时机拉去zip包并解压到本地, 持久化存储.

  • 用户访问的时候拦截WebView发出去的页面请求, 直接返回对应的本地文件.

  • 前端:

    • 生成zip包 -> 更新离线数据
  • APP:

    • 下载zip包 -> 拦截页面请求 -> 返回本地资源
  • 三个关键部分:

    • Web页面Zip包生成工具
    • 离线管理系统
    • 客户端离线实现
  • web打包工具

{
  [   
    "name": "index", // 页面名称
    "url": ["https://example.com/index"] //页面线上地址 
    "zipUrl": "https://assets.example.com/static/example.20190525_1020.zip", // zip地址
    "md5": "md5md5md5md5md5md5md5md5md5md5md5md5" // md5
  ]
}
  • 工具自动化分成通过中间件实现

  • 通用部分:

    • 拷贝页面依赖, 生成zip包
    • 判断包的完整性
    • 获取zip包的md5值
    • 生成zip包版本号
  • 定制部分:

    • 确定待更新zip包
    • 上传zip包到cdn
    • 更新离线数据, zip包版本数据
  • 通用部分:

    • 获取打包配置 -> 拷贝/打包 -> 检测包完整性 -> 获取MD5
  • 定制部分: (可以在打包工具做, 也可以手动上传)

    • 确定待更新包 -> 上传zip到cdn -> 更新数据
  • 离线管理系统:

    • 为离线工具提供打包信息及离线包信息存储
    • 为App提供离线数据
    • 页面离线数据在线管理
  • 应该完成多产品, 多用户设计.

  • 工具自动更新数据, 还可以在系统里添加数据, 对数据进行增删改查.

  • 离线数据保留最近5个版本, 发现线上zip包有问题, 可以迅速回归.

  • 核心功能:

    • 多产品
    • 多用户
    • 在线操作
    • 提供接口
  • 客户端实现 (最重要)

    • 离线资源更新
    • 拦截资源返回
  • 离线资源管理器总调度处理资源更新和拦截返回.

  • 根据配置离线配置细腻创建动态管理器, 部署每个url对应的页面入口文件, 静态资源目录等.

  • 更新app配置氛围主动和被动.

    • 主动通过app启动后通过接口获取离线配置信息.
    • 被动通过push更新.
  • 获取离线配置后, 读取本地配置缓存进行对比.

  • 根据页面名称确定离线文件的更新策略是什么.

    • 远端配置无, 本地配置有, 认为当前页面离线包被删除. 直接删除本地对应的离线页面入口文件.
    • 发现两个配置中同名页面zip包的md5不一致, 认为应该更新了.
    • 如果发现远端有, 本地无, 则是新增, 然后交给下载管理器下载. 下载解压完成后, 通知管理器更新本地配置.
  • 流程:

    • 获取离线配置 -> 匹配资源 -> 确定更新策略 -> 更新资源和本地配置.
  • 拦截返回细节.

    • 统一拦截所有网络请求, 通过管理器处理访问逻辑.
  • 需要拦截返回:

    • html
    • js, css, img
  • app在WebView发起请求时, 会拦截当前页面请求, 获取页面的URL地址, 根据管理器中的配置, 进行查找.

    • 找到直接返回入口文件
    • 未找到请求线上地址
  • 页面的加载会伴随着依赖资源的加载, 获取请求url, 如果在拦截域名内, 则替换域名为本地的静态资源目录进行查找.

  • 找到后, 获取文件扩展名, 设置返回的文件类型直接返回.

  • 拦截并返回本地资源

    • & 返回本地资源
    • & 获取离线配置 -> 匹配请求地址 -> -> 渲染Web页面
    • & 请求线上资源
  • app针对每个环节出现的错误进行上报:

  • 离线相关的错误类型有:

    • 获取离线配置接口网络错误
    • 获取离线配置接口数据解析失败
    • zip包请求网络
    • zip包解压错误
    • zip包md5值app端与前端不一致
    • zip包解压手机空间不足
  • 任何一种错误都不会更新本地离线资源和离线配置.

一. JSBridge

  • 大部分业务需要的native功能

    • 视图层面: 注册, 登录, 认证, 注销组件, 视图路由
    • 存储层面: 用户信息, 设备信息, 业务状态, 缓存
    • 网络层面: 请求header, 代理转发, 预请求
    • app层面: 唤起, 设置, push, 跨app操作
    • 系统层面: 底层api的调用
  • 其他辅助功能.

  • ...其他的不属于离线包, 暂不处理...

一. 实际应用

  • 请求代理
    • 预请求
    • 统一业务header
    • 统一日志管理
    • 跨域
  • WebView预创建

二.移动端本地 H5 秒开方案探索与实现》

二.为什么体验糟糕

  • 过程:

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

  • 过程图片

  • 一般页面在dom渲染后才能展示, 可以发现, H5首屏渲染白屏问题关键: 如果减少从请求下载页面到渲染之间这段时间的耗时.

二.如何优化

  • 优化常用方式:

    • 降低请求量: 合并资源, 减少HTTP请求数, minify/gzip压缩, webP, lazyLoad
    • 加快请求速度: 预解析DNS, 减少域名数, 并行加载, CDN分发
    • 缓存: HTTP协议缓存请求, 离线缓存mainifest, 离线数据缓存localStorage.
    • 渲染: js/css优化, 加载顺序, 服务端渲染模板直出.
  • 直接打包H5相关页面到客户端中, 然后客户端将数据传递给页面, 通过webView加载展示. 不需要网络请求, webView只要渲染页面, 执行js即可.

二.实现

  • H5和native通信

    • jsapi: 客户端提供接口, 注入api让js调用, 直接执行相应的native代码, 适用于需要通过交互, 进行数据请求的场景
    • url scheme: web端发送url scheme请求, 之后native拦截请求并根据url scheme以及所带参数进行相关操作. 使用页面跳转.
    • 字符串替换: 客户端读取本地H5后, 通过对h5中约定的标记位进行字符串替换. 然后加载展示页面. 适用于没有复杂交互, 只通过页面渲染数据的场景.
  • 开发本地H5模块, 本地模拟数据开发, 然后H5给各客户端打包后联调. 繁琐, 因为给客户端打包时比较分散, 不统一, 管理困难.

  • 本地h5实现模块的页面建议一个统一git仓库, IOS和android客户端通过git submoduleGit Submodule使用完整教程将本地h5的git外链到项目中, 客户端中的资源就可以统一管理了, 解放了每次都手动繁琐替换打包工作.

  • H5资源给到后台, 客户端按照业务模块预下载整个离线包, 离线包根据版本做增量更新.

  • 离线更新事宜图

二.细节

  • 预加载webView, 预拉取数据
  • 屏蔽webView HTML内容自动识别
  • 点击延迟
  • 国际化
  • WKWebView兼容
    • WKWebView性能相对UIWebView较好. 推荐使用
    • WKWebView加载本地的HTML时, 会有兼容问题, 在IOS8不能在HTML文件中引用本地的css或者js或者图片文件.
    • ios8以上是正常的, 可以引用远程资源.
    • ios8使用网络资源, ios8以上使用本地资源.
    • ios8中, 使用远程cdn的css或者js文件, 引用标签必须添加charset属性. 不然css和js乱码.

三.蚂蚁离线包简介

  • 离线包简介

  • 传统的H5技术容易受到网络环境影响, 因而降低H5页面性能. 离线包可以解决该问题, 同时保留H5的优点.

  • 离线包是将包括HTML, JavaScript, css等页面的内置静态资源打包到一个压缩包内. 预先下载该离线包到本地, 然后通过客户端打开, 直接从本地加载离线包.

  • 优势:

    • 提升用户体验: 通过离线包的方式把页面内置静态资源嵌入到应用中并发布, 当用户第一次开启应用的时候, 就无需以来网络环境下载该资源. 而是马上开始使用.
    • 实现动态更新: 在推出新版本或是紧急发布的时候, 可以把修改的资源放入离线包, 通过更新配置让应用自动下载更新. 无需通过应用商店审核, 就让用户及时接收更新.

三.离线包结构

  • 离线包是一个.amr格式的压缩文件, 后缀.amr改成.zip解压后, 可以看到其中包含了HTML资源和JavaScript代码等. 待H5容器加载完成后, 这些资源和代码能在WebView内渲染了.
    • 一级目录: 一般资源包的ID, 例如20150901
    • 二级目录及子目录: 业务自定义的资源文件. 建议所有前端文件保存在一个统一目录下.
* 20150901
  * hmpfile.json
  * sdk
  * www
    * index.html
    * js
    * test.html
* 20150901.tar
* CERT.json
* Manifest.xml

三.离线包类型

  • 基础通用库使用全局离线包.
  • 类型:
    • 全局离线包: 包含公共资源, 可供多个应用共同使用
    • 私有离线包: 只可以被某个应用单独使用.
  • 使用全局离线包后, 在访问H5的时候, 都会尝试在这个包读取. 如果该离线包里有对应资源的时候, 直接从该离线包里读取, 而不通过网络. 因此全局离线包的机制主要是为了解决对于通用库的使用.
  • 由于要保证离线包的客户端覆盖率以及足够的通用性. 此包一般更新周期至少为一个月, 并严格控制离线包大小.

三.渲染过程

  • H5容器发出资源请求时, H5容器或截获该请求:

    • 如果本地有资源可以满足该请求, H5容器会使用本地资源.
    • 如果没有可以满足请求的本地资源, H5容器会使用线上资源.
  • 无论资源使用线上或者是本地的, WebView都是无感知的.

  • 离线包的下载取决于创建离线包时的配置

    • 如果"下载时机"配置为仅WiFi, 只有wifi网络时才会下载.
    • 如果配置为"所有网络都下载", 会消耗用户流量自动下载, 慎用.
  • 如果当前用户点击app时, 离线包尚未下载完成, 则会跳转到fallback地址, 显示在线页面.

  • fallback技术用于应对离线包未下载完毕的长江. 每个离线包发布时, 都会在CDN发布一个对应的线上版本. 目录结构和离线包结构一致.

  • fallback地址会随离线包信息下发到本地. 在离线包未下载完毕的场景下, 客户端会拦截页面请求, 转向CDN地址. 实现在线页面和离线页面随时切换.

  • 渲染过程

三.离线包运行模式

  1. 请求包信息: 从服务端请求离线包信息, 存储到本地数据库过程. 离线包信息包括离线包的下载地址, 离线包版本号等.
  2. 下载离线包: 把离线包从服务端下载到手机.
  3. 安装离线包: 下载目录, 拷贝到手机安装目录.

三.虚拟域名

  • 虚拟域名仅对离线应用有效, 当页面保存在客户端之后, WebView通过file schema从本地加载访问的. 然而用户就能在地址栏里直接看到file的路径:
    • 用户体验问题: 当用户看到file地址, 会对暴露的地址产生不安全感和不自在.
    • 安全性问题: 由于file协议直接带上本地路径, 任何用户都可以看到这个文件所有的路径, 会存在一定的安全隐患.
  • 所以采用虚拟域名的机制. 而不直接使用file路径访问. 虚拟路径是一个符合URL Scheme规范的HTTPS域名地址, 例如: https://xxxxxx.h5app.example.com, 虚拟域名的父域名example.com一定得使用自己注册的域名
  • 这个域名可以是网上注册的, 但是一般情况下, 不建议将虚拟域名配置成互联网一致的域名, 这个在判断问题的时候, 容易增加判断难度, 容易出错不便于日常管理. 只要保证其父域名example.com域名是自己注册的域名即可.
  • 标准的虚拟域名如下: https://{appid}.h5app.example.com

四.蚂蚁生成离线包

  • 参考: 生成离线包
  • 根据不同需求, 将不同业务封装成为一个离线包, 通过发布平台发布对客户端资源进行更新.
  • 生成步骤:
    • 构建前端.zip包
    • 在线成才.amr包

四.构建前端.zip包

  • 根据场景不同: 配置路径分为:
    • 全局资源包
    • 普通资源包
  • 同一个H5离线包中, 全局资源包于普通资源包不可共存.
  • 离线包ID(下文中的一级目录), 必须为8位数字.

四.全局资源包

  • 可以将被其他多个资源包引用的通用资源放置在全局资源包内, 并按照下列规则指定包内的资源路径

    • 一级目录: 全局资源包的ID: 如77777777
    • 二级目录: 指向资源可访问的服务器域名地址.
      • 公有云: 固定为mcube-prod.oss-cn-hangzhou.aliyuncs.com(离线包管理->新增离线包页面的资源包类型中可看到相关提示信息)
      • 专有云: 请查询专有云部署的mdsweb服务器域名地址
    • 三级目录: appid_workspaceId, 例如: 53E5279071442_test
      • 三级目录往后即为业务自定义的公共资源文件. 在公共资源文件的文件夹名, 文件名以及文件中, 避免使用特殊字符. 特殊字符会被urlencode函数转换字符
  • 以上规则组织资源文件后, 即可按照如下格式快速获取资源文件的路径:

    • 公有云: http://域名/appid_workspace/资源文件路径
    • 专有云: http://域名/mcube/appId_workspace/资源文件路径
  • 示例:

    • 在公有云中: 二级目录固定为: mcube-prod.oss-cn-hangzhou-aliyuncs.com, 所以下图中资源文件common.js路径为: https://mcube-prod.oss-cn-hangzhou.aliyuncs.com/53E279071442_test/common.js
    • 示例地址
    • 在专有云环境中, 二级目录为专有云部署的mdsweb服务器域名地址, 此外mdsweb-outer.alipay.net为例. 下图资源文件common.js的路径为https://mdsweb-outer.alipay.net/mcube/53E5279071442_test/common.js
    • 示例地址
  • 注意实现:

    • 公共资源的绝对路径长度不超过100字符, 否则会导致客户端加载资源失败以及页面白屏
    • 服务端未控制全局资源包版本, 用户可根据实际需求, 通过在三级目录以后添加文件目录结构的方式, 来自定义控制文件的高低版本.
    • 在专有云环境中, 如果服务端采用的文件存储格式为HDFS或AFS, 则需要在上述第三级目录前增加一个目录, 该目录名称为mdsweb目录, 该目录名称为mdsweb服务器中的存储空间(bucket)的名称.
    • 引用公关资源: 在普通离线包内访问全局资源包中的内容, 必须通过绝对路径访问, 如: https://mcube-prod.oss-cn-hangzhou.aliyuncs.com/53E5279071442_test/common.js
    • 示例

四.普通资源包

  • 按照业务将相关的HTML, CSS, JavaScript, 图片等前端资源放置在同一个离线包内, 目录结构如下:

  • 一级目录 普通资源包的ID, 如20171228.

  • 二级目录及往后即为业务自定义的资源文件. 建议在所有的前端文件最好保存一个统一的目录下, 如/www, 并设定当前离线包默认打开的主入口文件, 如: /www/index.html

  • 示例.

  • 配置万资源包的路径后, 即可直接将appid所在的目录整体压缩为一个.zip包.

四.在线生成.amr包

  • 进入控制台的实时发布->离线包管理页面, .zip包上传到MDS发布平台, 生成.amr包.
  • 示例

五.H5秒开方案大全

五.常用的加速方法

  • 资源加载:

    • 针对首屏
    • 更小的资源包
    • 压缩, 减包, 拆包, 动态加载包, 图片优化
  • html渲染:

    • 针对可优化
    • 更快的展示内容
    • cdn分发, dns解析, http缓存, 数据预请求, 直出
  • rn, weex, flutter冲击传统hybrid, hybrid加速发展.

五.直出+离线包缓存

  • 直出: 后端渲染, 省去了ajax请求时间, 能够通过各种缓存策略优化很好, 加载html扔需要时间.
  • 离线包技术: 解决html本身加载需要的时间问题.
  • 离线包基本思路通过通过webview统一拦截url, 将资源映射到本地离线包, 更新的时候对版本资源检测, 下载和维护本地缓存目录中的资源.
  • 渲染loading过程
  • 对于web端而言: 相对透明, 侵入性比较小.

五.客户端代理的VasSonic 腾讯手 Q VasSonic 秒开

  • 用户点击到看到页面之间, 存在webview初始化, 请求资源的时间, 这里的过程是串行的, 所有存在优化空间.

  • 支持离线包策略, 并更进一步

    • webview初始化和通过客户端代理资源请求并行
    • 流式拦截请求, 边加载边渲染
    • 实现了动态缓存和增量更新
  • 客户端代理请求并行:

    • 创建webview之前, 通过客户端代理建立网络链接, 请求html, 然后缓存起来.
    • 等待webview线程发起请求html资源的时候,客户端拦截, 将缓存的html返回给webview
  • 动态缓存和增量更新:

    • 自定义了一套标签. 将html区分为模板和动态数据两部分.
    • 拓展了http头部, 定制了一套请求后台的约定
    • webview发情请求的时, 会将页面内容的id携带过去, 后台判断后, 再告诉客户端是否更新局部数据
    • 如果是缓存额html模板和新数据拼接成新的html, 最后计算差异部分, 通过js回调给页面, 进行局部刷新
  • 示例

  • 通过模板可以达到局部变化, h5秒开结果.

  • 但定义了一套特殊的注释标记及拓展了头部, 需要包括后台在内的前后端进行改造. 对web入侵性非常强. 维护成本高.

五.PWA+直出+预加载

  • 不管是离线包技术, 还是webview代理请求, 都对前端入侵比较大.
  • pwa能够通过纯web的方案优化加载性能.
  • 对于直出html, 配合pwa, 直出文件, 缓存到cacheStorage, 在下一次请求时, 优先从本地缓存中读取, 同时发起网络请求更新本地html文件.
  • 第一次加载通过app预加载一个js脚本, 拉去需要pwa缓存的页面, 可以提前完成缓存.
  • 非直出页面.
    • 第一次只能提前加载. 预加载脚本.
    • 第二次非直出页面, 每个页面需要有独一无二的标记, 比如hash. 浏览器获取到数据, 渲染好的html, 通过outerHtml方法, 将html缓存到cacheStorage中.
    • 第二次优先从本地获取, 同时发起html请求, 通过对比其中唯一标识的差距, 决定是否需要更新.
  • 示例
  • pwa一系列方案替代离线包, 属于web标准, 适用于普通能够支持service-worker的H5页面.
  • 在兼容问题允许的情况下, 建议主加.

五.NSR渲染

  • 前端SSR
  • 借住浏览器启用一个JS-Runtime, 提前将下载好的html模板及预取的feed流数据进行渲染. 然后将html设置到内存级别的MemoryCache中, 从而点开即看.
  • NSR将SSR渲染过程分发到各个用户的端. 减少后台请求压力, 进一步提高页面打开速度.
  • 数据预取和预渲染带来的额外流量和性能开销, 特别是流量. 如何更准确的预测用户行为, 提高命中率非常重要.

五.客户端PWA

  • service-worker在webview实现性能并没有想象中好.

五.小程序化

  • 小程序内部将webview渲染和js执行分离开, 然后通过离线包, 页面拆分, 预加载页面等手段
  • 牺牲了web的灵活性.
  • 对于hybrid开发, 通过原生客户端底层支持小程序环境, 大量业务逻辑采用小程序方案开发
  • 迭代速度和性能兼容, 是一个不错方向

五.总结

  • 在整个链路中减少中间环境, 例如串行改并行, 包括小程序内部执行机制.
  • 尽可能预加载, 预执行. 比如从数据拉去, 到页面渲染.
  • 任何转换都有代价, 加速本质上就是在用更多的网络, 内存和CPU换取速度. 以时间换空间

六.转转hybrid app web静态资源离线系统

六.前言

  • 优点:

    • web页面上线满足快速迭代的业务需求, 不收客户端审核和发版的时间限制.
    • 也可以将各个业务线的开发工作分摊到各个业务的fe团队上, 是的业务线并行开发.
  • 缺点:

    • web应用的性能和体验, 不及客户端.
  • 痛点1:

  • 打包后静态资源过大, 首次打开/线上H5资源更新/网络条件差/本地页面缓存失效.

  • 白屏.

  • 痛点2:

  • app使用系统原生的web view, 不兼容pwa.

  • 各个业务团队使用的技术栈比较广. 各个业务线快节奏开发, 需要低成本接入. 对业务代码不会产生入侵.

六.方案

  • 流程图

  • 前端构建发布:

    • ak-webpack-plugin: 根据配置, 将webpack的构建出的静态资源, 压缩成了静态资源在cdn路径url的zip包, 同时在配置的过程中, 可以选择排除掉部分文件.(例如, 部分图片)
    • 不需要关注资源之间的依赖关系, 更不需要关注具体的业务逻辑.
    • 只需要关注webpack构建后生成的资源文件夹的结构.
    • 使用jenkins.持续集成和发布.
    • 流程图
  • app:

    • 预置一份最新的各个业务线的离线包与版本号的配置表.
    • app启动时, 会将压缩包解压到手机rom中, 各业务线配置中包含app访问线上的静态资源时需拦截的url规则map:
    • 当app访问到与规则map相匹配的地址时, 就转为本地资源, 达到离线访问目的.
[{
  "bizid": 13,
  "date": "1513681326579",
  "ver": "20171219185710",
  "offlinePath": [
    "c.58cdn.com.cn/youpin/activities"
  ]
}]
  • 流程图

  • 离线资源如何更新

    • 客户端启动后, 向离线系统查询最新的各个业务的离线包版本号, 依次跟本地配置中的对应业务线比较.
    • 如果需要更新, 则再次向离线系统查询此业务线的离线包信息. 离线系统会提供此业务线的离线包的信息.

流程图

  • 判断是否需要更新:
    • 线上的各业务线的离线包版本号与本地配置中 同一业务线的配置不同(不论最新的离线包版本比本地更高或者更低)
    • 线上的各业务线的配置中包含本地配置没有配置的业务线.

六.离线包加载优化

  • 增量的资源更新 (bsdiff/bspatch)
    • 影响bsdiff生成的差分包的体积因素主要有:
      • zip包的压缩等级
      • zip包中文件内容的修改. 比如js进行了uglify压缩, 变量名的变化可能引起大幅的变更.
    • 可以减少客户端升级离线资源需要下载的流量
  • 单独控制各个业务线web应用是否使用离线机制
    • 每个业务都加入使用离线资源的开关和灰度放量的控制.
  • 数据一致性校验 与 数据安全性校验.
    • 防止下载离线资源在传输中被篡改, 使用md5验证.
    • 同时保证传输过程中, 资源文件不被篡改, 将md5值通过rsa加密算法进行加密, 在服务端和客户端分别使用一对非对称的密钥进行加解密.
  • 批量下载:
    • 启动app后, app会集中批量下载各个业务线的离线包资源, 在cdn中使用http2协议, 一次链接, 下载所有资源. 离线包个数较多的情况下, 可以比传统http1有更快的传输速度, 同时, 客户端只需要运行一次下载器. 减少多次运行下载对手机cpu损耗.

六.回退机制 fallback

  • 可能出现的问题:
    • 本地内置的base包(zip文件)解压失败
    • 离线系统接口超时
    • 下载离线资源失败
    • 增量的资源合并失败等情况

六.离线资源管理平台

  • 管理系统演示

  • 管理系统演示

  • 功能:

    • 查看和管理各个业务线信息及其离线功能的灰度放量比例.
    • 通过业务线, 版本号, 发布时间等条件. 查询各个版本的离线包资源列表及其详细信息
    • 针对全局离线功能, 提供了离线功能开关.
    • 允许将某个版本的离线包下线, 以实现离线资源版本的回滚功能.

六. 技术选型

  • 离线系统的服务端使用nodejs实现.
  • 使用轻量的koa框架.
  • 使用log4j进行node日志的采集和记录.
  • 使用轻量的nosql数据库mongdb记录离线包的数据信息. 使用对象模型工具mongoose进行nosql操作
  • md5的加密, 使用node-rsa库进行非对称密钥的生产, 操作和加密解密处理.
  • 前端离线系统的后台页面, 使用vue与组件库iview.
  • 压力测试: 使用压力测试工具siege.
  • 建立性能监控系统, 运行稳定性/承载的压力/占用服务器硬件资源情况.

六.运行情况

  • cpu使用率
  • 应用内存使用量
  • 页面静态资源加载(js, css)耗时
  • 页面可操作时间耗时

六.展望

  • 下载引擎优化
    • 断点续传/分块下载
  • 离线资源的统计
    • node api层/cdn的nginx层实施
  • pwa技术
    • 通过接入其他更强大的浏览器内核实施

七.今日头条品质优化:图文详情页秒开实践

七.数据建立

  • 页面加载时间 = 页面加载完成时间 - 页面开始加载时间

  • 页面开始加载时间: 点击了Feed上的卡片.

  • loadFinish回调不能反映用户的真是体验.

  • WebView渲染步骤:

    1. 解析HTML文件
    2. 记载JS和css文件
    3. 解析并执行js
    4. 构建DOM结构
    5. 加载图片等资源
    6. 页面加载完成
  • 用户真实角度: dom结构构建完成(domReady)的时间点作为页面加载完成

七.白屏

  • 对webview进行截图, 遍历截图的像素点的颜色值.
  • ios提供了webview快照的接口获取当前webview渲染的内容, 底层异步回调, api耗时10ms.
  • android中提供获取视图内容结构为getDrawingCache, api耗时40ms
  • webview截图的图片进行缩小到原图1/6, 遍历检测图片的像素点, 非白色像素点大于5%的时候, 认为非白屏情况, 可以相对高效检测准确得出详情页是否发生了白屏

七.指标建立

  • 明确问题: 什么指标可以反映用户刷头条时的真实体验

  • 最开始: 页面平均加载时长: 页面加载时长总和 / 页面pv

  • 平均加载时长虽然可以反映详情页加载速度, 但因为详情页的pv比较高. 如果使用平均加载数度, 很多用户体验问题, 都被忽略掉了. 并不能反映用户的真实情况.

  • 调整口径: 所有用户进入详情页的80分位值

  • 80分为值是1s, 说明80%用户进入详情页都能在1s内加载完成.

  • 优化目标为 80%用户, 0.3s加载完成.

  • 数据示例

  • 后来提高到95分位

七.模板优化

  • 模块拆分:

  • 点击后的过程: 点击过程

  • 线上页面加载用户每次进入详情页都要多次网络加载, 极容易受到网络波动影响, 无法保证页面加载的时长和成功率, 极大的影响用户体验.

  • 于是, 在头条中, 我们将新闻中标题和正文内容分开, 把头条详情页的公共样式css和逻辑js都抽离出来, 形成一个独立而完备的详情页模板, 可以把模板内置到客户端中(这不就是离线包吗.)

  • 同时和前端约定好js脚本, 通过接口将正文内容数据注入页面完成详情页的页面展示, 通过这种方式可以将接口放到客户端上请求.

  • 示例

  • 客户端通过一定策略预加载新闻数据, 在理想状态下用户进入页面看到页面时,就可以直接使用缓存的数据. 用户在看新闻的时候可以完全实现离线化, 避免受到网络的影响.

  • 模板预热:

  • 全流程离线化之后, 页面加载的瓶颈变成本地模板加载, 优化本地模板加载.

    • 模板合并: 加载完html再去加载js和css, 需要多次io操作, 于是把js和css还有一些图片都内联到一个文件中, 加载模板就只需要一次io操作.
    • 模板简化: 将非必须的脚本异步拉取, 精简不必要的样式和JS代码, 将模板大小压缩了20%以上.
  • 模板和数据分离后, 用户点击的时候加载的是同一个模板, 实际上, 我们并不需要在用户进入页面的时候去创建webview以及加载模板

  • 只需要在合适的时机在后台创建webview, 并且提前预热模板.

  • 用户进入页面的时候, 就能使用已经加载好模板的webview, 直接将详情页的内容数据通过js注入到页面中. 前端收到数据, 渲染即可.

  • 模板预热

  • 模板复用:

  • 95分位看实际数据优化并不明显, 从数据上观察, 用户预热模板的命中率只有53%, 可进一步提升.

  • 为了尽可能提高页面加载速度, 我们希望用户每次进入详情页都能够使用预热好的webview, 模板预创建池手段优化用户进入详情页时的预热模板命中率.

  • webview的创建是一个性能开销比较大的操作, 使用雨创建池的方案, 就会在后台频繁创建webview, 这样对用户在feed场景的浏览提体验也会有一定影响.

  • 每次使用同一个模板, 用户退出页面后, 把正文数据清空, 进入下一个页面的时候能够继续复用这个webview, 重新注入数据即可

  • 过程图

  • 避免了后台创建webview对用户刷新feed体验, 又提高了预热模板命中率.

七.网络优化

  • CDN加速
  • 容灾

七.渲染优化

  • 服务器端预渲染:

    • 服务器端把所有的详情页正文的HTML数据组装好, 通过将服务端直出内容注入到页面中. 直接给webview渲染.
  • 客户端渲染:

    • webview渲染非文字部分存在一下问题:
    • 渲染效率差
    • 大量图片, webview的渲染内容占用, 滑动体验
    • 多次打开同一篇文章, 多次加载问题, 无法与客户端进行缓存共享.
    • 图片和视频等非文字内容通过原生组件放在客户端进行渲染, 提高渲染效率, 减少流量
    • 多图文章, feed页面, 只能加载详情页需要的图片, 增加用户的文章首屏体现.
  • 白屏优化

    • ios中, 我们使用系统自带的WKWebView, 一个运行在独立进程中的组件, 所以, 内存过大的时候, WKWebView所在的WebContentProcess会被系统kill掉, 反映出来就是白屏
    • 根据WKWebView提供的回调webViewWebContentProcessDidTermainate函数进行reload重新加载. 因为是模板, 没有办法注入数据.
    • 尝试重新注入时间很长. 在注入脚本中判断是否存在数据注入接口, 如果不存在, 直接重试.
    • android, 使用自研内核webview
    • 多线程读模板问题, webview在运行中会读取的文件模板, 此时如果另一个线程同时更新模板文件, 就出现了模板加载问题, 需要保证模板加载的原子性.
    • Render卡死问题, 内部渲染可能会出现Render卡死问题. 从业务上做白屏监控进行重试.
  • 不管是IOS和Android, WebView加载逻辑都比较复杂, 重试页无法成功时, 会降级加载线上的详情页, 优先保证用户的体验.

七.总结

  • 数据很重要: 优化加载之前第一件事, 就是建立一个详情页的数据看板, 只有通过数据, 我们才能了解目前线上用户的现状, 从真实用户的体验找到瓶颈和优化点.
  • 用户体验优先: 优化方案很多, 除了加载速度之外, 需要从整体应用体验出发, 选择对用户最佳的方案
  • 追求极致: 扣细节, 到极致.