我所知道的 Web 性能优化策略

2,881 阅读10分钟

前言

本文核心分为两部分,第一部分讲述普通浏览器中能干的事情,第二部分则讲述在自建容器的背景下更能干的事情。

文章内容会比较粗略,如果你对具体实现感兴趣,欢迎在 留言。

一、止步于浏览器

1.1 DNS Prefetch

通常情况下,一个 html bundle 里面一般会有 script 等标签去加载其他的资源。浏览器在加载完 html 之后,就会去加载 script 等标签里面的内容,大多情况下,这种标签里 uri 的 host 和当前页面的往往是不相同的,那就会涉及到 DNS 解析的问题,会有一定程度的损耗。

在 HTML 里面加入 DNS Prefetch 则会让浏览器提前进行 DNS 的解析并且缓存到系统中,这样就可以提升网页的加载效率了。

<link rel="dns-prefetch" href="//img.alicdn.com">

1.2 域名收敛

在像 v2ex 这样的社区论坛中,往往会有很多用户外链图片,不同的图片有不同的域名。这个时候 DNS Prefetch 会显得很无力,如果说在图片上传之后做一定的 转化 或者 同步,把域名收敛到同一个域名中,这样就能弥补相关的缺憾了。

aaa.com/jjj.png ->  mycdn.com/aaa/jjj.png
bbb.com/jjj.png ->  mycdn.com/bbb/jjj.png

1.3 加载合适的图片

同一个图片在 在不同的设备、不同的网络条件、不同的渲染面积下,统一加载一样的尺寸难免是 “奢侈” 的,利用 CDN 裁剪 + 前端嗅探 去加载不同体积、不同压缩比率、不同格式的图片,能在节省不少流量的同时提升很大的性能。

目前主流 CDN / OSS 都是支持通过 URL 后缀进行图片裁剪的,比如:阿里云

http://image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=image/crop,x_100,y_50

在前端嗅探上,一般我们可以嗅探 是否支持 webp、客户端分辨率、当前网络状况,去全局设置不同的图片加载 URL。

base64、

1.4 不要展示 “绝对Loading / 占位”

当前时代,大部分网页的数据都是动态下发的,甚至千人前面,为了减少用户等待的焦灼感,往往会设置一个 Loading 动画或者 骨骼图占位。但当请求响应足够快的时候,会发现这种 Loading 或 占位 却会给人相反的感觉 —— 瞬间的闪动。

针对这种情况,在使用 Loading 或骨骼图占位的,可以做一定的优化,比如请求发起后 200ms 以上还未返回数据才展示占位图。在 React Suspense 中,为了这种效果官方甚至加了一个 API。

1.5 资源 combo

在 HTTP 请求中,请求创建往往因为 TCP 三次握手等会有一个非常大的开销。涉想在 HTTP 1.0 时代,如果页面上有 50 个 Script 标签,会有 50 次的请求创建,在资源不是那么大的情况下,请求创建的耗时往往会远大于资源真正的下载时间。

在服务端做资源 Combo,然后再往 CDN 回源,就能很大程序上减少这种开销。

xx.com/a.js
xx.com/b.js
xx.com/c.js
xx.com/d.js

xx.com/combo?a.js,b.js,c.js,c.js

在 HTTP 2.0 时代资源要不要 Combo + 多少个资源(多大的体积)Combo 到一起 是个有意思的话题,这里不做讨论。

1.6 在线 Shim

前端因为浏览器问题一直存在店铺这种东西,有很多的特性往往在新的浏览器版本里面已经支持,但奈何老的不支持需要做垫片。如果对代码统一加垫片则又会让新浏览器很尴尬(我要这新特性有何用?),使用在线的 Shim 应该是一个不错的 Shim。

比如著名的 polyfill.io/v3/url-buil… 就是一个这样的服务。在支持 Object.assign 中访问 polyfill.io/v3/polyfill… , 会得到:

/* Disable minification (remove `.min` from URL path) for more info */

而在不支持的浏览器中访问,会得到:

(function(undefined) {if (!(// In IE8, defineProperty could only act on DOM elements, so full support
// for the feature requires the ability to set a property on an arbitrary object
'defineProperty' in Object && (function() {
	try {
		var a = {};
		Object.defineProperty(a, 'test', {value:42});
		return true;
	} catch(e) {
		return false
	}
}()))) {!function(e){
...

在国内也有这样的服务,比如:polyfill.alicdn.com/polyfill.mi… ,如果你觉得不够信赖,可以自建然后部署在 CDN 上。

1.7 分离静态资源

在绝大多数情况下,访问静态资源的时候并不需要知道 Cookie 信息,为静态资源使用一个新的域名能有效避免用不着的 Cookie 上传,能减少一部分无用流量的传输。

1.8 使用 requestAnimationFrame 实现 60 FPS 动画

在绘制动画的时候,优先使用 requestAnimationFrame,会充分利用分利用显示器的刷新机制,实现 60 FPS 的感觉。

1.9 节流 & 防抖

在 Web 中,像 Scroll 这种事件,在界面操作中触发的频率是非常之高的。涉想这样的一种场景:用户往下滑动网页,当滑动距离超过 1000px 的时候,右下角展现一个 回到顶部 的按钮。想当然的操作就是监听 Scroll 事件,当值大于 1000px 的时候展示 按钮,但因为 Scroll 的高频率触发,尤其在移动端这样做就能感觉到比较明显的性能问题了,如果我们对其加个操作 —— 1s 内检测函数只触发 1 次 或者在用户停下来的时候再去检测位置。这样页面整体就会流畅很多了,相对应的两个操作就是 节流 和 防抖。

在 Web 开发过程中,对于这种高频次触发的事情,合理的进程节流和防抖能在很大程度上增加页面流产度。

1.10 使用新版本 JavaScript

一般情况下,V8 等 JavaScript 的 Runtime 都会对新特性有优化,在新浏览器上使用 Babel 转化后的代码难免会有一定的浪费与可惜。在新浏览器上使用新预发,老浏览器上使用老语法,才是比较好姿势。

实现上一种思路就是在线 Shim;第二种思路是正对先加载 seed 再加载 bundle 代码,可以在加载 bundle 之前做一个知否支持新版本 ES 的判断(比如是否支持 async 函数),然后再加载相应的 Bundle。

1.11 性能测量

window.performace 能展示绝大多数检测 Web 性能的指标,在业务代码中埋点收集 window.performance 的值,可以为网页性能短板做很好的测量与统计。

1.12 善用 LocalStorage

在一些场景下,每次用户进入时数据的变化不会太大,比如不怎么更新的个人博客页面。这个时候就可以使用 LocalStorage 去做 HTML 的缓存,页面进入的时候直接从 Storage 中获取缓存,然后 append 到页面上,等接口数据回来之后,再 Diff 做更新。

在新版本浏览器中,可以用 indexedDB 等代替 LocalStorage。

1.13 组件级缓存

在 SPA 网站中,加载 bundle 大致上可以分为两份:

  1. 所有的组件代码和业务代码打到一起,和业务代码一起输出
  2. 组件代码在组件内部各自打包,业务代码打包的时候 external 掉组件代码,最后 combo 到一起输出。

针对第二种情况,可以利用 LocalStorage 等单独缓存组件代码(带上版本号),在端侧实现一个 Combo 的机制(有 Cache 取 Cache,没 Cache Fetch),这样一来,就能让一个网站的多个页面享受同份缓存,让之第一次也能非常快速的访问。

1.14 GZIP & BBR

GZIP 压缩使用 Deflate 能有效压缩文本资源的大小,在现代浏览器中,对 GZIP 的支持也是非常良好。值的注意的是,GZIP 的压缩并不是压的越小越好,太小会产生压缩性能的问题。

传统 TCP 使用的是基于丢包的拥塞控制算法,但并不是所有的丢包都是网络堵塞所导致的,为此 Google 开发了 BBR 拥塞控制算法,能有效提升服务器的吞吐量,如果服务器支持的话,可以开启 BBR 来加快网络传输。

1.15 Service Worker

Service Worker 本质上充当Web应用程序与浏览器之间的代理服务器,利用 Service Worker 可以极致的控制每个请求,进而可以对 Web APP 在浏览器上做离线处理。

传说中的 PWA 就是对这个东西的一个极致应用。

1.16 WebWorker

WebWorker 为 JavaScript 在浏览器中多线程调用提供了能力,可以让主线程创建 Worker 线程,针对一些密集计算或者需要时间比较高的场景,是非常有效的。比如:网页版邮箱附件上传等。

二、外探于自建容器

2.1 WKWebview

从 iOS8 开始,iOS 提供了 WKWebview 来代替 UIWebview。相比于 UIWebview,在性能和内存控制上都有非常大的提升,当然问题也是有的,比如 Cookie 同步问题等,但坑总能趟过去。

回到优点上,WKWebview 给前端最直接的体验莫过于 “Scroll 终于不再需要滚动停止下来才能触发了”,进而 LoadMore 等会更加的流畅。

2.2 Webviw 内核内置

Android 的碎片化一直是一个很严重的问题,即使是在今天。通过内置高性能的 Webview(比如 U4、X5)等,会为整个 APP Web 页面提速不少,在兼容性方面,也会好很多。

2.3 资源 Cache

要实现页面秒开效果,Cache 肯定是第一优先级,通过下发离线包,让页面上的资源(HTML,CSS)离线,就可以很大程度上提升页面的性能。

在离线体系建设上,主要有两点需要考虑:1. 离线如果能快速下发,快速覆盖新版本 2. 前端如何才能无感知接入离线体系。

针对第一个问题,往往采取推+拉结合的方式;针对第二个问题,在实现的时候采用拦截网络的策略,就可以避免前端对离线的感知了

2.4 代理请求

如前文所述,创建一个 HTTP 请求是非常耗时的,从客户端的角度来看,是可以去优化这种请求的,比如被广泛使用的 Spidy。

在 WebAPP 中,发出一个数据请求,让走容器的通过而非 WebView 的通道,不仅有机会能让速度变快,同时还可以进行相应的加密,让抓包者懵逼。

2.5 数据预加载

试想一个网页的加载过程,加载 HTML -> 加载 JS -> 执行 JS -> 请求数据 -> 再次渲染。请求数据的流程是比较靠后的,在 WebAPP 中,如果能让这个请求或者请求到的数据提前,则会进一步提升网页速度。

一个比较常用的方式是往客户端下发一份配置,标识 页面地址、请求入参、缓存时间等信息,让客户端在访问这个 URL 的时候去加载数据,等前端代码真正请求的时候,直接拿客户端提前请求到的数据;或者说在前一个页面调用接口通知客户端去请求相关的接口。

目前这种做法应用比较广泛,比如:微信公众号,UC Feed 流等。

2.6 阉割版 WebView

WebView 因为各种历史原因,“慢” 一直徘徊在他的左右。去定制 WebView,甚至去实现一层上层 DSL,只保存比较优秀的部分,也能在限制部分场景的同时去提升页面性能。

最典型的设计就是微信小程序了,阉割版的 WebView + Cache 机制,很难让人会觉得这是一个 WebAPP

2.7 Weex/React Native

Weex/RN 相比于阉割版 WebView,是一个更加激进的优化访问。核心原理是自己造一个高性能的 Runtime,和客户端高度配置,由 JS Bundle 去 call native,用 Native 的方式去渲染页面。

同时对于 Web 上性能上有问题的部分,可以用传入表达式的方式让 Native 去执行。比如注明的 BindingX,就是传入一个表达式,然后去客户端去执行这个表达式,进而避免了频繁 call native 的问题,这样就可以保证动画的流畅运行了。

2.8 Flutter

...

原文地址:github.com/rccoder/blo…