关于前端性能优化

91 阅读15分钟
  • 留存率:慢速网站会导致用户流失,53%的移动用户会放弃加载时间超过3秒的页面
  • 转化率:页面加载速度与转化率呈负相关,每延迟1秒,转化率可能下降7%
  • 品牌形象:性能卓越的产品会给用户留下专业、可靠的印象
  • 用户满意度:流畅的体验会提升用户满意度和使用黏性
  1. 从输入 URL 到页面加载完成,发生了什么?

暂时无法在飞书文档外展示此内容

  1. 构建请求

根据 URL 构建请求

  1. 重定向

类型对比表:

特性永久重定向(301)临时重定向(302/307)
HTTP状态码301302(旧)/ 307(新)
SEO影响旧 URL 权重转移至新 URL保留旧 URL 权重
浏览器缓存长期缓存不缓存或短期缓存
用途永久性变更(如域名迁移)临时性跳转(如维护、测试)
请求方法保留可能变为 GET(历史实现)307 保留原始请求方法
  1. 查询缓存

浏览器缓存 → 系统缓存 → 路由缓存 → DNS 缓存查找

  1. DNS 解析

根域名 → 顶级域名 → 权威域名递归查找对应 IP 地址

  1. TCP 连接

传输层 TCP 三次握手建立连接

  1. HTTP 请求

客户端向服务端发起 HTTP 请求报文

服务端处理后返回响应报文(HTML/JS/CSS 等资源)

  1. 浏览器解析资源

解析 HTML → 生成 DOM Tree ↓ 解析 CSS → 生成 CSSOM Tree ↓ 合并生成 Render Tree ↓ Layout(布局)→ Compositing(合成)→ Painting(绘制)

  1. 页面呈现

哪些因素会影响性能

我们再从宏观角度来看,页面通常可分为 3 个阶段:初始化加载阶段、运行时交互阶段、关闭卸载阶段。我们从这 3 个阶段,同时结合上述页面初始化加载经历 8 大过程,可分析出影响页面性能的因素:

  • 加载阶段: 从发起请求到渲染出完整的页面过程,影响这一阶段的因素有网络、HTML、图片、CSS 和 JS 脚本
  • 交互阶段: 从页面加载后用户交互(比如:点击、滚动页面等)的过程,影响这一阶段的因素主要是网络、JS 脚本
  • 关闭阶段: 用户发出关闭页面做一些清理卸载工作,影响这一阶段的因素主要是 JS 脚本
  1. 问题分析和定位

当我们碰到页面性能、构建效率慢,我们首先需要分析定位具体问题。我们常用的参考性能指标和可

视化问题分析⼯具主要有如下几种:

用户感知的指标

  • 感知的加载速度: 网页加载并将其所有视觉元素呈现到屏幕的速度
  • 加载响应速度: 网页加载和执行任何必需的 JavaScript 代码的速度,以便让组件快速响应用户互动
  • 运行时响应速度: 网页加载后,网页能够以多快的速度响应用户互动
  • 视觉稳定性: 网页上的元素是否会以用户意想不到且可能会干扰互动的方式发生变化,比如图片加载后出现偏移闪动
  • 流畅性: 过渡和动画是否以一致的帧率渲染并流畅呈现

著名 2-5-8 原则:

  • 当用户能够在 2 秒以内得到响应时,会感觉系统的响应很快;
  • 当用户在 2-5 秒之间得到响应时,会感觉系统的响应速度还可以;
  • 当用户在 5-8 秒以内得到响应时,会感觉系统的响应速度很慢,但是还可以接受;
  • 而当用户在超过 8 秒后仍然无法得到响应时,会感觉系统糟透了,或者认为系统已经失去响应,而选择离开这个 Web 站点,或者发起第二次请求。

首屏加载时间First Contentful Paint(FCP)

  • 定义:从页面开始加载到页面渲染出第一片有意义内容的时间点

首次内容绘制时间,指浏览器首次绘制页面中至少一个文本、图像、非白色背景色的canvas/svg元素等的时间,代表页面首屏加载的时间点。

为了提供良好的用户体验,网站应尽量将首次有意义的绘制时间控制在 1.8 秒或更短的时间。为确保大多数用户都能达到此目标值,一个合适的衡量阈值是网页加载时间的第 75 个百分位数,并按移动设备和桌面设备进行细分。

首次绘制时间First Paint(FP)

首次绘制时间,指浏览器首次在屏幕上渲染像素的时间,代表页面开始渲染的时间点。

最大内容绘制时间Largest Contentful Paint(LCP)

最大内容绘制时间,指页面上最大的可见元素(文本、图像、视频等)绘制完成的时间,代表用户视觉上感知到页面加载完成的时间点。

最大内容元素有哪些?

  • 元素
  • 内的 元素
  • 包含海报图片的 元素
  • 为自动播放 元素而绘制的第一帧
  • 动画图片(如 GIF 动画)的第一帧
  • 包含文本节点或其他内嵌级别文本元素的子项的块级元素

用户可交互时间Time to Interactive(TTI)

可交互时间,指页面加载完成并且用户能够与页面进行交互的时间,代表用户可以开始操作页面的时间点。

页面总阻塞时间Total Blocking Time (TBT)

页面上出现阻塞的时间,指在页面变得完全交互之前,用户与页面上的元素交互时出现阻塞的时间。TBT应该尽可能小,通常应该在300毫秒以内。

搜索引擎优化Search Engine Optimization (SEO)

网站在搜索引擎中的排名和可见性。评分范围从0到100,100分表示网站符合所有SEO最佳实践。

FMP:

与 FP/FCP /LCP 相比, FMP 的采集相对比较复杂,它需要通过算法计算得出,而业界并没有统一的算法。不过比较认可的一个计算 FMP 的方式是「认定页面在加载和渲染过程中最大布局变动之后的那个绘制时间即为当前页面的 FMP 」。

由于在页面渲染过程中,「DOM 结构变化的时间点」和与之对应的「渲染的时间点」近似相同,所以字节内部计算 FMP 的方式是:计算出 DOM 结构变化最剧烈的时间点,即为 FMP。具体步骤为:

1.通过 MutationObserver 监听每一次页面整体的 DOM 变化,触发 MutationObserver 的回调

2.在回调计算出当前 DOM 树的分数

3.在结算时,通过对比得出分数变化最剧烈的时刻,即为 FMP

  1. 网络层面优化

  1. 使用缓存

使用缓存可以减少网络 IO 消耗,大幅提高访问速度。但开发在 “网络层面:缓存命中” 可做的并不多,能介入优化的主要是 “浏览器缓存” 这一阶段,下面我们具体来看看优化方案有哪些?

  1. 浏览器缓存

大多时候,大家将 “浏览器缓存” 简单地理解成 “HTTP 缓存”,实际上它分为 4 个方面,按获取资源的请求优先级排列如下:

  1. Memory Cache(内存缓存)
  2. Service Worker Cache(离线缓存)
  3. HTTP Cache(HTTP 缓存)
  4. Push Cache(HTTP2 推送缓存)

Push Cache 是 HTTP2 的新特性,升级 HTTP2 版本即可,我们接下来更多还是从 “Memory Cache、离线缓存、HTTP 缓存”3 个方面来看如何使用浏览器缓存,进而提升页面性能。

  1. HTTP缓存

状态码

  • 200:强缓存 Expires/Cache - Control 失效时,返回新资源文件
  • 200 (from disk cache) Expires/Cache - Control 两者都存在且有效,Cache - Control 优先 Expires 时,浏览器从本地获取资源成功。
  • 200 (from memory cache)
  • 304 (Not Modified) 协商缓存 Last - modified/ETag 有效,则服务端返回该状态码。

Cache - Control

  • no - cache 存储在本地缓存中,只是在与服务器进行新鲜度再验证之前,缓存无法使用。
  • no - store 不缓存资源到本地
  • public 可被所有用户缓存,多用户进行共享,包括终端或 CDN 等中间代理服务器
  • private 仅能被浏览器客户端缓存,属于私有缓存,不允许中间代理服务器缓存相关资源

内存还是磁盘

通过 Cache - Control 头部间接引导缓存位置

  1. 高频访问的小资源→倾向于内存缓存

    1.  Cache - Control: max - age = 300, must - revalidate
      
  2. 低频访问的大资源→倾向于磁盘缓存

    1.  Cache - Control: public, max - age = 31536000, immutable
      

如何缓存

缓存启用的顺序可列举如下:

  1. Cache - Control— 请求服务器之前
  2. Expires— 请求服务器之前
  3. If - None - Match (ETag) — 请求服务器
  4. If - Modified - Since (Last - Modified) — 请求服务器

需要注意的是协商缓存需要配合强缓存使用,如果不启用强缓存那么协商缓存就失去了意义。大部分web 服务器都默认开启了协商缓存,而且是同时启用(Last-Modified、lf-Modified-Since)和(ETag、lf-None-Match)。

  1. Service Worker

本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器,可以拦截资源请求根据自己的逻辑做处理,比如可以拦截网络请求、缓存资源、离线访问等。

  1. 资源压缩

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
          },
        },
      }),
      new CssMinimizerPlugin(),
    ],
  },
};    

4. ### DNS优化

在 HTML 中优化 DNS 解析主要通过减少 DNS 查询次数和提前解析关键域名来提升页面加载速度。

以下是具体实现方法和最佳实践:

DNS 预取(DNS Prefetching)

通过 HTML 的 <link> 标签提示浏览器提前解析关键域名的 DNS,减少后续资源请求的延迟。

 <!-- 预解析关键域名 -->
<link rel="dns - prefetch" href="//cdn.example.com"/>
<link rel="dns - prefetch" href="//api.example.com"/>
<link rel="dns - prefetch" href="//fonts.googleapis.com"/>

适用场景:

  • 页面中引用了第三方资源(如 CDN、字体、分析脚本)。

注意事项:

  • 避免过度预解析,通常建议预解析 3 - 5 个关键域名
  • 现代浏览器(Chrome、Firefox、Edge)默认支持 dns - prefetch

预连接(Preconnect)

在 DNS 预取的基础上,进一步建立 TCP 连接和 TLS 握手,适用于高频访问的域名。

 <!-- 提前建⽴连接(DNS + TCP + TLS) -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>

适用场景:

高频请求的核心 API 域名。

静态资源 CDN 域名。

域名收敛

域名收敛(Domain Sharding 或 Domain Consolidation)是指将网站的资源(如静态文件、API 等)从多个域名合并到少数几个甚至单个域名的策略。这一策略在 HTTP/2 普及后逐渐被重视,其核心目标是通过减少域名数量优化网站性能、简化管理并提升安全性。以下是域名收敛的主要好处。

优化前(多域名):

<script src="https://static1.example.com/app.js"></script>
<link href="https://static2.example.com/style.css" rel="stylesheet"/>
<img src="https://static3.example.com/logo.png"/>

优化后(单域名 + CDN):

<script src="https://cdn.example.com/app.js"></script>
<link href="https://cdn.example.com/style.css" rel="stylesheet"/>
<img src="https://cdn.example.com/logo.png"/>

优势:

  • 减少 DNS 查询次数。
  • 利用 HTTP/2 多路复用特性,避免重复 TCP 握手。

减少 DNS 解析开销

问题: 每个新域名都需要独立的 DNS 解析,增加延迟(尤其移动端网络)。

收敛好处:

  • 减少域名数量 → 减少 DNS 查询次数。
  • 缩短整体解析时间,加速页面首次加载(尤其是低延迟网络环境)。

避免不必要的 TCP 连接和 TLS 握手

  • HTTP/1.1 的并发限制: 浏览器对同一域名的并发请求数有限(如 Chrome 默认 6 个),过去常通过多域名(如static1.example.comstatic2.example.com)突破限制。

  • HTTP/2 多路复用: HTTP/2 支持单连接多路复用,无需多域名即可并发传输资源。

  • 收敛好处:

    • 减少 TCP 连接数 → 降低连接建立和 TLS 握手开销。
    • 避免因过多域名导致的连接竞争和资源浪费。

适用场景与权衡

  • 推荐收敛的场景:

    • 使用 HTTP/2 或 HTTP/3 的现代网站。
    • 移动端或高延迟网络环境(需减少 DNS 和连接开销)。
    • 需简化运维和安全管理的企业级应用。
  • 需谨慎的场景:

    • 仍需支持 HTTP/1.1 的旧系统(可适度保留少量域名)。
    • 第三方资源(如广告、分析脚本)需隔离时。
  1. CDN

暂时无法在飞书文档外展示此内容

  1. CDN在整个架构中的位置:CDN位于SSR架构和服务端之间,是网络动静分离架构的重要组成部分。

  2. CDN的主要功能和作用:

    1. 接收来自SSR架构的请求
    2. 响应静态部分的内容
    3. 处理流式返回的动态内容
    4. 与服务端进行交互,获取服务端返回的动态部分
  3. CDN在性能优化中的具体体现:

    1. 静态资源缓存:CDN可以缓存静态部分,减少对服务端的请求
    2. 动态内容处理:CDN能够处理流式返回的动态内容,提高响应速度
    3. 网络分离:通过动静分离,优化了网络传输效率
    4. 就近访问:CDN的分布式特性可以让用户就近获取内容
  4. 与其他组件的交互关系:

    1. 从SSR架构接收请求和响应静态部分
    2. 与服务端交互获取动态内容
    3. 支持流式返回机制,提高用户体验
  5. 在整体架构中的优化价值:

    1. 减轻服务端压力
    2. 提高内容分发效率
    3. 支持动静分离策略
    4. 改善用户访问体验

CDN在该网络动静分离架构中发挥着关键的性能优化作用:

核心功能:

  • 作为SSR架构与服务端之间的中间层,负责内容分发和缓存管理
  • 处理静态资源的缓存和分发,减少对服务端的直接请求
  • 支持流式返回机制,实现动态内容的高效传输
  1. 渲染层面优化

  • 尽可能减少资源个数
  • 尽可能减少资源体积大小
  1. 减少文件大小(压缩、精简)

  • 压缩处理 HTML,减小 HTML 体积

  • 精简 HTML:

    • 尽量减少 HTML 嵌套、iframe/table 使用(table 标签比其他 html 标签占用更多字节,导致下载时间长,占用服务器更多的流量资源)
    • 删除多余的空格、换行符、缩进和不必要注释
    • 删除冗余标签和属性
  1. DOM 优化

  • 控制 DOM 大小:

    • 合理的业务逻辑拆分
    • 先加载可视区,其他延迟加载(懒加载)
  • 减少 DOM 操作:尽可能对 DOM 操作统一逻辑处理,或是使用虚拟 DOM(借鉴 vue/react),再插入到真实 DOM(减少重排重绘)

  1. CSS 优化

减少资源请求个数

  • 合并多个 CSS 样式文件
  • 按需加载样式

减少文件大小

  • 压缩处理 CSS 文件

  • 位置放在 里,尽早地进行样式解析,构建 CSSOM 树

  • 简化 CSS 选择器(选择器优先级:!important > 内联 > id > class / 属性 / 伪类 > 标签 > 伪元素)

    • 尽可能减少样式层级数(选择器嵌套)
    • 少用标签选择器,尽量选择高优先级的 id/class/ 属性 / 伪类选择器代替
    • 少用通配符 *,只对需要修改样式的元素进行选择
    • 关注可继承属性,避免重复定义和匹配
  1. 图片加载优化

懒加载

图片列表一般采用懒加载进行按需加载,滚屏时当图片已出现在可视区域的时候进行加载。(有效地减轻服务器批量加载图片的压力)

思路

  • 添加自定义属性:给 img 标签添加data-src,值为图片的 url,同时不要设置 src 属性

  • 判断目标元素与视口的交叉状态:通过获取元素的getBoundingClientRect属性的 top 值和页面的clientHeight进行对比,如果 top 值小于clientHeight,则说明元素出现在可视区域之内

  • 设置真实的 src:当元素出现在可视区域内时,将真实的图片地址赋值给目标元素的 src 属性

IntersectionObserver

IntersectionObserver 接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

性能优化策略

  • 单例观察者 对同类元素使用同一观察者,减少内存开销:
const observer = new IntersectionObserver(callback);
document.querySelectorAll('.lazy').forEach(el => observer.observe(el));
  • 动态解除观察 触发后立即unobserve避免重复检测:
if (entry.isIntersecting) {
    doSomething(entry.target);
    observer.unobserve(entry.target);
}
  • 合理设置阈值

根据场景选择最佳触发时机:

  • 懒加载:threshold: 0.01 + rootMargin: '200px'
  • 精准曝光统计:threshold: 0.8
  • 批量处理元素 对长列表分批次观察,避免同时观察数千个元素:
const chunkSize = 50;
const elements = [...document.querySelectorAll('.list - item')];
elements.forEach((el, index) => {
    if (index % chunkSize === 0) {
        setTimeout(() => observeChunk(elements.slice(index, index + chunkSize)), 0);
    }
});
  • 预加载

预加载 preload,在大图片加载完成前先加载小的 loading,用于提升用户体验。(该优化思想不仅可以用于图片加载,也能用于异步请求、html 标签预加载)

// 创建 img 图片元素
const myImage = (function(){
    let imgNode = document.createElement('img');
    document.body.appendChild( imgNode );
    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    }
})()

/**
 * 预加载
 */
const preload = (function(){
    let img = new Image();
    img.onload = function() {
        myImage.setSrc(this.src); // this指向img
    }
    return {
        setImg: function(src) {
            myImage.setSrc('//img/loading.gif');
            img.src = src;
        }
    }
})()

preload.setImg('//img/bg_gaoqing.jpg')