百度爱番番移动端网页秒开实践

导读:目前是移动互联网全面发展的时代,随着产品迭代速度的不断提高,网页在 App 开发中占据的比例也与日俱增。网页开发不只能够较低成本的实现 iOS、Android 和 Web 等多端复用节省人力,还可以有效减小程序安装包的体积,更重要的是能够堂而皇之的规避 Apple 对 iOS 端热更新的封锁。但另外一方面,移动端网页相较于原生页面而言在加载速度方面仍有比较明显的差距。如何最大程度的减少这种差距,为用户提供一个良好的交互体验就成了每个移动开发者都须要掌握的能力。本文将结合百度爱番番前端团队在过去一段时间里的实际研发经历,为你们从体验、性能、安全等方面系统分析并优化解决移动端网页开发所面临的一些问题,让用户在 App 中打开网页时可以作到秒开,如原生页面般流畅。前端

全文5800字,预计阅读时间 12分钟。算法

1、明确问题:网页缓慢

现阶段移动端设备相较于传统的桌面级电脑还有不少不足之处,“ 带宽低 ”、“ 速度慢 ”、“ 内存小 ”是三个最明显的瓶颈,而这些却偏偏是网页所依赖的重点。后端

其中首当其冲的就是网络条件,尽管近年来伴随着 4G、5G 的普及用户手机的网速不断提高,可是移动端的网络延迟永远是不肯定的,它会受到各类条件的限制,现实生活中仍然会有不少状况下会致使用户的网速不佳。而这种制约对网页而言是十分严重的,它会使得网页加载过程变得更加漫长,甚至是失败。跨域

另外一个方面是处理器的速度,现今的网页承载的信息愈来愈多,界面交互和业务逻辑也愈来愈复杂,过多的计算量会让网页的处理时间增长。而用户设备的硬件配置又是多种多样的,这个问题在其中占大多数的中低端机型上会更加明显。浏览器

对于网页而言,设备的内存大小也很重要的,更大的内存表明能够支持更多的网页内容。反以内存紧张会让 APP 在处理网页时变得效率低下,频繁出现卡顿问题,最致命的是会更容易引起 OOM( Out Of Memory )现象致使程序崩溃。缓存

随着技术的发展,移动端交互体验的不断提高,人们对网页加载缓慢的忍耐度也愈来愈低。有调查代表,超过 2/3 的用户认为对于网页来讲加载速度是影响浏览体验最大的一个因素。当移动端的网页加载时间超过3秒,过半的用户会选择直接离开。因此一个快速的加载过程,是咱们提升 APP 网页质量的重要一环。安全


2、分析痛点:加载耗时


在讨论如何提高网页加载速度前,须要先以数字的形式给出网页加载缓慢的定义,明确一个基准点——如何定义用户所感觉到的网页加载耗时。这里有一个计算公式:性能优化

网页加载耗时 = 网页加载完成的时间 – 页面开始加载的时间服务器

其中页面开始加载的时间比较容易判断,从用户的角度来看,当他在上级页面点击某处跳转网页的时候就能够理解为页面开始加载了。网络

关键是如何界定网页加载完成的时间,从客户端开发的方向来讲,不管是 iOS 仍是 Android,做为承载网页的 WebView 控件,都有一个 loadFinish 回调表示网页加载完成,可是实际上它并不能真实反应用户的实际感官体验。

这里咱们先来梳理一下移动端加载一个普通网页大体须要通过哪些步骤:

图片

因而可知,用户在打开网页的整个过程当中前后会经历 无反馈、白屏、loading 这几个阶段,而在 WebView 控件 loadFinish 后,页面基本上还停留在 loading 界面。因此上面公式里提到的网页加载完成通常能够理解为业务数据渲染完成的时候,由于只有在这以后用户才可以真正看见想要的内容。

换言之网页加载缓慢体如今数值上来讲就是指用户点击开启网页到业务数据渲染完成这段时间差过大,那么如何下降这个时间差就是咱们亟待解决的问题。

3、提供方案:优化实践


图片

针对移动端网页加载时无反馈、白屏、loading 这三个阶段,爱番番前端团队从前面提到的网络条件、处理速度、内存占用这几个点进行切入,针对缓存系统、网页渲染机制、浏览器内核、网络请求效率等方向,制定了一系列的优化方案。

首先是 “ 独立组件打包分发 ” ,其后在此基础之上前后进行了 “ 页面按需预先渲染 ”、“ 网页容器预初始化 ” 和 “ 业务请求前置执行 ” 等处理。

咱们的目标是使爱番番内各主要网页的加载耗时下降到 1s 之内。

3.1 独立组件打包分发

图片

一般来讲,加载网页时静态资源的下载是很是耗时的,并且这个过程也是最容易受到网络环境影响的。为了解决这个问题,咱们将一组独立网页的 HTML、JavaScript、CSS 等静态资源压缩打包,造成一个离线组件包,在 App 启动后预先下载并解压到手机本地,当用户打开目标网页时,直接从本地加载这些资源。

并且一个 App 中根据业务模块能够分为多个离线组件包,每一个离线包都拥有惟一的版本号,经过后端搭建的离线包平台进行管理和下发,客户端会在指定的时机和平台同步离线包的版本信息,当有版本更新的时候,会在后台批量静默下载并更新本地文件,用户在正常操做 App 时基本处在一个无感知的状态。

经过独立组件打包分发的方案能够绕过耗时的静态资源下载环节,网页加载过程当中的白屏时间也可以获得大幅下降。

3.2 页面按需预先渲染

图片

页面按需预先渲染是为了一次性解决网页加载过程当中各个环节问题所制定的优化方案,它基于客户端渲染( NSR,Native Side Rending )的思想实现,而 NSR 又是由服务端渲染( SSR,Server Side Rendering )引伸而来的,NSR 的本质是分布式的 SSR。

SSR 是指在服务端完成网页的渲染,在服务端完成页面模板、数据填充、页面排版等工做,而后将完整的 HTML 内容返回给浏览器。因为全部的渲染工做都在服务端完成,所以网页加载耗时会有所下降。可是这种优化方案致使前端页面的渲染须要在服务端完成,并不能很好进行先后端职责分离,并且页面加载过程当中不可避免仍会有一段白屏时间,同时对于服务端的负载要求也会比较高。

因此这里咱们采用了 NSR 的方式,在用户登陆成功后,借助 WebView 控件启用一个 JS-Runtime ,在用户手动跳转目标网页以前提早在后台加载本地离线组件包中的资源并发送网络请求获取业务数据,再进行排版和渲染,动态直出,最后将网页设置到内存级别的 MemoryCache 中,从而达到点开即看的效果。退一步说,即使用户在点开页面时以上流程并未所有执行完毕,也会由于提早执行了其中部分流程,较传统模式下降一些用户感知时间。

可是另外一方面,预先渲染也是一柄双刃剑,它本质上是利用空间换取时间,会占用大量额外的内存空间。但内存在一些较低端的移动设备上是十分宝贵的,太高的内存占用会引起一系列的体验和稳定性的问题。因此如何在尽量低的内存占用状况下完成预先渲染,是须要仔细权衡的。最终咱们决定按需只对 App 内入口级的几个重要页面开放了此功能,尽可能避免占用太高的内存空间。

页面按需预先渲染的收益是十分显著的,经数据统计,目标页面的平均网页加载耗时 iOS 从2500ms 下降到了 231ms ,Android 从 2803ms 下降到了 628ms。

3.3 网页容器预初始化

移动端和 Web 端网页的加载过程并不彻底一致,当App启动时默认是不会自动初始化内嵌浏览器内核的,只有看成为网页容器的 WebView 初始化时才会执行。因此针对这一点咱们设计了网页容器预初始化的优化方案。

3.3.1 容器预加载

图片

容器预加载是网页容器预初始化方案的核心,即在用户开启网页前预先进行 WebView 控件的初始化以及相关资源和框架的加载以下降网页加载耗时。

爱番番在下载和更新离线组件包后,会在后台初始化 WebView 控件,并加载组件包内的一个中间态网页,提早加载相关资源和框架,中间页加载成功后 WebView 会被放置在容器池中,开始监听一个自定的 JS 方法并进行等待。

当用户点击开启目标网页时,会先根据所在离线组件包内的配置文件,判断该该页面是否开启了容器预加载的功能,若是开启了会向容器池请求获取初始化好的 WebView,获取成功后调用自定的 JS 方法通知 H5 端,最后 H5 端经过 Vue Router 跳转目标页面。

而且在容器池向外交付 WebView 时,会自动从新初始化一个新的 WebView 开始加载中间页,为下一次用户操做作准备。

由于在容器池中获取的 WebView 已经提早进行了初始化,而且完成了组件包内一些公共资源和框架的加载,因此在当用户开启网页时所见到的白屏阶段就会大幅缩短,网页加载耗时也会显著下降。具体体如今数据方面,使用该方案优化的网页在 iOS 和 Android 双端加载速度均提高了 200~300ms 。

3.3.2 微前端架构

图片

在前面的 “ 容器预加载 ” 方案中,由于各业务离线组件包内的页面间是相互独立的,没法经过Vue Router 跳转至其余组件包内的页面,因此须要在容器池中为每一个业务组件包都提供一个WebView 控件,用于加载中间态网页。随着业务组件包数量的不断增多,容器池中的 WebView 也会同步增多,如此会大幅提升内存占用,而如前文所说,较高的内存占用可能会引起程序运行卡顿,甚至崩溃等问题,这在较低端的设备上是尤为致命的。

而微前端方案则很好的解决了这个难题,所谓的微前端主要是将原先的多个业务离线组件包聚合成为了一个系统,实现系统内的总体调度,完成组件包间的交互。爱番番采用的是 Master-Slave 架构,即主-从式设计:

Master:公共组件包,负责加载其余组件包,而且提供公共资源;

Slave:各业务组件包,负责不一样模块的具体业务代码。

其中 Master 和 Slave 之间的数据交互在本地主要依赖 Symbolic Link 实现,Native 端会为包括公共组件包在内的每一个离线组件包提供一个对应所在本地路径的 Symbolic Link。容器池中仅为公共组件包提供一个 WebView 控件,而公共组件包能够经过 Symbolic Link 进行本地寻址,找到对应业务组件包内页面的路径,再使用 Vue Router 就能够完成 “ 容器预加载 ” 中的跳转逻辑。这样一来就在原有 n个 组件包的条件下,将组件容器池中的 WebView 控件从 n个缩减为了 1个,内存占用也缩减到了原来的 1/n,有效下降了程序的卡顿率和崩溃率。

另外一方面,公共组件包也将各业务组件包内的一些公共框架资源提取了出来,如 Vue Router等,各业务组件包在使用它们时,一样能够经过 Symbolic Link 定位到公共组件包中的对应框架资源。这样作的好处在于能够对公共资源进行统一管理,并在必定程度上下降了离线组件包总体的体积。

经过微前端架构的优化,使得咱们的 App 在展现网页时明显下降了内存占用,避免了不少高内存带来的问题,并且各业务离线组件包的体积也都有所缩小。

3.3.3 预置离线包

图片

因前面提到的 “ 微前端架构 ” 中采用了主-从式设计,做为 Master 的公共离线组件包内包含了业务离线组件包( Slave )所须要的一些公共框架资源。当用户打开业务组件包的某个页面时,公共组件包的存在就成为了这个页面能正常运行的前提条件,而当用户初次安装启动 APP时,一定有一个从离线包平台下载公共组件包的过程,若是下载过慢会致使期间其余全部业务组件包都没法正常使用。

为了不此类问题的出现,咱们采用了将公共离线组件包预置进 APP 安装包内的方式来确保其优先性,而且它会随着 App 发版进行更新。APP 在初次安装启动后,通常会跳过预置包的下载流程,将其直接从 APP 复制到本地沙盒中。

并且另外一方面,预置离线组件包的方案不只适用于公共组件包,也适用于业务组件包,尤为对于其中一些体积较大下载耗时较长的包,能够在 APP 初次安装启动时为用户提供更加良好的交互体验。

3.4 业务请求前置执行

网页大多须要依赖服务器提供业务数据驱动页面展现内容。在前面分析网页加载耗时的过程当中能够得知,在传统模式下业务网络请求要在 WebView 容器 loadFinish 后才会执行,针对这一点咱们设计了业务请求前置执行的优化方案。

3.4.1 客户端请求

图片

为了支持业务请求前置执行的方案,首先须要对网页中的网络请求进行客户端化改造,即由Native 端来处理网页中的业务数据网络请求。而且用客户端请求和服务器进行交互,还能够解决原先使用 XHR 请求的一系列相关问题,好比跨域限制、测试联调时没法直连后端,网络层配置逻辑不统一等。

具体步骤方面,在网页进行网络请求时,首先由 H5 端配置业务请求信息,如请求地址、接口入参、自定义请求头等,并经过 JS-Bridge 将这些内容发送到 Native 端,Native 端再执行一些网络层的统一配置和优化后发送请求,好比添加 Cookie 和一些必要的请求头数据等。最后在收到 response 后再次经过 JS-Bridge 将内容返回给 H5 端。

这样一方面使得前端开发人员在调试时不用作任何配置和代理便可直连服务器,避免了传统模式下耗时的发包流程,大幅提高了迭代开发的效率。另外一方面,iOS 端的 WKWebView 增强了安全性限制,在访问本地网页时禁止跨域请求,使用客户端请求能够完美规避这个限制。最后它还能够将网页中全部的请求都在Native端进行集中管理和统一优化,这也为以后的“网络预加载”的实现提供了前提条件。

3.4.2 网络预加载

优化前(图1):

图片

优化后(图2):

图片

网络预加载是业务请求前置执行方案的核心。通常来讲,网页中的业务网络请求最先能够在在页面构建 DOM 完成后执行,即 图1 中的 B点之后,而在爱番番中,大部分网页的业务请求相关参数都依赖于一些页面级的入参,可是页面入参须要等待本地 JS 脚本( 爱番番 App 内置的 JS 文件,其中包含 H5 端和 Native 端间交互的众多逻辑,是网页正常加载的前提条件 )注入完成以后才能够获取,又由于本地JS脚本须要在 loadFinish( 即 图1 中的 B点 )以后才执行注入,因此对咱们的大多数网页来讲,发送业务请求的最先时机在 图1 中的 C点 之后。而且按前文 “ 客户端请求 ” 的技术方案,网页发送网络请求须要先由 H5端 配置请求信息,再经过 JS-Bridge 申请 Native 端来真正执行。因此在进行网络预加载优化之前,业务网络请求的发送时机为 图1 中的 D点。

这里总结一下,传统方式下网页加载耗时能够简单分为两个部分:

一、解析静态资源并构建 DOM 结构和本地 JS 脚本注入( 即 A-D ):这里咱们将此部分称为 Part1,它的耗时主要取决于前端;

二、业务网络请求的具体执行过程( 即 D-F ):这里咱们将此部分称为 Part2,它的耗时主要取决于用户的网络环境和后端;

Part1 和 Part2 两者之间是先后串行的关系。那么咱们为了尽量的下降用户感知时间,能够将 Part2 尽可能前置,将其与 Part1 作并行处理。

网络预加载正是基于这种思想实现的,首先前端开发人员会先将要执行预加载的网络请求的相关信息写入对应离线组件包的配置文件当中,当用户打开该离线包的指定网页时,会在 Native端直接从组件包的配置信息中读取信息并当即开始发送网络请求。这个动做会在子线程中执行,几乎和 WebView 控件的初始化同时开始。

而以后获取 response 的时机(即图2中的I点)根据 Part2 耗时的不一样,大致可分为2种状况:

一、在Part1 完成以前(即I点在J点以前、G点以后,如图2所示):这时 Native 端会将网络预加载获取到的 response 进行缓存并开始等待,当 Part1 完成后 Native 端会收到H5端以正常方式发来的同一请求的申请,此时当即经过 JS-Bridge 将缓存的 response 交付给H5端使用,并销毁缓存。另外一方面,若等待时长超过必定时间限制,Native端也会销毁缓存,视做这次网络预加载行为失败。若是成功,这种状况下的收益为整个 Part2 的耗时(D-F);

二、在 Part1 完成以后(即I点在J点以后):这种状况下,当网络预加载的请求仍在途中时,Native 端就收到了 H5 端正常方式发来的请求申请,此时 Native 端会拦截本次的请求申请,继续等待前面网络预加载的请求返回,待返回后再经过 JS-Bridge 将 response 交付给 H5端使用。这种状况下的收益为 Part1 的耗时(A-D)。

以上就是网络预加载的主要实现原理,经过这种优化方案,在用户打开移动端网页时,能够很大程度的下降用户在开启网页时的可感知时长,体如今数据方面,爱番番内使用此方案的网页加载耗时下降了 300 ~ 600ms 。


4、总结收益:持续探索


除了以上提到的这些,百度爱番番前端团队为了下降网页加载耗时还作了许多工做,好比 iOS升级 WKWebView、Android 升级 X5 内核等等,限于篇幅缘由,在此就不作展开了。

经过这些优化手段,爱番番移动端各一级核心页面的加载耗时从平均 2-3秒 收敛到了 1秒 之内,基本上达到了秒开的既定目标。

固然,咱们将来须要作的还有不少,后续也会持续推动移动端网页的性能优化工做,让用户在使用爱番番时可以享受到接近原生页面的体验。

本期做者 | 时恩宝贝,百度爱番番前端高级工程师,拥有多年研发经历。擅长iOS、Android、Web多端开发。

招聘信息

不管你是后端,前端 ,大数据仍是算法,这里有若干职位在等你,欢迎投递简历, 爱番番业务部期待你的加入!

简历投递邮箱:geektalk@baidu.com (投递备注【爱番番】)

推荐阅读:

解密百TB数据分析如何跑进45秒

从 Web 图标演进历史看最佳实践 | 文末送书

百度内容风控词表那些事儿|文末送书

---------- END ----------

百度Geek说

百度官方技术公众号上线啦!

技术干货 · 行业资讯 · 线上沙龙 · 行业大会

招聘信息 · 内推信息 · 技术书籍 · 百度周边

欢迎各位同窗关注

相关文章
相关标签/搜索