专栏/Webview的分析及优化

Webview的分析及优化

2020年12月30日 06:15--浏览 · --喜欢 · --评论
粉丝:164文章:8

    H5技术已经非常广泛的应用在app开发中,尤其针对电商等需要快速迭代的业务,突破版本、系统限制,可以很有效的促进业务发展。但是也会导致性能问题,存在一些加载慢、卡顿的情况。在这个用户体验至上的年代,H5页面加载优化已经成为了必需品。本篇文章主要介绍业务通过实践总结的一套优化经验(主要基于Android的角度),分享大家,一起探讨、一起学习。

    上图是webview加载的过程,大致可以分为以下三个阶段:

    Webview初始化:App加载Webview,启动单独的web进程,内核的初始化一般是在这个进程过程中进行一次,这个耗时比较大(过程可以耗时几百毫秒,视Android机型而定)),但是实例是每次加载webview的时候都需要创建的,耗时相对较小,经过测试也需要几十毫秒。可以基于业务的场景确定提前初始化的时机,能够很大程度上提高用户打开H5页面的渲染速度。思考:为什么要单独起来一个进程?(考虑到H5的各种新老版本兼容,容易出现崩溃的情况,所以一般单独进程)

    资源加载及渲染:前面所有的初始化操作都做好了,webview开始loading url、建立链接、请求及接收静态资源css、html、js等,解析及执行脚本,然后进行渲染。这个过程可以参考下图(参考W3C官方文档)


  • 加载准备,页面加载之前先判断当前容器是否已经加载了一个页面,这种情况一般存在于单个webview容器支持多页面跳转的情况,存在需要卸载的页面,就会走到redirect的流程,同时卸载当前页面,否则直接从fetchSatrt开始的。在电商的webview一般都是单个容器加载单个页面,所以基本没有这种情况。

  • 缓存处理,webview加载先查询缓存,如果有会直接命中。这里的缓存分为两种情况,一种是内存缓存,直接拿来加载,那么后面的所有流程都省掉了。另外一种是文件缓存,webview找到静态资源文件并判断有效期,有效的拿来直接加载,这样就省掉了后面的网络连接及下载的流程了。优化的时候可以基于这个方向思考,不过不能完全靠webview的缓存,因为缓存空间是有限制的,当达到一定程度会将老的清除掉。

  • 网络连接,首次打开webview的时候一般DNS Lookup的时间会相对长一些,后面同一域名下DNS耗时会减少。然后进行TCP建立连接,发起请求,接收文件。这个过程在chrome调试上都可以看到,参考下图,这里可以看到请求的等待时间,http1.1之前,浏览器会对请求的数量限制在6个以内,后来的排队,之后的都不做数量限制了。不过还是有优先级的限制。如果通过这里查看耗时比较长需要分析原因了,这里的优化需要多端协同了。

  • 树构建及渲染展示,Html解析器拿到网页内容,构建DOM树,然后形成Render树,再进行布局和绘制(依赖基础能力:2D/3D模型、音视频解析、图片解码器等),然后展示,这个过程如果耗时过长就需要H5开发同学去分析优化了。

业务数据渲染:经过前面的阶段就已经基本具备页面框架了,这两个阶段的耗时也就基本是白屏的时间了,如果有很好的优化会大大提高的用户体验。然后就是基于业务场景请求一些数据,拿到数据后对页面全局或者部分渲染。

基于上面流程,我们可以分析出以下优化点,从而确定优化方向

  • Webview提前初始化及实例构建;

  • 利用缓存机制,人工干预缓存;

  • 资源提前下载准备,以便用时减少加载时间;

  • 业务数据提前准备,提前可交互时间;

  • 以上优化点尽可能覆盖所有H5加载。

    我们的业务经过几次演进,对以上的加载流程进行了优化,大大提高了页面打开性能。接下来就针对优化方案进行介绍。

  1. 获取Config配置,这个配置是磁盘缓存,每次启动业务都会更新,包含sourceUrl列表,这里的地址是抽象出来的Html地址,里边包含了一些业务所需要的基础资源,每一个Html会对应一个Webview实例,所以Html不可以放过多,过多会影响整体页面加载耗时,经过测试,当超过3以上的时候,每增加一个,会将其他页面的耗时性能拉低10%左右。

  2. 启动预加载Service,将列表中的地址依次初始化成Webview实例,常驻内存。当用户点击卡片加载包含Webview页面的时候,页面在Router解析的时候,通过地址到实例缓存池匹配实例,如果找到,会通过Jsb告知Webview进行一些初始化操作。等到页面创建好后,再次匹配缓存池,将符合的Webview拿来直接Attach并展示,如果前一个页面有按照规则的数据投递过来,也可以通过这个过程直接解析展示,能够匹配到的就是命中,命中这个过程的页面性能会大大的提高。如果没有匹配到sourceUrl的Webview,则会匹配到一个空的Webview,虽然是空的,但是这个也是初始化好的,经过测试发现,Webview页面加载预先准备好的空Webview可以使页面耗时减少70ms左右。

  3. 页面加载完消耗掉缓存池中的Webview实例后,会同时创建一个新的同样地址实例补充到缓存池,这样就是做到提前准备的效果,不过这个补充过程需要做个延迟,不然会出现和现有页面加载资源争夺的情况,影响当前体验。

    上面这套方案,基本上可以满足核心业务的需求。我们做个分析总结:满足了上面的可优化点。但是同时也存在一些不足,基于业务配置预加载,如果命中,会大大提高页面渲染速度,如果没有命中或者没有配置预加载的就只能用到了Webview的提前初始化了,效果大打折扣。低频的页面一般不做配置,一些活动页也没有去配置预加载。由于内存限制导致的实例数量限制,也只能同时保持少数实例,如果再优化,要么资源集中抽象,要么增加Webview实例个数。抽象过多的H5 静态资源在单个实例中,一方面影响Webview实例加载成功率,另一方面因为本身Webview缓存上限的限制,老的缓存会被自动清理,也未必能起到很好的效果。于是,我们又尝试进行了一次优化。

    Native来提前缓存文件,对Webview中的http/https请求进行拦截,匹配到的文件直接由Native合成 WebResourceResponse,返还给Webview进行渲染。方案如下:

注意点:

  • CORS(Cross-origin resource sharing)问题,一般处理方式在headers里加一些配置,在这里处理header的时候需要小心,一般是request headers里包含“Origin”才会去做跨域处理,并且避免同样参数重复插入到header里,会报错的。各位开发同学处理的时候注意。

  • Md5校验,在文件匹配的时候,一定要进行文件md5校验:

    • 防止文件被篡改,规避安全风险;

    • 防止文件下载失败或者不完整,直接拿来用是会出问题;

    • 如果同样url的文件出现更新可以及时放弃老的缓存,用新的缓存。

    这样,我们可以拿到一系列的需要预加载的文件列表,提前下载到本地,在webview加载的时候,针对url进行拦截,如果发现存在缓存文件,则匹配文件并以webview能够识别的形式传给webview。可以突破缓存的限制,并且基本不占用内存,作用于任何webview。当然这个方案是针对上一个优化方案的补充,叠加使用的,这样可以做到核心页面和其他页面加载的互补。当前这套方案仅用于Android,iOS的因为https基于实例的拦截很难实现,所以是从全局的角度去拦截,处理起来会比较复杂,方案已经在尝试中了。

    做了那么多优化,需要去量化效果,以便衡量方案是否真的有效果。我们从几个方面进行衡量:80/90分位渲染耗时、页面白屏率、页面drop情况。一般业界都是统计80/90分位页面耗时,这个就不多说了。关于白屏,一方面是观察页面离开的时候页面还没有加载,另一个方面也是内存的情况反应,当内存吃紧会出现内存栈内的Webview被杀死的情况,在上线一个方案的时候可以观察这个指标,作为一个该方案能否在线上平稳运行的依据。页面Drop情况,我们曾经做过一些数据统计,当页面超过一定时间还没有加载出来,用户会选择离开,在Android上集中在1-1.5s区间,iOS集中0.8-1.2s区间,所以优化页面速度可以以此为目标进行优化,不断尝试。

    Webview的优化探索还有很多,我们只是尝试了一部分,后续还会继续,感谢小哥哥、小姐姐们花时间阅读,如果有什么意见或者建议,欢迎留言讨论 ^_^

    求贤(哔哩哔哩专栏求点赞,求关注,求推荐):               

    B站电商也称「会员购」,是哔哩哔哩的二次元电商平台。我们是B站多元化商业收入的重要航道之一,同时也是B站增长最快的潜力业务。我们寻找志同道合的伙伴一起寻找我们的One Piece。

投诉或建议