离线包方案参考思考过程-总结了几篇文章
总结了下几篇文章
网易(资源离线/JsBridge通信/接口预请求)
- 网易新闻客户端H5秒开优化
- H5优势: 跨平台, 实时更新, 便于传播等
- 劣势: 功能(硬件访问能力, 离线功能), 性能, 体验等
一. 资源离线
-
静态资源加载耗时, 资源离线到本地, 能很好解决.
-
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 秒开方案探索与实现》
- 参考: 移动端本地 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 submodule
Git 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
- 二级目录及子目录: 业务自定义的资源文件. 建议所有前端文件保存在一个统一目录下.
- 一级目录: 一般资源包的ID, 例如
* 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地址. 实现在线页面和离线页面随时切换.
三.离线包运行模式
- 请求包信息: 从服务端请求离线包信息, 存储到本地数据库过程. 离线包信息包括离线包的下载地址, 离线包版本号等.
- 下载离线包: 把离线包从服务端下载到手机.
- 安装离线包: 下载目录, 拷贝到手机安装目录.
三.虚拟域名
- 虚拟域名仅对离线应用有效, 当页面保存在客户端之后, 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, 将资源映射到本地离线包, 更新的时候对版本资源检测, 下载和维护本地缓存目录中的资源.
- 对于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压缩, 变量名的变化可能引起大幅的变更.
- 可以减少客户端升级离线资源需要下载的流量
- 影响bsdiff生成的差分包的体积因素主要有:
- 单独控制各个业务线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渲染步骤:
- 解析HTML文件
- 记载JS和css文件
- 解析并执行js
- 构建DOM结构
- 加载图片等资源
- 页面加载完成
-
用户真实角度: 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加载逻辑都比较复杂, 重试页无法成功时, 会降级加载线上的详情页, 优先保证用户的体验.
七.总结
- 数据很重要: 优化加载之前第一件事, 就是建立一个详情页的数据看板, 只有通过数据, 我们才能了解目前线上用户的现状, 从真实用户的体验找到瓶颈和优化点.
- 用户体验优先: 优化方案很多, 除了加载速度之外, 需要从整体应用体验出发, 选择对用户最佳的方案
- 追求极致: 扣细节, 到极致.