Skip to content

前端性能优化:从输入 URL 到页面可交互的完整链路

前端性能优化不要按“知识点清单”去背,而要按浏览器真实执行链路去理解。

用户输入一个 URL 之后,性能问题并不是随机出现的,而是会依次出现在下面这些阶段:

  1. 浏览器接管导航,检查缓存、Service Worker、是否需要发起网络请求
  2. DNS 解析、TCP/QUIC 建连、TLS 握手、HTTP 请求发送
  3. 服务端处理并返回 HTML,浏览器拿到首字节
  4. 浏览器下载并解析 HTML,发现 CSS / JS / 图片 / 字体等资源
  5. 构建 DOM、CSSOM、Render Tree,进行 Layout、Paint、Composite
  6. 执行 JavaScript、框架初始化、Hydration,页面变得可交互
  7. 页面运行期间响应滚动、点击、动画、列表渲染和数据更新

所以性能优化的逻辑主线也应该是:

  • 让资源更早到达
  • 让首屏更早可见
  • 让页面更早可交互
  • 让运行时更流畅
  • 让资源占用更可控

1. 先建立性能优化总视角

1.1 性能优化到底在优化什么

从用户视角看,性能优化主要是在优化四件事:

  • 页面多久能看到内容
  • 页面多久能操作
  • 操作时会不会卡
  • 页面会不会乱跳、崩溃、耗电、占内存

从技术视角看,前端性能优化可以归为两大类:

  • 时间优化:减少等待、下载、解析、执行、渲染的耗时
  • 空间优化:减少 CPU、内存、缓存、本地存储和包体积的占用

1.2 核心指标要和链路对应起来、

指标关注点对应阶段
TTFB (Time to First Byte)首字节返回是否快网络建链、服务端处理
FCP (First Contentful Paint)首次内容是否尽快出现HTML/CSS 到达、首屏渲染
LCP (Largest Contentful Paint)最大内容何时出现关键资源加载、首屏渲染
CLS (Cumulative Layout Shift)页面是否乱跳布局稳定性、图片字体尺寸
INP (Interaction to Next Paint)点击输入响应是否流畅JS 执行、主线程负载
TBT (Total Blocking Time)主线程是否长期阻塞JS 长任务、Hydration、重计算

> [!important] > 面试里不要只说“减少请求、压缩资源、懒加载”。 > 更好的回答方式是:先说瓶颈处在哪个阶段,再说对应优化手段影响的是 TTFB / FCP / LCP / CLS / INP / TBT 中的哪一个。

1.3 一句话总纲

前端性能优化的完整逻辑链路是:

输入 URL -> 更快建链 -> 更快拿到 HTML -> 更快发现关键资源 -> 更快完成首屏渲染 -> 更快进入可交互状态 -> 更平稳地运行页面

2. 第 1 段:输入 URL 后,浏览器首先做了什么

2.1 发生了什么

浏览器接到一个 URL 后,通常会先做这些事:

  • 解析 URL,判断协议、域名、端口、路径
  • 检查是否命中浏览器缓存
  • 检查是否被 Service Worker 拦截
  • 检查是否已有可复用连接
  • 决定是否真正发起网络请求

这个阶段看起来不显眼,但很多“明明资源不大,为什么还慢”的问题,其实从这里就开始了。

2.2 这个阶段的性能瓶颈

  • 缓存策略设计不合理,导致静态资源重复请求
  • HTML 不可缓存,每次都必须重新请求
  • 第三方资源域名过多,浏览器一开始就要建立很多连接
  • 没有提前告诉浏览器关键域名和关键资源

2.3 对应优化手段

1. 用好浏览器缓存

对静态资源采用强缓存,对 HTML 和经常变化的数据采用协商缓存。

  • 静态资源:Cache-Control: public, max-age=31536000, immutable
  • 文件名使用 hash,内容变化才换 URL
  • HTML 通常不做长时间强缓存,避免用户拿到旧壳

典型组合是:

  • index.html:协商缓存或短缓存
  • main.[hash].js / css / image:强缓存 + hash

2. 合理使用 Service Worker

Service Worker 可以拦截请求并命中本地缓存,适合:

  • 离线可用
  • 弱网优化
  • 静态资源二次访问加速

但不要把它当成“无脑缓存一切”的方案,否则版本更新、缓存失效和数据一致性会变得复杂。

3. 提前告诉浏览器关键域名

html
<link rel="dns-prefetch" href="https//cdn.example.com">
<link rel="preconnect" href="https://api.example.com" crossorigin>
  • dns-prefetch:提前做 DNS 解析
  • preconnect:更进一步,提前做 DNS + TCP/TLS

适用于:

  • 首屏一定会访问的 CDN
  • 必定发请求的 API 域名
  • 字体、图片等关键静态资源域名

2.4 常见误区

  • dns-prefetch 不等于 preconnect
  • 缓存命中率越高越好,这句话不完全对,关键是版本可控
  • Service Worker 不是纯收益,它会引入额外缓存治理成本

3. 第 2 段:DNS、TCP/TLS、HTTP 建链阶段怎么优化

3.1 发生了什么

如果浏览器最终决定要发请求,就会进入真正的网络建链阶段:

  1. DNS 解析,找到目标 IP
  2. 建立 TCP 或 QUIC 连接
  3. TLS 握手,协商安全连接
  4. 发送 HTTP 请求
  5. 等待服务端响应

这一段主要影响 TTFB,同时也间接影响 FCPLCP

3.2 这个阶段的性能瓶颈

  • DNS 解析慢
  • 跨地域访问源站
  • TLS 握手慢
  • 多次重定向
  • 域名太多,连接建立成本高
  • 资源全在源站,没有走 CDN

3.3 对应优化手段

1. 使用 CDN,把资源拉近用户

CDN 是网络优化里最通用、最有效的手段之一。

适合放到 CDN 的资源:

  • JS
  • CSS
  • 图片
  • 字体
  • 视频
  • 公开静态 JSON

CDN 的收益主要体现在:

  • 降低用户到资源的物理距离
  • 复用边缘缓存,减少回源
  • 降低源站压力

2. 减少重定向

比如:

  • http -> https
  • example.com -> www.example.com
  • 多次 301 / 302 跳转

重定向会直接增加链路长度。性能优化里,能少一次跳转就少一次。

3. 优先使用 HTTP/2 / HTTP/3

它们的主要收益:

  • 连接复用能力更强
  • 多路复用更好
  • 头部压缩更高效
  • HTTP/3 在弱网和丢包场景下表现通常更稳定

> [!tip] > HTTP/2/3 的核心价值不是“更高级的协议”本身,而是让同一连接上的资源并发和传输效率更高,从而降低首屏关键资源的等待时间。

4. 打开 Brotli / Gzip 压缩

对文本资源压缩通常有非常稳定的收益:

  • HTML
  • CSS
  • JS
  • SVG
  • JSON

一般优先级:

  • 优先 Brotli
  • 回退 Gzip

5. API 聚合和 BFF

如果首屏需要 5 个接口,前端串行发请求会非常慢。可以考虑:

  • BFF 聚合首屏接口
  • 服务端拼装首屏数据
  • SSR 阶段提前拉取关键数据

这类优化本质上是在缩短关键请求链。

3.4 一个需要纠正的老知识点

域名发散不是通用最优解

在 HTTP/1.1 时代,域名发散可以增加并发连接数,所以曾经是有效手段。

但在 HTTP/2 / HTTP/3 时代,大多数场景更推荐域名收敛,原因是:

  • 多路复用让单连接承载能力变强
  • 域名越多,DNS/TLS 成本越高
  • 连接和证书治理也更复杂

所以更准确的说法是:

  • HTTP/1.1 时代:域名发散有一定价值
  • HTTP/2/3 时代:优先考虑域名收敛和连接复用

4. 第 3 段:服务端返回 HTML 之前,性能问题在哪

4.1 发生了什么

用户并不关心服务端用了什么框架,但服务端处理慢,前端一定会慢。

从请求发出到浏览器收到 HTML 首字节,中间通常包括:

  • CDN 是否命中
  • 网关和反向代理处理
  • 服务端模板渲染
  • SSR 数据拉取
  • 数据库和下游服务访问

这一段直接决定 TTFB

4.2 这个阶段的性能瓶颈

  • 服务端没有缓存
  • SSR 阶段串行查多个接口
  • 后端接口过慢
  • HTML 模板过重
  • 首屏接口返回数据量过大

4.3 对应优化手段

1. 提升首字节速度

典型手段包括:

  • 页面级缓存
  • 片段缓存
  • 接口缓存
  • 静态化输出
  • 边缘渲染

2. SSR / SSG / CSR 要按场景选

不是所有页面都要 SSR。

模式优点缺点适用场景
CSR前后端分离简单首屏慢、SEO 弱后台系统、强交互应用
SSR首屏更快、SEO 好服务端成本更高、Hydration 成本存在电商、营销、内容页
SSG最快、最稳、最适合缓存不适合强实时内容博客、文档、静态内容页

3. 流式返回 HTML

如果页面采用 SSR,可以考虑流式输出,让浏览器更早开始解析 HTML,而不是等整页全部渲染完再一次性返回

这类优化的核心思想是:

  • 先把关键壳子返回给浏览器
  • 再陆续返回后续内容

4. 减少首屏数据体积

不要在首屏就把“用户看不到、暂时用不到”的数据全部塞进去。

策略包括:

  • 首屏只返回首屏数据
  • 分页和延迟拉取列表
  • 把推荐、评论、二级模块延后请求

> [!warning] > 骨架屏是体验优化,不等于真实性能优化。 > 如果骨架屏出现了很快,但真实内容很久才出来,指标和体验仍然可能很差。

5. 第 4 段:HTML 下载与解析阶段,为什么会白屏

5.1 发生了什么

浏览器拿到 HTML 后,不会等整个文件下载完才开始工作,而是边下边解析。

这一阶段会同时发生几件事:

  • HTML Parser 解析标签,构建 DOM
  • Preload Scanner 提前扫描后续资源
  • 发现 CSS、JS、图片、字体并发起请求

这就是首屏性能里最关键的一段,因为浏览器终于开始“找资源”和“准备渲染”了。

5.2 这个阶段的性能瓶颈

  • HTML 过大
  • 首屏 DOM 结构太重
  • 关键 CSS 发现太晚
  • 关键图片发现太晚
  • 同步脚本阻塞 HTML 解析
  • 第三方脚本抢占主线程

5.3 对应优化手段

1. 减少 HTML 体积和首屏 DOM 数量

  • 不要把大量隐藏内容直接输出到首屏 HTML
  • 避免首屏渲染几十层嵌套容器
  • 超长列表不要首屏全量渲染

2. 让关键 CSS 尽早到达

首屏 CSS 直接影响 FCPLCP。 可选手段:

  • 提取 Critical CSS
  • 首屏关键样式内联
  • 非关键 CSS 延后加载
html
<style>
  /* 首屏关键样式 */
</style>
<link rel="stylesheet" href="/styles/app.css">

3. 正确使用 asyncdefer

html
<script src="/a.js" async></script>
<script src="/b.js" defer></script>

区别:

  • async:下载完成就立即执行,顺序不保证,适合独立脚本
  • defer:HTML 解析完成后按顺序执行,适合业务脚本

面试里要答准一点:

  • 普通同步脚本会阻塞 HTML Parser
  • defer 一般比 async 更适合主业务脚本
  • 第三方统计类脚本更适合 async

4. 预加载关键资源

html
<link rel="preload" href="/main.css" as="style">
<link rel="preload" href="/hero.webp" as="image">
<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin>

适合预加载的资源:

  • 首屏样式
  • 首屏 Hero 图
  • 首屏字体
  • 关键脚本

5. 字体优化

字体文件经常被忽略,但它经常直接影响首屏文本显示。

优化策略:

  • 使用 woff2
  • 做字体子集
  • 只加载必要字重
  • 设置 font-display: swap
css
@font-face {
  font-family: 'MyFont';
  src: url('/font.woff2') format('woff2');
  font-display: swap;
}

5.4 常见误区

  • CSS 会阻塞渲染,但更准确地说,是它会影响首次渲染时机
  • 同步 JS 会阻塞 HTML 解析,这是首屏最常见的阻塞点之一
  • preload 是当前页面高优先级资源,prefetch 更偏“下一跳可能会用到”

6. 第 5 段:从 DOM/CSSOM 到真正渲染出来,浏览器做了什么

6.1 浏览器渲染主流程

当浏览器已经拿到 HTML 和 CSS 后,会继续进入渲染流水线:

  1. 构建 DOM Tree
  2. 构建 CSSOM Tree
  3. 合成 Render Tree
  4. Layout,计算元素几何信息
  5. Paint,把元素绘制为像素
  6. Composite,把图层合成到屏幕

这一段主要影响:

  • FCP
  • LCP
  • CLS

6.2 这个阶段的性能瓶颈

  • DOM 太大
  • CSS 太复杂
  • 大图解码慢
  • 图片、广告、字体导致布局抖动
  • JS 一边改 DOM,一边强制读取布局信息
  • 动画使用了会触发 Layout 的属性

6.3 对应优化手段

1. 控制 DOM 规模

  • 减少不必要的包裹层
  • 列表做虚拟滚动
  • 折叠区域不要一开始就全部渲染

DOM 越大,样式计算、布局计算和回流成本通常越高。

2. 减少回流和布局抖动

典型高成本操作包括:

  • width / height / top / left / margin
  • 频繁插入删除节点
  • 在循环里反复读写 offsetWidth / getBoundingClientRect()

错误示例:

js
for (let i = 0; i < 100; i++) {
  el.style.width = `${el.offsetWidth + 1}px`;
}

优化思路:

  • 读写分离
  • 批量更新 DOM
  • 使用 class 一次性切换样式
  • 使用 DocumentFragment

3. 预留尺寸,降低 CLS

页面布局跳动最常见的来源:

  • 图片没有宽高
  • 字体切换导致文字重排
  • 广告位、异步卡片没有占位
  • 动态插入内容把页面挤下去

优化手段:

  • 图片和视频设置 widthheightaspect-ratio
  • 广告、卡片、骨架屏预留空间
  • 字体使用 font-display: swap

4. 动画优先用 transformopacity

尽量避免:

  • top
  • left
  • width
  • height

推荐:

css
.card {
  transform: translateY(10px);
  opacity: 1;
}

这类属性通常更容易走合成层,能减少布局和绘制成本。

5. 合理使用 will-change

css
.animating {
  will-change: transform;
}

它的意义是提前告诉浏览器某个属性要变化,让浏览器做预优化。

但不要滥用,因为滥用会增加内存和图层管理成本。

6. 利用现代 CSS 能力隔离渲染成本

比如:

  • contain
  • content-visibility: auto

适合大块、可独立渲染的区域:

  • 长列表
  • 页签内容
  • 折叠面板

7. 第 6 段:JavaScript 执行、Hydration 与可交互时间

7.1 发生了什么

即使页面已经“看起来出来了”,也不代表用户已经能顺畅交互。

浏览器还要继续做很多事:

  • 下载 JS
  • 解析 JS
  • 编译 JS
  • 执行业务代码
  • 框架初始化
  • 绑定事件
  • Hydration 或重新接管 DOM

这一段主要影响:

  • TBT
  • INP
  • 可交互时间

7.2 这个阶段的性能瓶颈

  • JS 包太大
  • 首屏执行逻辑太多
  • 框架 Hydration 成本高
  • 第三方脚本过多
  • 主线程长任务过多
  • 重计算、复杂序列化、复杂 diff 都堆在首屏

7.3 对应优化手段

1. 降低首屏 JS 体积

这是最核心的优化方向之一。

主要手段:

  • 路由级代码分割
  • 组件级按需加载
  • Tree Shaking
  • 精简依赖
  • 减少 polyfill
js
const Detail = () => import('./Detail');

2. 不要把并发写成串行

错误写法:

js
const user = await getUser();
const order = await getOrder();

如果两者无依赖,应该并发:

js
const [user, order] = await Promise.all([getUser(), getOrder()]);

这类问题本质上也属于性能优化。

3. 把长任务拆开

如果一段 JS 连续执行几百毫秒,主线程就会卡住,用户点击没有响应。

可以考虑:

  • 分片执行任务
  • 延后非关键逻辑
  • 把非首屏逻辑放到空闲时机
  • 用 Web Worker 承接重计算

4. 重计算放进 Web Worker

适合的场景:

  • 大数据计算
  • 大文件解析
  • 图像处理
  • 富文本转换
js
const worker = new Worker('/worker.js');
worker.postMessage(data);
worker.onmessage = (e) => {
  console.log(e.data);
};

5. 延迟第三方脚本

很多线上页面真正拖慢性能的不是主业务代码,而是:

  • 埋点
  • 广告
  • 客服
  • A/B 平台
  • 监控 SDK

策略包括:

  • 延迟加载
  • 条件加载
  • 空闲时加载
  • 非首屏不加载

7.4 一个很容易答错的问题

SSR 不一定让页面更快可交互

SSR 往往可以让页面更早可见,但如果:

  • 首屏 HTML 很大
  • Hydration 很重
  • JS 包依然很大

那么用户虽然更早看到页面,却仍然会遇到“能看不能点”的问题。

所以:

  • SSR 更偏优化 FCP / LCP
  • Hydration 和主线程执行更偏影响可交互时间和 INP

8. 第 7 段:首屏之后,运行时性能怎么优化

8.1 运行时性能优化关注什么

首屏之后,性能问题主要来自三类场景:

  • 滚动卡顿
  • 输入和点击延迟
  • 列表/动画/图表更新掉帧

8.2 对应优化手段

1. 长列表使用虚拟滚动

不要一次性渲染几千条 DOM。

适合:

  • 聊天记录
  • 商品列表
  • 日志表格

2. 高频事件做节流和防抖

适合:

  • scroll
  • resize
  • input
  • mousemove
js
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

3. 滚动监听尽量使用被动事件

js
window.addEventListener('scroll', onScroll, { passive: true });

它能告诉浏览器这个监听器不会 preventDefault,有助于滚动更流畅。

4. 图片按需加载

html

对于更复杂场景,也可以用 IntersectionObserver

5. 动画控制在合成层

还是那条原则:

  • 优先 transform
  • 优先 opacity
  • 避免频繁触发布局

6. 清理定时器、监听器和引用

很多“越用越卡”的问题,最后查下来是内存泄漏。

常见来源:

  • 没有清理的 setInterval
  • 没有解绑的事件监听
  • 长生命周期闭包引用大对象
  • 页面切换后仍存活的观察器

9. 把工程化优化放回链路里看

现成文章最常见的问题,是把一切都叫“前端性能优化”。

但实际上要区分两件事:

  • 影响用户访问性能
  • 影响开发或构建性能

这两者有关,但不是一回事。

9.1 真正影响用户访问性能的工程化手段

这些优化会直接影响 URL 到渲染链路:

  • 代码分割
  • Tree Shaking
  • 按需加载
  • 资源压缩
  • 图片转 WebP / AVIF
  • 字体子集
  • hash 命名配合强缓存
  • SSR / SSG / 流式渲染
  • 首屏资源预加载

它们影响的是:

  • 下载体积
  • 解析体积
  • 执行体积
  • 缓存命中率

9.2 更偏构建速度的工程化手段

下面这些更偏“让你打包更快”,不是直接让用户页面更快:

  • resolve.alias
  • resolve.extensions
  • loader include/exclude
  • 构建缓存
  • 多线程构建
  • SourceMap 策略

这些优化很重要,但它们主要优化的是:

  • 本地开发体验
  • CI 构建速度
  • 发布效率

> [!tip] > 面试里如果被问到 aliasextensionsloader 范围,最好明确补一句: > 这类优化主要影响构建性能,不是直接影响用户线上页面加载速度。

9.3 代码分割是用户性能和工程化的交叉点

比如 splitChunks、路由懒加载、组件懒加载,既是构建产物设计问题,也是访问性能问题。

它们的目标是:

  • 降低首包体积
  • 让非首屏代码延后下载
  • 减少首屏解析和执行压力

所以这类优化值得重点掌握。

10. 空间与资源占用优化

前端性能优化不是只看时间,还要看空间占用。

10.1 主要关注哪些资源

  • JS 堆内存
  • 图片和视频解码内存
  • 浏览器缓存
  • Service Worker Cache
  • IndexedDB / localStorage
  • GPU 图层占用

10.2 常见优化方向

1. 控制缓存大小和生命周期

  • 不是缓存越多越好
  • 需要版本治理和淘汰策略
  • Service Worker 缓存要及时清理旧版本

2. 避免内存泄漏

  • 清理事件监听
  • 清理定时器
  • 清理观察器
  • 页面卸载时释放大对象引用

3. 图片不要只看下载体积

图片下载小,不代表解码和渲染成本也低。

要同时关注:

  • 资源体积
  • 图片尺寸
  • 解码成本
  • 是否超出真实展示尺寸

4. 谨慎提升图层

will-changetransform: translateZ(0) 虽然能提升动画表现,但也会增加内存和图层管理成本,不能全站乱加。

11. 用一句面试回答串起整篇内容

如果让我从浏览器输入 URL 到页面展示,分析前端性能优化,我会按浏览器真实执行链路来讲。

第一段是导航和网络建链,重点是缓存、DNS、CDN、HTTP/2/3、压缩和减少重定向,核心目标是优化 TTFB
第二段是HTML 返回后的资源发现阶段,重点是让关键 CSS、关键图片、关键字体和关键脚本更早被浏览器发现和加载,减少阻塞解析和阻塞渲染,核心目标是优化 FCPLCP
第三段是渲染流水线,重点是减少 DOM 规模、减少回流重绘、控制布局抖动、使用合成友好的动画属性,核心目标是优化 LCPCLS
第四段是JavaScript 执行和可交互阶段,重点是降低首屏 JS 体积、拆分长任务、延迟非关键脚本、把重计算移出主线程,核心目标是优化 TBTINP
最后再补充运行时性能和资源占用优化,比如虚拟列表、节流防抖、内存泄漏治理、缓存治理和图层治理,这样整条性能优化链路就完整了。

12. 一张总表收口

阶段发生了什么典型瓶颈核心优化
导航开始URL 解析、缓存判断、SW 拦截缓存策略差、关键域名未预热强缓存、协商缓存、dns-prefetchpreconnect
网络建链DNS、TCP/QUIC、TLS、HTTP建链慢、跨地域、重定向CDN、HTTP/2/3、Brotli、减少跳转
服务端处理SSR、模板渲染、接口聚合TTFB 高、首屏数据太重缓存、BFF、流式 SSR、静态化
HTML 解析构建 DOM、发现资源同步 JS 阻塞、关键资源发现晚Critical CSS、deferpreload、减少 HTML 体积
渲染流水线CSSOM、Layout、Paint、Composite回流重绘、CLS、大 DOM预留尺寸、批量 DOM 更新、transform/opacity
JS 执行与交互下载、解析、执行、HydrationJS 包大、长任务、第三方脚本重代码分割、Tree Shaking、Worker、延迟非关键逻辑
运行时滚动、输入、动画、列表更新掉帧、响应慢、内存泄漏虚拟滚动、节流防抖、被动监听、清理引用

13. 最后一句

前端性能优化不是“把所有优化点背一遍”,而是要能把每个优化手段放回浏览器的真实执行链路里,回答清楚:

  • 它优化的是哪一段
  • 它影响的是哪个指标
  • 它解决的核心瓶颈是什么
  • 它有没有副作用和适用边界 这才是“从输入 URL 到完成解析与渲染”的完整性能优化逻辑。

评论区

欢迎留言、补充或勘误。

xiaoba.blog