移动 WEB 通用优化策略介绍

原址:http://web.jobbole.com/85673/?utm_source=blog.jobbole.com&utm_medium=relatedPosts


在标题里用了「通用」二字,说明我要介绍的优化策略不是为特定的 Webview 容器定制,它面向的是所有主流的移动端浏览器,包括各种 APP 嵌入的通用 Webview。

借助定制化的 Webview 容器,我们完全可以通过主动提前本地化资源包,使得移动 WEB 应用跟 Native 应用一样,只需走网络获取必要的数据。这种情况下的性能优化,更多需要关注代码执行效率,本地化资源包的推送流程和成功率。

而对于通用的移动 WEB 性能优化,首先要考虑的是网络传输性能。

我们知道,浏览器获取一个资源,花在网络上的时间开销至少有这几块:DNS 解析、建立 TCP 连接、发送请求、等待响应、传输响应正文。相比 PC 的网络环境,移动端更为糟糕,集中在这几点:高时延、高丢包、低速率、多劫持

所以,在移动 WEB 上,引入外链资源的网络成本非常高。在我们最近的一次测试中,移动端同域名空图片(使用 Nginx 的 empty_gif 指令构造)加载失败率(我们对失败的定义是触发图片的 error 事件,或者超过 9s 仍未触发 load 事件)高达 2%,外域图片失败率还要高上几个百分点。

我们还知道,头部的外链 CSS、JS 会阻塞页面渲染,通俗来讲就是头部的这些外链资源加载完之前,页面会一直白屏。之前我们统计过,一个 GZip 后十几 KB 的头部 JS,会增加大概半秒的白屏时间。

基于上述原因,我的移动 WEB 通用优化策略第一要点:

重要的 CSS、JS 直接内联在 HTML 中,头部禁止出现任何外链资源。

需要注意的是,很多性能评估工具 / 文档都说 HTML 头部不要有任何 JS。实际上这一点在实际项目中很难做到,至少大部分页面性能监控就需要在头部计时(Web Performance API 并不能解决所有性能监控问题)。对于头部内联 JS,我只有两点要求:1)没有耗时操作;2)只保留必要代码。

现在很多 WEB 应用,尤其是 SPA,服务端往往都只提供 RESTful 数据接口。这样页面 JS 代码执行过程中,还要异步获取数据,在数据加载完成之前,页面一片空白或者只有 Loading。这么做也违背了我提出的第一要点,要解决这个问题最简单的做法是服务端直接将首屏数据以 JSON 变量的形式输出到页面上;高级点的方案是利用 JavaScript 同构框架,首屏直接输出 HTML。

将重要 CSS、JS 甚至数据接口都内联在页面上,可以减少由于移动网络环境造成的页面呈现慢或者不可用等情况,但是也带来另外的问题:多次请求之间无法利用缓存,浪费流量,也让移动网络低速率的问题雪上加霜。

为了聚焦,本文不讨论代码压缩、传输压缩以及清理无用代码等减少文件体积等基础优化项目。我们假设要加载的所有内容都是必须的且压缩过。那么,对于用户首次访问,内联无疑是最优选择,因为无论如何这些资源都要加载,能减少连接数就是最大的改进。那么,如何解决用户后续访问,内联导致的无法利用 HTTP 缓存机制的问题呢?

我们引入了 localStorage 方案:用户首次访问时,服务端输出包含内联 CSS、JS 和 JSON 数据的页面,并通过 JS 将这些数据存入 localStorage;用户后续访问时,服务端只需要输出从 localStorage 读取并执行代码的 JS 片段即可。这样,后续访问的页面体积就小很多了。

可以看到,这个方案的难点在于:1)服务端如何得知用户本地存有 localStorage;2)服务端如何得知用户本地存的 localStorage 中的某个具体文件的版本是否最新。有同学会说,把存入 localStorage 的文件及对应版本都记在 Cookie 里不就可以了?但别忘了往 Cookie 里存太多信息,本身就是一种错误的做法。况且,如果 Cookie 信息完好,但 localStorage 却被清除要怎么处理?

为此,我们设计了一整套流程。首先,我们引入了由以下字符组成的 70 进制:

它们会被用来在 Cookie 中存放文件路径及版本号,70 进制意味着可以用单字符区分 70 种不同的文件或版本。另外,这 70 个字符都无需编码即可存入 Cookie。

然后,在编译流程中,对全站代码里所有需要存入 localStorage 的资源(我们通过外链标签是否存在某个自定义属性来判断)进行分析,生成文件名及对应版本号的 Map 文件。示意如下:

这份配置每次编译都需要重新构建,但有两点需要保证:1)相同文件路径对应的 70 进制字符标记必须固定;2)文件内容 md5 发生变化时,对应的版本号需要 +1。每个使用到本方案的页面头部都需要包含这份配置。

那么对于下面这样的原始 HTML 代码片段:

用户首次访问时,将以这样的形式输出:

这时,用户本地将会新增 css_1js_1js_2 三份 localStorage 数据,以及取值为 001020 的一个 Cookie。

用户再次访问时,服务端会分析 Cookie,找出对应文件在 Map 配置中的版本号,与 Cookie 中的版本进行比较,如果都没有变化,则只会输出这样少量的代码:

这样,浏览器就会从 localStorage 中取出之前存储的内容,创建相应的标签并执行。

如果之前的资源有改动,编译后 Map 配置文件就会更新。假设 js_1 已经迭代到版本 3,那么服务端会输出这样的代码:

可以看到,只有本次更新的资源才会输出全部内容。这份代码执行完之后,本地 js_1 这份 localStorage 会随之更新,Cookie 也会更新为 001320

看到这里,大家应该明白这个方案的基本原理了,这个方案需要在服务端处理一系列复杂的分支判断,具体实现代码我就不贴了。下面说几个需要特别关注的点:

首先,移动端部分浏览器在隐私模式下,访问 localStorage 对象会直接抛出异常,必须把 localStorage 的几个方法包装一下,加上 try。

其次,如果 Cookie 中的标记存在,但是 localStorage 内容丢失如何处理?我们来看这行代码:

它执行的具体操作是:查找 localStorage 中名为 css_1 的变量,创建 style 标签插入页面中。第三个参数值 cookie_name 是为了在读取 localStorage 失败时,能够清掉这个 Cookie 标记,并且刷新页面。这时,服务端发现 Cookie 标记不存在,就会全量输出内联内容,等同于首次访问。

另外,我们用单字符标记文件名和版本,容量只有 70。每个项目中,允许同时有 70 个不同的文件存入 localStorage,完全够用。假设一个文件每周修改两次版本,那么 70 个版本号会在大半年后循环到起点。假设用户浏览器存在某个文件的版本 0,大半年期间一直没来访问,直到这个文件的版本号循环到 0 他再访问一次,这时候服务端会认为他本地的文件已经是最新的。这种极端情况我们评估后认为完全可以接受。如果实在不放心,可以将文件或版本扩充为两位来表示,就能应对 4900 种不同情况。

有些 Webview 没有开启 localStorage 功能,如果我们检测到这种情况,就额外记一个 Cookie 标记,服务端看到这个标记,每次直接全量输出内联。同样,如果 Webview 连 Cookie 也不支持,那么最终效果也是每次都全量内联,至少不比优化前差。

实际上,还可以采用类似于购物车的做法实施这套方案:在用户浏览器存一个 Cookie 标识,然后服务端通过 Redis 这样的 KV 服务来找出曾经给他发送过哪些资源及各自版本。这样做的好处是代码逻辑简单,但需要引入外部服务。

资源内联可以缓解移动端网络的高时延、高丢包等问题;而资源 localStorage 化可以应对低速率;二者结合使用,才能有最好的效果。也就是说,我前面提出的移动 WEB 通用优化策略第一要点需要完善下:

重要的 CSS、JS、JSON 数据直接内联在 HTML 中,头部禁止出现任何外链资源。同时,尽可能减少页面传输体积。


采用了这个策略的页面,理应能让用户在很短时间内看到主体内容,因为头部 CSS 和 JS 都内联了,浏览器在渲染 HTML 时不会被阻塞。在我们的认识里,浏览器会异步加载页面用到的图片,加载图片不会阻塞页面渲染,更不会阻塞 JS 执行。实际情况是这样吗?

本文主要讨论在移动 WEB 中,图片的加载给页面整体性能带来的影响以及优化策略。

我们知道,浏览器的 DOMContentLoaded 事件会在主页面加载并解析完成之后触发,不会等页面样式、图片、iframe 等子资源加载完。以下是 MDN 对它的描述:

The DOMContentLoaded event is fired when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.

但在实际测试中,移动端完全相同的页面,加载与不加载图片对 DOMContentLoaded 触发时机的影响却很大。以下是我们在某个移动产品中,将图片延迟加载后的 DOMContentLoaded 时间对比,可以看出明显变化:

我们只是将页面所有图片(大约十几张)进行延迟加载,就让 DOMContentLoaded 事件提前 250 毫秒触发。这是我之前没有意料到的,移动设备在网络、CPU、内存等方面的性能与 PC 相比差距很大,很多 PC 上可以忽略的问题,在移动端必须重视起来。

移动 WEB 要做好图片优化,无外乎两点:控制图片大小控制图片加载

控制图片大小

图片高宽越大,意味着需要越多的网络开销。常见图片格式都经过了高度压缩,尺寸越大的图片还意味着浏览器在解码过程中需要耗费更多 CPU,解码之后的位图需要占用更多内存。在移动端,我们更应该关注图片大小。

根据 DPR(window.devicePixelRatio,设备像素比)选择合适的图片尺寸。现在的手机基本上都是高清屏,如果一味追求让图片更小而使用单倍图也不现实。这一点上,最佳实践是根据产品特性,结合用户 DPR 分布情况来选择合适的尺寸。例如在我们某个产品中:图片加载速度比图片质量更重要;用户 DPR 分布前三是:2、3、1.5。我们最终使用了 1.5 倍图,并且在图床缩放图片时,加了一点点锐化效果。最终图片体积很小,质量也尚可接受。

处理好响应式图片(Responsive Image)。移动上很多图片宽度不是固定像素值,例如通栏 Banner 图片的宽度是跟着设备走的。对于这种场景,使用 JS 获取设备宽度,拼出最适合当前设备的图片尺寸,交给图床进行缩放,无疑能在图片体积和质量上找到最佳平衡点。但这种做法并不可取,移动设备宽度各式各样,如果裁图规格太多,容易降低 CDN 缓存命中率。图床实时处理完图片再分发到 CDN 更耗时,在移动端让图片命中 CDN 缓存也很重要。处理响应式图片的最佳实践是根据用户屏幕尺寸分布,制定出几档裁图规则,页面根据用户设备宽度使用最合适的档位,并对重要的图片(例如头部焦点图)提前预热 CDN。

使用 WEBP 格式。有一种减少图片体积的灵丹妙药 —— 使用压缩比例更高的 WEBP 格式。《移动端图片格式调研》这篇文章详细地对比了各种移动端图片格式及各自适用场景。对于 WEBP 的最佳实践是只要浏览器支持就用,虽然 WEBP 解码慢于 JPG,但在同等图片质量下,WEBP 体积通常比 JPG 小很多。

要判断浏览器是否支持 WEBP,可以检查 HTTP 请求头部字段 Accept 的值是否包含 webp。例如这是 Chrome 给图片请求加的 Accept

不是所有支持 WEBP 的浏览器都会这样处理,可以针对这种情况使用特性检测:

这段示意代码的原理是:用 JS 加载 WEBP 图片,如果能触发 onload 并获取到宽度,说明当前浏览器支持 WEBP。

控制图片加载

我在《AMP,来自 Google 的移动页面优化方案》一文中写到:「将图片、视频等标签和第三方功能换成 AMP Components 后,AMP Runtime 可以自动处理延迟加载、按需加载等逻辑,确保页面首屏性能」。在移动浏览器打开网页,经常能感觉到明显的卡顿。造成卡顿的原因除了页面 DOM 结构复杂、CSS 过多地触发 Layout/Paint/Composite、存在复杂 JS 逻辑等等,也可能是没有控制图片的加载时机。

通常浏览器会并发加载 6 个同域名图片,如果做了域名散列,那很可能在打开页面后的短短几秒内,几十个图片都在加载。这些连接带来的 TCP、带宽、CPU、内存等开销,很容易让页面卡顿。所以在移动端,我们要让图片加载变得可控。

按需加载图片。在 PC 端,我们基本都会做图片 Lazy Load,这个优化策略在移动端同样适用。由于移动端性能有限、带宽昂贵,Lazy Load 更为重要。实际上不光是图片可以做 Lazy Load,页面所有资源包括 DOM 节点都应该做成按需加载。通常在移动端,我们只加载页面可视区域及其下方一定距离内的资源。

顺序加载图片。在 PC 端,由于硬件性能和带宽足够,并行加载更多的图片通常是最好的选择。而在移动端,人为控制图片加载顺序,例如使其从上到下、从左到右逐个加载,有时可以带来更好的体验。

不要在页面滚定时加载图片。按需加载图片逻辑需要监听页面滚动事件,根据页面当前可视区域决定加载哪些图片。在移动端滚动页面本来就很耗费性能,如果这时候还要加载图片,非常容易造成页面卡顿。在页面滚定停止之后才开始载入图片,能有效减少这种卡顿。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值