前端性能优化方法与实战 - 前 58 集团技术副总监 - 拉勾教育
上一讲我们介绍了性能平台的监督功能搭建和诊断清单,在性能诊断和优化当中,首屏秒开是其中非常重要的一环,可以说前端性能优化中最重要的一个目标就是保证首屏秒开。
那么,如何优化来保证这一点呢?这一讲我就介绍 4 个方法——懒加载,缓存,离线化,并行化。
懒加载
懒加载是性能优化的前头兵。什么叫懒加载呢?懒加载是指在长页面加载过程时,先加载关键内容,延迟加载非关键内容。比如当我们打开一个页面,它的内容超过了浏览器的可视窗口大小,我们可以先加载前端的可视区域内容,剩下的内容等它进入可视区域后再按需加载。
另外,我在上一讲诊断清单部分已经提到过,如果首屏只需要几条数据,后端接口一次可以吐出 50 条数据,这会导致请求时间过长,首屏特别慢。这种情况就非常适合用懒加载方案去解决。
具体怎么做呢?我们可以先根据手机的可视窗口,估算需要多少条数据,比如京东 App 列表页是 4 条数据,这时候,先从后端拉取 4 条数据进行展现,然后超出首屏的内容,可以在页面下拉或者滚动时再发起加载。
那么如果首页当中图片比较多,比如搜索引擎产品的首页,如何保证首屏秒开呢?同样也可以采用懒加载。以百度图片列表页为例,可视区域范围内的图片先请求加载,一般会根据不同手机机型估算一个最大数据,比如 ihone12 Pro 屏幕比较大, 4 行 8 条数据,我们就先请求 8 条数据,用来在可视区域展示,其他位置采用占位符填充,在滑动到目标区域位置后,才使用真实的图片填充。这样,通过使用懒加载,可以最大限度降低了数据接口传输阶段的时间。
缓存
如果说懒加载本质是提供首屏后请求非关键内容的能力,那么缓存则是赋予二次访问不需要重复请求的能力。在首屏优化方案中,接口缓存和静态资源缓存起到中流砥柱的作用。
接口缓存
接口缓存的实现,如果是端内的话,所有请求都走 Native 请求,以此来实现接口缓存。为什么要这么做呢?
App 中的页面展现有两种形式,使用 Native 开发的页面展现和使用 H5 开发的页面展现。如果统一使用 Native 做请求的话,已经请求过的数据接口,就不用请求了。而如果使用 H5 请求数据,必须等 WebView 初始化之后才能请求(也就是串行请求),而 Native 请求时,可以在 WebView 初始化之前就开始请求数据(也就是并行请求),这样能有效节省时间。
那么,如何通过 Native 进行接口缓存呢?我们可以借助 SDK 封装来实现,即修改原来的数据接口请求方法,实现类似 Axios 的请求方法。具体来说就是,把包括 post、Get 和 Request 功能的接口,封装进 SDK 中。
这样,客户端发起请求时,程序会调用 SDK.axios 方法,WebView 会拦截这个请求,去查看 App 本地是否有数据缓存,如果有的话,就走接口缓存,如果没有的话,先向服务端请求数据接口,获取接口数据后存放到 App 缓存中。
静态资源缓存
数据接口的请求一般来说较少,只有几个,而静态资源(如 JS、CSS、图片和字体等)的请求就太多了。以京东首页为例,177 个请求中除了 1 个文档和 1 个数据接口外,其余都是静态资源请求。
那么,如何做静态缓存方案呢?这里有两种情况,一种是静态资源长期不需要修改,还有一种是静态资源修改频繁的
资源长期不变的话,比如 1 年都不怎么变化,我们可以使用强缓存,如 Cache-Control 来实现。具体来说可以通过设置 Cache-Control:max-age=31536000,来让浏览器在一年内直接使用本地缓存文件,而不是向服务端发出请求。
至于第二种,如果资源本身随时会发生改动的,可以通过设置 Etag 实现协商缓存。具体来说,在初次请求资源时,设置 Etag(比如使用资源的 md5 作为 Etag),并且返回 200 的状态码,之后请求时带上 If-none-match 字段,来询问服务器当前版本是否可用。如果服务端数据没有变化,会返回一个 304 的状态码给客户端,告诉客户端不需要请求数据,直接使用之前缓存的数据即可。当然,这里还涉及 WebView 相关的东西,我会放在 15 讲详细展开。
离线化
离线化是指线上实时变动的资源数据静态化到本地,访问时走的是本地文件的方案。说到这里,你是不是想到了离线包?离线包是离线化的一种方案,是将静态资源存储到 App 本地的方案,不过,在这里,我重点讲的是离线化的另一个方案——把页面内容静态化到本地。
离线化一般适合首页或者列表页等不需要登录页面的场景,同时能够支持 SEO 功能。那么,如何实现离线化呢?其实,打包构建时预渲染页面,前端请求落到 index.html 上时,已经是渲染过的内容。此时,可以通过 Webpack 的 prerender-spa-plugin 来实现预渲染,进而实现离线化。Webpack 实现预渲染的代码示例如下:
var path = require('path')
var PrerenderSpaPlugin = require('prerender-spa-plugin')
module.exports = {
plugins: [
new PrerenderSpaPlugin(
path.join(__dirname, '../dist'),
[ '/', '/about', '/contact' ]
)
]
}
并行化
懒加载、缓存和离线化都是在请求本身上下功夫,想尽办法减少请求或者推迟请求,并行化则是在请求通道上功夫,解决请求阻塞问题,进而减少首屏时间。 这就像解决交通阻塞一样,除了限号减少车辆,还可以增加车道数量,我们在处理请求阻塞时,也可以加大请求通道数量——借助于HTTP 2.0 的多路复用方案来解决。
HTTP 1.1 时代,有两个性能瓶颈点,串行的文件传输和同域名的连接数限制(6 个),到了 HTTP 2.0 时代,因为提供了多路复用的功能,传输数据不再使用文本传输(文本传输必须按顺序传输,否则接收端不知道字符的顺序),而是采用二进制数据帧和流的方式进行传输。
其中,帧是数据接收的最小单位,流是连接中的一个虚拟通道,它可以承载双向信息。每个流都会有一个唯一的整数 ID 对数据顺序进行标识,这样接收端收到数据后,可以按照顺序对数据进行合并,不会出现顺序出错的情况。所以,在使用流的情况下,不论多少个资源请求,只要建立一个连接即可。
文件传输环节问题解决后,同域名连接数限制问题怎么解决呢?以 Nginx 服务器为例,原先因为每个域名有 6 个连接数限制,最大并发就是 100 个请求,采用 HTTP 2.0 之后,现在则可以做到 600,提升了 6 倍。
你一定会问,这不是运维侧要做的事情吗,我们前端开发需要做什么?我们要改变静态文件合并(JS、CSS、图片文件)和静态资源服务器做域名散列这两种开发方式。
具体来说,使用 HTTP 2.0 多路复用之后,单个文件可以单独上线,不需要再做 JS 文件合并了。因为原先遇到由 A 和 B 组成的 C 文件,其中 A 文件稍微有点修改,整个 C 文件就需要重新加载的情况,如今由于没有同域名连接数限制了,也就不需要了。
此外, 02 讲性能瓶颈点我提到过,为了解决静态域名阻塞,提升请求并行能力,需要将静态域名分为 pic0-pic5。虽然通过静态资源域名散列的办法解决了问题,但这样做的话,DNS 解析时间会变长很多,同时还需要额外的服务器来满足,如今,采用 HTTP 2.0 多路复用之后,也不需要这样做了。
小结
好了,以上就是解决首屏时间秒开的四种方案,在实际工作当中,前端工程师还会用到离线包和 SSR 。但 SSR 的实现比较重,要做的改造比较多,要求开发者对 node 生态有很好理解和把握,而离线包依赖于 App 端内的环境,对于端外和 PC 站不具有普适性。所以,这一讲我着重介绍了懒加载、缓存、离线化、并行化这四种具有普适性的优化方案,有关离线包和 SSR 我在后面也会有专门介绍。
最后,给你留一个思考题:
懒加载、缓存、离线化、并行化这四种方案分别解决性能加载过程中哪个瓶颈点的问题呢?
欢迎你在留言区写下你的答案。接下来我们进入白屏 300ms 和界面流畅优化技巧部分。