前言
对于 Web 页面首屏优化,有时候明明感觉自己做了很多事情,比如精简了首屏内容,合并了请求资源,优化了项目打包配置,也对图片进行了压缩,但最后的首屏时间还是没有降低下来,这是为什么呢?
要解答这个问题,需要先了解页面加载的全流程。
1、页面加载全流程
页面加载主要分为 3 个大的阶段:
- 客户端发起请求阶段。
- 服务器处理请求阶段。
- 客户端页面渲染阶段。

接下来针对这几个阶段进行详细介绍。
2、客户端请求阶段
客户端请求阶段可以分为下面 4 个子阶段:
- 查询本地缓存。
- DNS 解析。
- TCP 连接。
- HTTP 请求。
2.1 查询本地缓存
当用户在浏览器地址栏输入网址并按下 enter 键之后,浏览器会查询是否有可用的本地缓存,这个缓存分为两种:
- 强缓存 :浏览器在加载资源时,会先根据请求头的
expires或者cache-control判断是否命中强缓存,若命中,则直接从缓存中获取,不会发送请求到服务器。一般来说,对js、css和图片等资源会启用强缓存。 - 协商缓存 :浏览器会先发送请求到服务器,通过
If-Modified-Since / Last-Modified、If-None-Match / ETag这两对请求/响应头来判断资源是否命中协商缓存。- 若命中,服务器会返回
304状态码,代表资源Not Modified,自然也不会返回资源内容,而浏览器接收到这个状态码之后,会从缓存中加载该资源。 - 若没命中缓存,会返回
200状态码和新资源。
- 若命中,服务器会返回
本地缓存是前端性能的瓶颈点之一,因为它可以大大提升静态资源的加载速度。
比如一个列表页的请求,DNS 解析时间 50ms + TCP 三次握手、TLS 协商 400ms + 数据返回 200ms,一个请求下来大约就 650 ms了,这个还是在强网(4G/5G/WIFI)情况下,如果在弱网(2G/3G)情况下,一个请求连接都需要 1.5s,而如果使用缓存的话,强缓存在几毫秒内就能完成,协商缓存的话,如果命中缓存,也只需要几十毫秒而已。
2.2 DNS 解析阶段
DNS 解析之所以会成为前端性能瓶颈点,是因为每进行一次 DNS 查询,都要走一遍手机 -> 移动信号塔 -> 认证 DNS 服务器的过程,一般来说,DNS 解析要耗费 50ms 左右(强网环境),而如果使用浏览器本地 DNS 缓存,则耗时会在 1ms 以内。
我们可以对浏览器或者 webview 配置DNS 预解析的接口,加快这一速度。
2.3 TCP 连接阶段
在 OSI七层网络通信参考模型中,这个主要是建立 TCP 连接主要是传输层做的工作,我们前端能做的事情十分有限,这里不多做介绍。
2.4 HTTP 请求阶段
这个阶段最大的瓶颈点来源于请求阻塞。请求阻塞 指的是浏览器为了保证访问速度,会对同域名的资源做连接数限制,一般是 6 个,超过了就要排队,后续请求需要等最先返回请求的连接结束之后,才能开始请求。
对于请求阻塞,我们可以采取一些优化手段:
- 域名规划。可以看看当前页面需要用到哪些域名,然后最关键的首屏需要用到哪些域名,规划下域名的发送顺序。
- 域名散列 。前面说同域名有连接数限制,那我们多搞几个域名就好了呀,比如我们图片资源的地址原来是
image.example.com,我们做成支持image0-5的域名地址,比如image0.example.com、image1.example.com、...image5.example.com,每次请求时随机选一个域名进行请求,这样在同时有 6 个域名可以使用的情况下,我们网页的最大并发连接数就来到了 36 个,当然,这个域名个数不是越多越好,太分散的话会遇到多域名无法缓存静态资源的问题。 - 使用HTTP2 。
HTTP/2只需要一个 TCP 连接就能实现多路复用,解决了HTTP/1.1的队头阻塞问题,而且还有二进制分帧(Binary Framing)、头部压缩(HPACK)、服务器推送(Server Push)等优化,其效率会比HTTP/1.1高出不少。
3、服务端处理请求阶段
服务端处理请求阶段是指服务器收到请求后,从数据存储层取到数据,并经过一系列处理后返回给前端的过程。
其瓶颈点如下:
- 是否做了数据缓存。
- 是否做了
Gzip压缩。 - 是否有重定向。
3.1 数据缓存
- 对于后端来说,某一些特定的数据实时计算需要消耗大量的时间,比如排行榜数据,我们可以做
T+1数据显示,也就是只显示前一天的数据,我们用定时任务把这些数据计算出来并缓存,然后取的时候不用实时计算速度就很快。 - 在前端层面,我们可以借助
Service Worker的数据接口缓存能力,Service Worker本质上是一个请求代理层,可以拦截和处理网络数据请求。 - 对于静态资源来说,我们可以使用
CDN加速,它的基本思路是在各个地方放置缓存节点服务器,构造出一个智能虚拟网络,然后采用就近访问的原则,把用户的请求导向离用户最近的服务节点上,。
3.2 Gzip 压缩
- 对于静态资源来说,可以全局在
nginx上全局开启gzip压缩。 - 对于接口数据来说,可以利用一些中间件或者自己手动实现
gzip数据压缩,配合请求头accept-encoding和响应头去content-encoding去实现。
3.3 重定向
- 重定向指的是当网站资源地址发生变化后,程序会自动将请求导向另一个页面的过程。
- 重定向主要包括
服务端 301、302 重定向、meta 标签实现的重定向、执行 JavaScript 通过 window.location 实现的重定向。 - 重定向会重新走一遍
DNS 查询 + TCP 连接 + TLS 协商 + 新的 HTTP 请求过程,用户需要耗费更多的时间才能看到最终的页面内容,严重影响前端性能。
4、客户端页面渲染阶段
当客户端拿到服务器返回的 HTML 内容后,就会开始进入页面解析和渲染阶段,它主要包括以下流程:
- 构建 DOM 树 :浏览器是无法理解和使用 HTML,所以需要把 HTML 转换为浏览器能够理解的结构,即
DOM 树。 - 样式计算 :将 CSS 的样式来源中(包括 link 标签、style 标记内书写的 CSS 以及元素的 style 属性中内嵌的 CSS),按照优先级整合成浏览器可以理解的结构,即
styleSheets,然后计算出 DOM 节点中每个元素的具体样式。 - 布局阶段:计算出 DOM 树中可见元素的几何位置。
- 分层 :在渲染页面之前,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree),比如 3D 变换、页面滚动、或者使用了
z-index的元素。 - 绘制:生成绘制指令,然后按照顺序组成一个待绘制列表。
- 光栅化:将上一步骤的待绘制列表,提交给合成线程。合成线程会把图层划分为图块,并按照视口附近的图块来优先生成位图,光栅化就是把图块生成位图的过程。
- 合成和显示:当所有的图块都被光栅化之后,合成线程会提交给浏览器进程来绘制图块,浏览器会把页面内容绘制到内存中,最后显示在页面上。
构建 DOM 树的瓶颈点:
- HTML 标签是否语义化以及标签嵌套是否合理 :当我们书写的 HTML 标签不符合语义化标准时,浏览器需要花更多的时间去解析各个 DOM 标签的含义,同时也会使 页面的 SEO 变差。比如 br 没写结束标签,表格嵌套不标准,标签层次结构复杂等,浏览器遇到这些情况时,需要进行语法纠错,导致页面总的解析和渲染时间变长。
- DOM 节点数量 :这个很好理解,DOM 节点越多,构建 DOM 树的时间越长,所以我们可以使用懒加载或者虚拟列表等手段减少 DOM 节点个数。
- script 加载时机 :
<script>标签中的JavaScript可以获取并修改 DOM,所以在解析到<script>标签后,DOM树的构建会被暂停,所以能延迟加载,就使用defer和async等属性异步加载,不阻塞 DOM 的解析。
CSS 样式计算中的瓶颈点:
- CSS 加载性能:是否做了公共样式抽离?是否删除了多余的 CSS?是否合理利用了内嵌CSS?
- CSS 选择器性能:是否滥用了通配符选择器?选择层级是否嵌套过深?
- CSS 属性性能 :是否使用了复杂或者不必要的 CSS 属性?是否滥用了
!important? - CSS 动画性能 :是否使用了
transform、will-change、requestAnimationFrame()等优化了动画? - CSS 渲染性能 :是否使用了
class合并DOM的修改?是否让DOM元素脱离文档流?
关于 CSS 的性能优化,有兴趣详细了解的话可查看我之前写的 前端性能优化之CSS篇。
布局中的瓶颈点:
- 如果在页面渲染过程运行时修改了一个元素的属性,这时候就会触发浏览器的
重排或者重绘,增加布局时间。 - 每次布局都需要计算整个 DOM 的几何位置,如果 DOM 节点很多,那么会花费很长的时间。
小结
页面加载主要分为 3 个大的阶段,包括客户端请求、服务端处理请求和客户端渲染阶段:
- 客户端请求的瓶颈点:查询本地缓存、DNS 解析、TCP 连接和 HTTP 请求阶段。
- 服务端处理请求阶段的瓶颈点:数据缓存、Gzip 压缩和重定向。
- 客户端页面渲染阶段的瓶颈点:构建 DOM 树阶段、CSS 样式计算阶段和布局阶段。