Skip to content

浏览器与网络

说明

共 15 题,难度 ⭐ ~ ⭐⭐⭐,覆盖浏览器渲染、HTTP 协议、缓存策略、Web 安全、性能优化等前端必备的浏览器与网络知识。

1. 从输入 URL 到页面展示,发生了什么? ⭐⭐⭐

完整描述浏览器加载页面的全流程。

考察点:全链路理解

完整流程

1. URL 解析
   浏览器判断输入是 URL 还是搜索关键词
   → 解析协议、域名、端口、路径

2. DNS 解析
   浏览器缓存 → OS 缓存 → hosts 文件 → 路由器缓存
   → ISP DNS → 递归查询根域名服务器
   → 最终得到 IP 地址

3. TCP 连接
   三次握手建立连接
   → SYN → SYN+ACK → ACK
   如果是 HTTPS → 再进行 TLS 握手(交换证书、协商密钥)

4. 发送 HTTP 请求
   浏览器构建请求报文(方法、URL、Headers、Body)
   → 带上 Cookie 等信息

5. 服务器处理 & 返回响应
   服务器收到请求 → 处理业务逻辑 → 返回响应报文
   → 状态码 + Headers + Body

6. 浏览器解析与渲染
   ┌─── 关键渲染路径 ───┐
   │ HTML → DOM Tree     │
   │ CSS  → CSSOM Tree   │
   │ DOM + CSSOM          │
   │ → Render Tree        │
   │ → Layout(计算位置) │
   │ → Paint(绘制)      │
   │ → Composite(合成)  │
   └─────────────────────┘

7. JS 执行
   遇到 <script> → 阻塞解析(除非 async/defer)
   → JS 可能修改 DOM/CSSOM → 重新触发 Layout/Paint

8. 加载子资源
   图片、字体、CSS、JS 等子资源并行/按序加载

9. TCP 断开(四次挥手)
   FIN → ACK → FIN → ACK
   (HTTP/1.1 默认 keep-alive 复用连接)

每个阶段的优化方向

阶段优化手段
DNSDNS 预解析 <link rel="dns-prefetch">
TCPHTTP/2 多路复用、连接预建立 <link rel="preconnect">
请求资源合并、CDN、GZIP/Brotli 压缩
响应HTTP 缓存(强缓存/协商缓存)
解析CSS 放 head、JS 放 body 底部或 defer
渲染减少回流重绘、GPU 加速(transform)
资源懒加载、预加载 <link rel="preload">

追问延伸

  • DNS 的递归查询和迭代查询有什么区别?
  • preload / prefetch / preconnect / dns-prefetch 分别做什么?
  • HTTP/2 的多路复用为什么能解决队头阻塞?

2. 浏览器缓存策略?强缓存和协商缓存的区别? ⭐⭐⭐

完整说明 HTTP 缓存机制。

考察点:HTTP 缓存

缓存判断流程

浏览器发起请求

  ├── 有缓存? → 否 → 直接请求服务器

  └── 有缓存 → 检查强缓存

       ├── 未过期(Cache-Control / Expires)
       │   → 200 (from cache) → 直接使用 ✅ 不发请求

       └── 已过期 → 发起协商缓存请求

            ├── 服务器返回 304 Not Modified
            │   → 使用本地缓存 ✅

            └── 服务器返回 200 + 新内容
                → 更新缓存 → 使用新内容

强缓存

Header说明示例
Cache-ControlHTTP/1.1,优先级高max-age=31536000
ExpiresHTTP/1.0,绝对时间Thu, 01 Jan 2026 00:00:00 GMT
Cache-Control 常用指令:
  max-age=3600       → 缓存 1 小时
  no-cache           → 不用强缓存,每次都协商(不是不缓存!)
  no-store           → 完全不缓存
  public             → CDN 和浏览器都可缓存
  private            → 只有浏览器可缓存(如包含用户信息的页面)
  s-maxage=3600      → CDN 缓存时间(覆盖 max-age)
  must-revalidate    → 过期后必须重新验证
  immutable          → 永不变,连刷新都不验证(配合 hash 文件名)

协商缓存

请求 Header响应 Header比较方式
If-Modified-SinceLast-Modified最后修改时间
If-None-MatchETag内容哈希
ETag 优先级 > Last-Modified

Last-Modified 的缺点:
  - 精度只到秒(1 秒内多次修改无法区分)
  - 文件内容不变但修改时间变了(如 touch 命令)

ETag:
  - 基于内容生成的哈希值
  - 内容不变 → ETag 不变 → 304
  - 强 ETag: "abc123"(字节级一致)
  - 弱 ETag: W/"abc123"(语义一致)

实际配置策略

nginx
# HTML — 不缓存(每次协商)
location ~* \.html$ {
    add_header Cache-Control "no-cache";
}

# JS/CSS(带 hash)— 长期缓存
location ~* \.(js|css)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

# 图片 — 中等时长
location ~* \.(png|jpg|gif|svg)$ {
    add_header Cache-Control "public, max-age=86400";
}

# API — 不缓存
location /api/ {
    add_header Cache-Control "no-store";
}

追问延伸

  • 浏览器"强制刷新"(Ctrl+Shift+R) 和"普通刷新"(F5) 对缓存的影响?
  • Service Worker 的缓存和 HTTP 缓存是什么关系?(SW 优先级更高)
  • CDN 缓存和浏览器缓存的区别?CDN 如何更新缓存?

3. HTTP/1.1、HTTP/2、HTTP/3 的核心区别? ⭐⭐⭐

对比三代 HTTP 协议的演进。

考察点:HTTP 协议

对比

维度HTTP/1.1HTTP/2HTTP/3
传输层TCPTCPQUIC (UDP)
多路复用❌ 一个连接一个请求✅ 多路复用✅ 多路复用
队头阻塞✅ 有(应用层)🟡 TCP 层仍有❌ 无
头部压缩✅ HPACK✅ QPACK
服务器推送✅ Server Push❌(已弃用)
连接建立TCP 三次握手 + TLS同 1.10-RTT / 1-RTT
加密可选(HTTPS)实践中必须 TLS内置加密

HTTP/1.1 的问题

问题 1: 队头阻塞 (Head-of-Line Blocking)
  连接 1: [请求A]----[响应A]----[请求B]----[响应B]
  A 慢了 → B 必须等

  变通方案: 浏览器开 6 个并行连接(但消耗资源)

问题 2: 冗余头部
  每个请求都携带完整 Header(Cookie 可能几 KB)
  100 个请求 → 重复传输 100 次相同的 Header

HTTP/2 多路复用

HTTP/1.1:
  连接1: [A请求][A响应]  [B请求][B响应]
  连接2: [C请求][C响应]  [D请求][D响应]
  → 需要多个 TCP 连接,串行等待

HTTP/2:
  单个连接:
  ──[A帧1][B帧1][A帧2][C帧1][B帧2][A帧3]──
  → 所有请求/响应混合在同一连接中传输
  → 通过 Stream ID 区分不同请求

但仍有 TCP 层队头阻塞:
  如果一个 TCP 包丢失 → 所有 Stream 都等待重传

HTTP/3 (QUIC)

QUIC 基于 UDP:
  - 每个 Stream 独立,丢包互不影响
  - Stream A 丢包 → 只有 A 等待重传,B/C 正常接收
  - 0-RTT 连接建立(之前连接过的服务器)

0-RTT 连接:
  HTTP/1.1: TCP握手(1RTT) + TLS握手(2RTT) = 3RTT
  HTTP/2:   TCP握手(1RTT) + TLS1.3(1RTT) = 2RTT
  HTTP/3:   QUIC(0-1RTT) = 0-1RTT 🚀

追问延伸

  • 为什么 HTTP/2 不需要"雪碧图"、"域名分片"等优化了?
  • QUIC 的连接迁移(Connection Migration)是什么?(WiFi 切 4G 不断连)
  • 如何检查网站是否使用了 HTTP/2/3?(DevTools → Network → Protocol 列)

4. HTTPS 的工作原理?TLS 握手过程? ⭐⭐

解释 HTTPS 加密通信的建立过程。

考察点:HTTPS、TLS

HTTPS = HTTP + TLS

HTTP:  明文传输 → 窃听、篡改、冒充
HTTPS: TLS 加密 → 机密性、完整性、身份认证

加密方式:
  非对称加密(RSA/ECDHE)→ 交换密钥(慢,仅握手阶段使用)
  对称加密(AES)→ 传输数据(快,会话中使用)
  数字证书 → 验证服务器身份
  MAC → 验证数据完整性

TLS 1.3 握手(1-RTT)

Client                              Server
  │                                    │
  ├── ClientHello ──────────────────→  │
  │   (支持的密码套件, 随机数,         │
  │    key_share: 客户端公钥)          │
  │                                    │
  │  ←─────────── ServerHello ─────────┤
  │               (选定密码套件,       │
  │                key_share: 服务端公钥,
  │                证书, 签名验证)      │
  │                                    │
  │   此时双方已计算出对称密钥          │
  │                                    │
  ├── Finished (加密) ──────────────→  │
  │                                    │
  │  ←──────── Finished (加密) ────────┤
  │                                    │
  │   ===== 加密通信开始 =====         │

证书链验证

浏览器收到服务器证书后:

服务器证书 (example.com)
  │ 由中间 CA 签名

中间 CA 证书
  │ 由根 CA 签名

根 CA 证书 (预装在操作系统/浏览器中)

验证过程:
  1. 用中间 CA 的公钥验证服务器证书的签名
  2. 用根 CA 的公钥验证中间 CA 的证书
  3. 根 CA 是受信任的 → 整条链可信 ✅

前端需要关注的

① 混合内容 (Mixed Content)
  HTTPS 页面加载 HTTP 资源 → 浏览器阻止/警告
  → 所有资源都要走 HTTPS

② HSTS (HTTP Strict Transport Security)
  Strict-Transport-Security: max-age=31536000; includeSubDomains
  → 浏览器自动将 HTTP 升级为 HTTPS

③ 证书透明度 (Certificate Transparency)
  → 防止 CA 误发证书

追问延伸

  • TLS 1.2 和 1.3 有什么区别?(1.3 更快、更安全、去掉了不安全的算法)
  • 什么是 ECDHE 密钥交换?为什么比 RSA 更好?(前向安全性)
  • 自签名证书和 CA 签名证书的区别?Let's Encrypt 免费证书原理?

5. 同源策略和跨域解决方案? ⭐⭐

列举并对比各种跨域方案。

考察点:同源策略、CORS

同源策略

同源 = 协议 + 域名 + 端口 全部相同

https://example.com:443/path
  ↑ 协议    ↑ 域名    ↑ 端口

以 https://example.com 为例:
  https://example.com/api      ✅ 同源
  http://example.com           ❌ 协议不同
  https://api.example.com      ❌ 域名不同(子域也不行)
  https://example.com:8080     ❌ 端口不同

限制范围:
  ❌ AJAX 请求(XMLHttpRequest / fetch)
  ❌ DOM 操作(iframe 跨域)
  ❌ Cookie / LocalStorage / IndexedDB
  ✅ <img> <script> <link> 标签可以跨域加载(但不能读取内容)

CORS(跨域资源共享)— 主流方案

简单请求(GET/HEAD/POST + 特定 Content-Type):
  浏览器直接发请求,带 Origin 头
  → 服务器返回 Access-Control-Allow-Origin
  → 浏览器检查 → 匹配则允许

预检请求(非简单请求: PUT/DELETE、自定义 Header 等):
  浏览器先发 OPTIONS 请求(预检)
  → 服务器返回允许的方法/头部/来源
  → 通过后浏览器发真正的请求
# 服务端响应头
Access-Control-Allow-Origin: https://example.com  # 或 *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true  # 允许携带 Cookie
Access-Control-Max-Age: 86400  # 预检缓存时间(秒)

# 注意: Allow-Origin 为 * 时不能同时 Allow-Credentials: true

其他跨域方案

方案原理适用场景
CORS服务端设置响应头标准方案,首选
代理服务器同源服务器转发请求开发环境(Vite proxy)、BFF
JSONP<script> 不受同源限制仅 GET,已过时
postMessage跨窗口通信 APIiframe、popup 通信
WebSocket独立协议,不受同源限制实时通信
document.domain设置相同的主域已废弃

开发环境代理

typescript
// vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      }
    }
  }
})

// 开发时: fetch('/api/users') → Vite 代理到 → https://api.example.com/users
// 同源请求,无跨域问题

追问延伸

  • 为什么表单提交不受同源策略限制?(历史原因,表单提交后页面跳转,无法读取响应)
  • CORS 预检请求可以缓存吗?(Access-Control-Max-Age
  • Nginx 反向代理怎么配置 CORS?

6. XSS 和 CSRF 攻击与防御? ⭐⭐⭐

解释两种常见 Web 安全攻击的原理和防御措施。

考察点:Web 安全

XSS(跨站脚本攻击)

原理: 攻击者注入恶意脚本,在受害者浏览器中执行

三种类型:
  ① 存储型: 恶意脚本存入数据库 → 其他用户访问时执行
     例: 评论区提交 <script>steal(document.cookie)</script>

  ② 反射型: 恶意脚本在 URL 参数中 → 服务器原样返回
     例: https://example.com/search?q=<script>alert(1)</script>

  ③ DOM 型: 前端 JS 直接将不可信数据插入 DOM
     例: element.innerHTML = userInput

XSS 防御

typescript
// ① 输出编码(最重要)
function escapeHtml(str: string): string {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
}

// React/Vue 默认会转义 → 使用框架就已经防了大部分 XSS
// ❌ 危险: v-html / dangerouslySetInnerHTML → 必须先消毒

// ② CSP(内容安全策略)
// Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'
// → 只允许加载同源脚本和带有特定 nonce 的内联脚本

// ③ HttpOnly Cookie
// Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict
// → JS 无法通过 document.cookie 读取

// ④ 输入验证
// 限制输入格式(如邮箱只能包含特定字符)

CSRF(跨站请求伪造)

原理: 利用用户已登录的身份,诱导用户在其他网站发起请求

攻击流程:
  1. 用户登录 bank.com → 浏览器存储了 Cookie
  2. 用户访问恶意网站 evil.com
  3. evil.com 中有: <img src="https://bank.com/transfer?to=hacker&amount=10000">
  4. 浏览器自动带上 bank.com 的 Cookie 发起请求
  5. bank.com 认为是用户本人操作 → 转账成功

CSRF 防御

① SameSite Cookie(最简单有效)
  Set-Cookie: token=xxx; SameSite=Strict  → 跨站不携带
  Set-Cookie: token=xxx; SameSite=Lax     → GET 跳转允许,POST 不允许(默认值)

② CSRF Token
  服务器生成随机 token → 嵌入表单隐藏字段
  → 提交时验证 token → 攻击者无法获取 token

③ 验证 Referer / Origin
  检查请求来源是否是本站

④ 关键操作二次验证
  转账 → 输入密码或短信验证码

对比

维度XSSCSRF
攻击方式注入脚本,在目标站执行利用已有身份,跨站发请求
信任方向服务器信任了浏览器中的恶意脚本服务器信任了浏览器带来的身份
获取数据✅ 可以(读 Cookie、DOM)❌ 只能发请求,不能读响应
防御核心输出编码 + CSPSameSite Cookie + CSRF Token

追问延伸

  • React/Vue 是怎么防 XSS 的?(默认 textContent 而非 innerHTML)
  • SameSite=None 什么时候需要用?(第三方 Cookie,如 OAuth)
  • 点击劫持(Clickjacking)是什么?怎么防?(X-Frame-Options / CSP frame-ancestors)

7. 浏览器的事件循环?宏任务和微任务? ⭐⭐⭐

用代码示例说明事件循环的执行顺序。

考察点:Event Loop、任务队列

事件循环模型

┌──────────────────────────────┐
│         Call Stack            │  ← 同步代码在这里执行
└──────────┬───────────────────┘
           │ 同步代码执行完

┌──────────────────────────────┐
│     Microtask Queue          │  ← Promise.then / queueMicrotask
│     (清空所有微任务)          │     MutationObserver
└──────────┬───────────────────┘
           │ 微任务全部清空

┌──────────────────────────────┐
│     渲染(可能)              │  ← requestAnimationFrame
│     Layout / Paint            │     浏览器决定是否需要渲染
└──────────┬───────────────────┘


┌──────────────────────────────┐
│     Macrotask Queue          │  ← setTimeout / setInterval
│     (取出一个宏任务执行)      │     I/O / UI 事件
└──────────┬───────────────────┘

           └── 回到顶部,重复循环

经典面试题

javascript
console.log('1')

setTimeout(() => console.log('2'), 0)

Promise.resolve()
  .then(() => console.log('3'))
  .then(() => console.log('4'))

console.log('5')

// 输出: 1 5 3 4 2

// 解析:
// 同步: 1 → 5
// 微任务队列: [3] → 执行 3 → [4] → 执行 4
// 宏任务队列: [2] → 执行 2
javascript
async function async1() {
  console.log('a1 start')
  await async2()
  console.log('a1 end')
}
async function async2() {
  console.log('a2')
}

console.log('script start')
setTimeout(() => console.log('setTimeout'), 0)
async1()
new Promise(resolve => {
  console.log('promise1')
  resolve()
}).then(() => console.log('promise2'))
console.log('script end')

// 输出:
// script start
// a1 start
// a2
// promise1
// script end
// a1 end
// promise2
// setTimeout

// 关键: await 后面的代码 = .then() 回调 → 微任务

宏任务 vs 微任务

宏任务 (Macrotask)微任务 (Microtask)
setTimeout / setIntervalPromise.then/catch/finally
setImmediate (Node)queueMicrotask
I/OMutationObserver
UI renderingprocess.nextTick (Node)
MessageChannel
requestAnimationFrame

追问延伸

  • requestAnimationFrame 是宏任务还是微任务?(都不是,在渲染前执行)
  • Node.js 的事件循环和浏览器有什么区别?(6 个阶段 vs 2 个队列)
  • queueMicrotaskPromise.resolve().then 有什么区别?

对比浏览器端的各种存储方案。

考察点:浏览器存储

对比

维度CookieLocalStorageSessionStorageIndexedDB
容量~4KB~5-10MB~5-10MB无限(受磁盘限制)
生命周期设置过期时间永久页面关闭即失永久
作用域同源 + 路径同源同源 + 同标签页同源
随请求发送✅ 自动
APIdocument.cookiegetItem/setItemgetItem/setItem异步事务 API
适用场景身份认证偏好设置、缓存表单临时数据大量结构化数据
Set-Cookie: token=abc123;
  Domain=.example.com;       → 作用域(包括子域名)
  Path=/;                    → 路径
  Max-Age=86400;             → 过期时间(秒)
  Expires=Thu, 01 Jan 2026;  → 绝对过期时间
  HttpOnly;                  → JS 不可访问(防 XSS)
  Secure;                    → 仅 HTTPS
  SameSite=Lax;              → 跨站限制(防 CSRF)

LocalStorage vs SessionStorage

javascript
// LocalStorage: 永久存储,同源所有标签页共享
localStorage.setItem('theme', 'dark')
localStorage.getItem('theme')   // 'dark'
localStorage.removeItem('theme')

// SessionStorage: 标签页关闭即清除,不跨标签页
sessionStorage.setItem('scroll', '500')

// 监听存储变化(跨标签页通信)
window.addEventListener('storage', (e) => {
  console.log(e.key, e.oldValue, e.newValue)
})
// 注意: 只有其他标签页修改时触发,当前标签页修改不触发

现代替代方案

typescript
// Cache API (配合 Service Worker)
const cache = await caches.open('v1')
await cache.put('/api/data', new Response(JSON.stringify(data)))

// Cookie Store API (异步,替代 document.cookie)
const cookie = await cookieStore.get('token')
await cookieStore.set({ name: 'token', value: 'abc', sameSite: 'strict' })

追问延伸

  • 如何实现跨标签页通信?(localStorage event / BroadcastChannel / SharedWorker)
  • 第三方 Cookie 被禁用后,追踪和广告怎么办?(Privacy Sandbox)
  • IndexedDB 适合存什么?(离线 App 的结构化数据、图片缓存)

9. 什么是回流(Reflow)和重绘(Repaint)?如何减少? ⭐⭐

解释浏览器渲染机制中的回流和重绘。

考察点:渲染性能

回流 vs 重绘

回流 (Reflow / Layout):
  元素的几何属性变化(位置、大小)
  → 重新计算元素的位置和尺寸
  → 必然导致重绘
  → 开销大

重绘 (Repaint):
  元素的外观变化(颜色、背景、阴影)
  → 不影响布局
  → 只重新绘制
  → 开销中等

合成 (Composite):
  transform / opacity 变化
  → 直接在 GPU 合成层处理
  → 不触发回流和重绘
  → 开销小

触发回流的操作

javascript
// ❌ 这些操作都会触发回流
element.offsetHeight          // 读取布局信息
element.getBoundingClientRect()
element.style.width = '100px' // 修改几何属性
element.style.padding = '10px'
element.style.display = 'none'
window.getComputedStyle(element)
document.body.appendChild(child)
window.scrollTo(0, 100)

浏览器的优化:批量处理

javascript
// 浏览器会将多次修改合并为一次回流
element.style.width = '100px'
element.style.height = '200px'
element.style.padding = '10px'
// 浏览器只触发一次回流(异步批量处理)

// ❌ 但读取布局信息会强制刷新队列
element.style.width = '100px'
console.log(element.offsetHeight) // 强制回流!
element.style.height = '200px'
console.log(element.offsetHeight) // 又一次强制回流!
// 共 2 次回流

减少回流的方法

javascript
// ① 批量读写分离
// ❌
for (const el of elements) {
  el.style.width = el.offsetWidth + 10 + 'px' // 读写交替 → N 次回流
}
// ✅
const widths = elements.map(el => el.offsetWidth) // 先全部读
elements.forEach((el, i) => {
  el.style.width = widths[i] + 10 + 'px' // 再全部写
})

// ② 离线 DOM 操作
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li')
  li.textContent = `Item ${i}`
  fragment.appendChild(li) // 不触发回流
}
list.appendChild(fragment) // 只触发一次回流

// ③ 使用 class 替代多个 style
// ❌
el.style.width = '100px'
el.style.height = '200px'
el.style.background = 'red'
// ✅
el.className = 'new-style' // 一次回流

// ④ 动画使用 transform
// ❌ el.style.left = x + 'px'  → 回流
// ✅ el.style.transform = `translateX(${x}px)` → 仅合成

追问延伸

  • requestAnimationFrame 如何帮助减少回流?
  • Chrome DevTools 的 Performance 面板如何查看回流和重绘?
  • will-change 属性和回流有什么关系?

10. 前端性能指标?Core Web Vitals 是什么? ⭐⭐⭐

说明现代前端性能度量指标和优化方向。

考察点:性能指标、CWV

Core Web Vitals(核心 Web 指标)

Google 定义的三个核心用户体验指标:

┌─────────────────────────────────────┐
│  LCP (Largest Contentful Paint)     │  加载性能
│  最大内容绘制 ≤ 2.5s               │  → 最大元素何时可见
├─────────────────────────────────────┤
│  INP (Interaction to Next Paint)    │  交互响应
│  交互到下一次绘制 ≤ 200ms          │  → 点击后多快响应
├─────────────────────────────────────┤
│  CLS (Cumulative Layout Shift)     │  视觉稳定性
│  累积布局偏移 ≤ 0.1                │  → 页面是否"跳动"
└─────────────────────────────────────┘

所有性能指标

指标含义需改进
LCP最大内容绘制≤ 2.5s2.5-4s> 4s
INP交互到绘制≤ 200ms200-500ms> 500ms
CLS累积布局偏移≤ 0.10.1-0.25> 0.25
FCP首次内容绘制≤ 1.8s1.8-3s> 3s
TTFB首字节时间≤ 0.8s0.8-1.8s> 1.8s
TBT总阻塞时间≤ 200ms200-600ms> 600ms

优化 LCP

LCP 可能的元素: <img>, <video>, 有 background-image 的元素, 大段文本

优化方向:
  ① 优化服务器响应时间 (TTFB)
     → CDN、缓存、SSR
  ② 资源加载优先级
     → <link rel="preload"> 关键图片
     → fetchpriority="high"
  ③ 图片优化
     → 使用 WebP/AVIF、响应式图片 (srcset)、懒加载非首屏图片
  ④ 减少阻塞渲染的资源
     → CSS 内联关键路径、JS defer/async

优化 CLS

html
<!-- ❌ 图片不设尺寸 → 加载后撑开布局 → CLS -->
<img src="photo.jpg">

<!-- ✅ 始终设置尺寸 -->
<img src="photo.jpg" width="800" height="600">

<!-- ✅ 或用 aspect-ratio -->
<img src="photo.jpg" style="aspect-ratio: 4/3; width: 100%;">

<!-- ❌ 动态内容插入 → 下面的内容被推开 -->
<!-- ✅ 预留空间(skeleton、min-height) -->

<!-- ❌ 字体闪烁 (FOUT/FOIT) -->
<!-- ✅ font-display: swap + preload 字体 -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>

性能度量工具

javascript
// Web Vitals 库
import { onLCP, onINP, onCLS } from 'web-vitals'

onLCP(console.log)  // { name: 'LCP', value: 1800, rating: 'good' }
onINP(console.log)
onCLS(console.log)

// Performance API
const paint = performance.getEntriesByType('paint')
// [{ name: 'first-paint', startTime: 800 }, { name: 'first-contentful-paint', startTime: 1200 }]

// PerformanceObserver
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.startTime, entry.duration)
  }
})
observer.observe({ type: 'largest-contentful-paint', buffered: true })
工具类型特点
Lighthouse实验室本地跑分,模拟网络
PageSpeed Insights综合实验室 + 真实用户数据 (CrUX)
Chrome DevTools开发时Performance 面板详细分析
web-vitals npm生产环境采集真实用户数据 (RUM)

追问延伸

  • INP 为什么替代了 FID(First Input Delay)?(INP 衡量所有交互,不只是第一次)
  • 如何设置性能预算(Performance Budget)?
  • Lighthouse 评分和真实用户体验为什么有差异?(网络/设备差异)

11. 浏览器的垃圾回收机制?V8 的分代回收? ⭐⭐⭐

解释 JavaScript 引擎的内存管理。

考察点:V8、GC、内存管理

为什么需要垃圾回收

JavaScript 自动内存管理:
  ① 分配 → 声明变量/创建对象时自动分配内存
  ② 使用 → 读写变量
  ③ 回收 → 不再使用时自动释放(GC 负责)

核心问题: GC 如何判断"不再使用"?

两种基础策略

① 引用计数 (Reference Counting) — 已淘汰
  每个对象维护一个引用计数
  引用 +1,解引用 -1,计数为 0 → 回收

  致命问题: 循环引用
  let a = {}
  let b = {}
  a.ref = b
  b.ref = a
  a = null
  b = null
  // a 和 b 互相引用 → 计数永远不为 0 → 内存泄漏

② 标记-清除 (Mark-Sweep) — 现代引擎使用
  从根对象 (window/global) 出发
  → 遍历所有可达对象 → 标记为"活跃"
  → 未被标记的 → 回收
  → 循环引用但不可达 → 也会被回收 ✅

V8 的分代回收

V8 将堆内存分为两个区域:

┌─────────────────────────────────────────┐
│  新生代 (Young Generation)    ~1-8MB    │
│  存放生命周期短的对象                      │
│  使用 Scavenge (复制算法)                 │
├─────────────────────────────────────────┤
│  老生代 (Old Generation)     ~700MB+    │
│  存放生命周期长的对象                      │
│  使用 Mark-Sweep + Mark-Compact         │
└─────────────────────────────────────────┘

为什么分代? → 弱分代假说:
  "大部分对象都是短命的,少数对象存活很久"
  → 针对不同生命周期使用不同策略 → 效率最高

新生代回收 — Scavenge

新生代分为两个半空间:
  From 空间 (正在使用)
  To 空间   (空闲)

回收过程:
  1. 在 From 空间分配对象
  2. From 满了 → 触发 Scavenge
  3. 遍历 From 中的活跃对象 → 复制到 To
  4. From 和 To 角色互换
  5. 非活跃对象被直接丢弃(不需要逐个释放)

晋升到老生代:
  - 对象经历过一次 Scavenge 仍存活 → 晋升
  - To 空间已使用超过 25% → 晋升

老生代回收

① Mark-Sweep (标记-清除)
  从根出发标记所有可达对象
  → 清除未标记对象
  → 问题: 产生内存碎片

② Mark-Compact (标记-整理)
  标记后不直接清除
  → 将活跃对象向一端移动
  → 然后清除边界外的内存
  → 解决碎片问题,但速度更慢

③ 增量标记 (Incremental Marking)
  全量标记会导致主线程卡顿(Stop-the-World)
  → 将标记分成小步 → 每步 5ms → 穿插在 JS 执行中
  → 减少单次暂停时间

④ 并发回收 (Concurrent GC)
  标记和清除在辅助线程中进行
  → 主线程几乎不暂停
  → V8 的 Orinoco GC 采用此策略

前端常见内存泄漏

javascript
// ❌ 1. 意外的全局变量
function leak() {
  name = 'oops' // 没有 var/let/const → 挂到 window 上
}

// ❌ 2. 遗忘的定时器
const timer = setInterval(() => {
  // 引用了外部变量 → 外部变量无法被 GC
}, 1000)
// 忘记 clearInterval(timer)

// ❌ 3. 闭包持有不必要的引用
function outer() {
  const hugeData = new Array(10000)
  return function inner() {
    // inner 持有 outer 的作用域 → hugeData 无法释放
    console.log('hi')
  }
}

// ❌ 4. 未清理的事件监听
const handler = () => {}
element.addEventListener('click', handler)
// 组件卸载时忘记 removeEventListener

// ❌ 5. 分离的 DOM 节点
const el = document.getElementById('foo')
document.body.removeChild(el)
// el 变量仍然引用该 DOM 节点 → 无法被 GC

// ✅ 使用 WeakRef / WeakMap 避免强引用
const cache = new WeakMap()
// key 被 GC 时,对应的 value 也自动清理

排查工具

Chrome DevTools:
  Memory 面板:
    - Heap Snapshot → 查看堆内存中的对象分布
    - Allocation Timeline → 查看内存分配时间线
    - Allocation Sampling → 采样分析

  操作步骤:
    1. 拍一次快照 (Snapshot 1)
    2. 执行可能泄漏的操作
    3. 再拍一次快照 (Snapshot 2)
    4. 对比两次快照 → 找到"只增不减"的对象

  Performance 面板:
    - 勾选 Memory → 查看 JS Heap 曲线
    - 如果曲线持续上升不回落 → 内存泄漏

追问延伸

  • WeakRefFinalizationRegistry 是什么?有什么用?
  • V8 的隐藏类(Hidden Class)和内联缓存(Inline Cache)怎么优化 JS 对象访问?
  • 为什么 delete obj.keyobj.key = undefined 性能差?(破坏隐藏类)

12. WebSocket 和 HTTP 的区别?心跳机制如何实现? ⭐⭐

对比 WebSocket 和 HTTP,实现可靠的长连接。

考察点:实时通信、长连接

WebSocket vs HTTP

维度HTTPWebSocket
通信方向单向(客户端请求,服务器响应)双向(服务器可主动推送)
连接方式短连接(每次请求新建连接)长连接(一次握手,持续通信)
数据格式文本(Header 冗余)帧(Header 仅 2-14 字节)
协议http:// / https://ws:// / wss://
适用场景REST API、页面加载即时聊天、实时协作、股票行情

WebSocket 连接过程

1. HTTP 升级请求(握手)
   GET / HTTP/1.1
   Host: example.com
   Upgrade: websocket              ← 请求协议升级
   Connection: Upgrade
   Sec-WebSocket-Key: dGhlIHNh...  ← 随机 Base64
   Sec-WebSocket-Version: 13

2. 服务器响应
   HTTP/1.1 101 Switching Protocols  ← 协议切换成功
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Accept: s3pPLM...   ← 根据 Key 计算

3. 后续通信在同一 TCP 连接上进行
   双方可随时发送/接收数据帧

前端 WebSocket 使用

typescript
const ws = new WebSocket('wss://api.example.com/ws')

ws.onopen = () => {
  console.log('连接成功')
  ws.send(JSON.stringify({ type: 'subscribe', channel: 'chat' }))
}

ws.onmessage = (event) => {
  const data = JSON.parse(event.data)
  console.log('收到消息:', data)
}

ws.onerror = (error) => {
  console.error('连接错误:', error)
}

ws.onclose = (event) => {
  console.log('连接关闭:', event.code, event.reason)
}

心跳机制

为什么需要心跳?
  - 中间代理(Nginx / 负载均衡)可能因超时断开空闲连接
  - 客户端网络断开(如进隧道)→ 服务端不知道
  - 需要定期发送小数据包证明连接存活
typescript
class WebSocketClient {
  private ws: WebSocket | null = null
  private heartbeatTimer: number | null = null
  private reconnectTimer: number | null = null
  private reconnectAttempts = 0
  private readonly maxReconnectAttempts = 5
  private readonly heartbeatInterval = 30000
  private readonly reconnectDelay = 3000

  constructor(private url: string) {
    this.connect()
  }

  private connect() {
    this.ws = new WebSocket(this.url)

    this.ws.onopen = () => {
      this.reconnectAttempts = 0
      this.startHeartbeat()
    }

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      if (data.type === 'pong') return
      this.handleMessage(data)
    }

    this.ws.onclose = () => {
      this.stopHeartbeat()
      this.reconnect()
    }
  }

  private startHeartbeat() {
    this.heartbeatTimer = window.setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'ping' }))
      }
    }, this.heartbeatInterval)
  }

  private stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)
      this.heartbeatTimer = null
    }
  }

  private reconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) return

    this.reconnectAttempts++
    const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)

    this.reconnectTimer = window.setTimeout(() => {
      this.connect()
    }, delay)
  }

  private handleMessage(data: unknown) {
    // ...
  }

  destroy() {
    this.stopHeartbeat()
    if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
    this.ws?.close()
  }
}

WebSocket vs SSE vs 轮询

方案方向实时性复杂度适用场景
短轮询单向低(依赖间隔)简单状态检查
长轮询单向兼容性要求高
SSE单向(服务器→客户端)AI 流式输出、通知推送
WebSocket双向即时聊天、协作编辑、游戏

追问延伸

  • Socket.IO 和原生 WebSocket 的区别?(封装了自动重连/回退/房间/命名空间)
  • WebSocket 如何处理断线重连?指数退避算法?
  • SSE 的自动重连机制?retry 字段和 Last-Event-ID 怎么用?

13. 什么是 Service Worker?PWA 的核心原理? ⭐⭐

解释 Service Worker 和 PWA 的关键技术。

考察点:离线缓存、PWA

Service Worker 是什么

Service Worker (SW):
  - 运行在浏览器后台的独立 JS 线程
  - 不能访问 DOM(独立于页面)
  - 可拦截和处理网络请求(代理层)
  - 支持推送通知和后台同步
  - 必须在 HTTPS 下运行(localhost 除外)

与 Web Worker 的区别:
  ┌──────────────────┬────────────────────┐
  │   Web Worker     │  Service Worker     │
  ├──────────────────┼────────────────────┤
  │ 依附于页面        │ 独立于页面          │
  │ 页面关闭即销毁    │ 可在后台持续运行     │
  │ 用于计算密集任务   │ 用于网络代理/缓存   │
  │ 一个页面可多个     │ 一个 scope 一个     │
  └──────────────────┴────────────────────┘

Service Worker 生命周期

注册 → 安装 → 激活 → 控制页面

┌─────────────┐
│  register()  │  主线程调用 navigator.serviceWorker.register()
└──────┬──────┘

┌─────────────┐
│   install    │  SW 首次安装,预缓存关键资源
│   事件       │  self.addEventListener('install', ...)
└──────┬──────┘

┌─────────────┐
│   waiting    │  如果旧 SW 还在控制页面,新 SW 等待
└──────┬──────┘
       ↓ (旧 SW 终止后)
┌─────────────┐
│   activate   │  清理旧缓存
│   事件       │  self.addEventListener('activate', ...)
└──────┬──────┘

┌─────────────┐
│   fetch      │  拦截页面的网络请求
│   事件       │  self.addEventListener('fetch', ...)
└─────────────┘

注册与缓存

javascript
// main.js — 注册
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', { scope: '/' })
    .then(reg => console.log('SW registered:', reg.scope))
    .catch(err => console.error('SW failed:', err))
}
javascript
// sw.js — Service Worker
const CACHE_NAME = 'app-v2'
const PRECACHE_URLS = ['/', '/index.html', '/style.css', '/app.js']

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(PRECACHE_URLS))
      .then(() => self.skipWaiting())
  )
})

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(
        keys
          .filter(key => key !== CACHE_NAME)
          .map(key => caches.delete(key))
      )
    ).then(() => self.clients.claim())
  )
})

缓存策略

javascript
// ① Cache First — 缓存优先(适合静态资源)
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then(cached => cached || fetch(event.request))
  )
})

// ② Network First — 网络优先(适合 API 请求)
self.addEventListener('fetch', (event) => {
  event.respondWith(
    fetch(event.request)
      .then(response => {
        const clone = response.clone()
        caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone))
        return response
      })
      .catch(() => caches.match(event.request))
  )
})

// ③ Stale While Revalidate — 先返回缓存,后台更新(适合频繁变化的资源)
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open(CACHE_NAME).then(cache =>
      cache.match(event.request).then(cached => {
        const fetched = fetch(event.request)
          .then(response => {
            cache.put(event.request, response.clone())
            return response
          })
        return cached || fetched
      })
    )
  )
})

PWA 核心要素

PWA (Progressive Web App) 三大支柱:

① Service Worker → 离线能力 + 缓存控制
② Web App Manifest → 安装到桌面
③ HTTPS → 安全

manifest.json:
{
  "name": "My App",
  "short_name": "App",
  "start_url": "/",
  "display": "standalone",    ← 全屏/独立窗口/浏览器
  "background_color": "#fff",
  "theme_color": "#6366f1",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}

追问延伸

  • Workbox 是什么?它如何简化 Service Worker 开发?
  • skipWaiting()clients.claim() 分别做什么?为什么需要?
  • Service Worker 的更新流程?如何通知用户"有新版本可用"?

14. WebSocket / SSE / HTTP 轮询的对比?DNS 解析过程和优化? ⭐⭐

补充网络协议细节和 DNS 优化。

考察点:DNS、网络优化

DNS 解析过程

浏览器输入 www.example.com:

┌───────────────────────┐
│ 1. 浏览器 DNS 缓存     │  → 命中则直接返回 IP
└──────────┬────────────┘
           ↓ 未命中
┌───────────────────────┐
│ 2. 操作系统 DNS 缓存    │  → hosts 文件 + OS 缓存
└──────────┬────────────┘
           ↓ 未命中
┌───────────────────────┐
│ 3. 路由器 DNS 缓存      │
└──────────┬────────────┘
           ↓ 未命中
┌───────────────────────┐
│ 4. ISP DNS 服务器       │  → 运营商的递归解析器
│    (递归 DNS)           │     通常有大量缓存
└──────────┬────────────┘
           ↓ 未命中 → 开始递归/迭代查询
┌───────────────────────┐
│ 5. 根域名服务器 (.)     │  → 告知 .com 的 NS 地址
└──────────┬────────────┘

┌───────────────────────┐
│ 6. TLD 域名服务器       │  → .com NS 告知 example.com 的 NS
│    (.com)              │
└──────────┬────────────┘

┌───────────────────────┐
│ 7. 权威 DNS 服务器      │  → 返回 www.example.com 的 IP
│    (example.com)       │     93.184.216.34
└───────────────────────┘

DNS 优化手段

html
<!-- ① 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">

<!-- ② 预连接 — DNS + TCP + TLS 一步到位 -->
<link rel="preconnect" href="https://cdn.example.com">
<!-- 比 dns-prefetch 更激进,适合确定会用到的域名 -->

<!-- ③ 预加载 — 高优先级加载关键资源 -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
<link rel="preload" href="/hero.webp" as="image">

<!-- ④ 预获取 — 低优先级加载下一页可能用到的资源 -->
<link rel="prefetch" href="/next-page.js">
各指令对比:
  dns-prefetch  → 只做 DNS 解析(最轻量)
  preconnect    → DNS + TCP + TLS(连接就绪)
  preload       → 立即高优先级加载(当前页需要)
  prefetch      → 空闲时低优先级加载(下一页可能需要)
  prerender     → 在后台渲染整个页面(已弃用,用 Speculation Rules 替代)

DNS 缓存层级与 TTL

DNS 记录都有 TTL (Time To Live):
  A 记录:    www.example.com → 93.184.216.34  TTL=300s (5min)
  CNAME 记录: cdn.example.com → d111111.cloudfront.net

TTL 策略:
  短 TTL (60s)   → 灵活切换 IP(如故障转移),但 DNS 查询频繁
  长 TTL (86400s) → 减少 DNS 查询,但切换 IP 后生效慢
  最佳实践: 正常时长 TTL + 故障时降低 TTL

其他 DNS 优化

① 减少域名数量
  HTTP/1.1 时代:域名分片(6 个并行连接限制)
  HTTP/2 时代:合并到同一个域名(利用多路复用)

② HTTPDNS
  绕过运营商 DNS → 直接向 DNS 服务器发 HTTP 请求
  → 避免 DNS 劫持 / DNS 解析慢
  → 移动端常用(如阿里云 HTTPDNS)

③ DNS over HTTPS (DoH) / DNS over TLS (DoT)
  加密 DNS 查询 → 防止窃听和篡改
  Chrome/Firefox 已支持 DoH

追问延伸

  • dns-prefetchpreconnect 的性能开销?滥用会怎样?
  • 什么是 DNS 劫持?前端如何检测和应对?
  • CDN 是如何利用 DNS 实现就近接入的?(CNAME + 智能 DNS)

15. 前端性能优化的完整方案?从加载到运行时? ⭐⭐⭐

系统性总结前端性能优化方案。

考察点:性能优化体系

性能优化全景图

前端性能优化 = 加载优化 + 渲染优化 + 运行时优化 + 感知优化

┌─────────────────────────────────────────────┐
│  网络层                                      │
│  DNS 预解析 / CDN / HTTP/2 / 压缩 / 缓存    │
├─────────────────────────────────────────────┤
│  资源层                                      │
│  代码分割 / Tree Shaking / 图片优化 / 字体    │
├─────────────────────────────────────────────┤
│  渲染层                                      │
│  关键渲染路径 / CSS 阻塞 / JS 阻塞           │
├─────────────────────────────────────────────┤
│  运行时                                      │
│  虚拟列表 / Web Worker / 防抖节流 / 内存      │
├─────────────────────────────────────────────┤
│  感知性能                                    │
│  骨架屏 / 渐进加载 / 乐观更新 / 动画过渡     │
└─────────────────────────────────────────────┘

一、加载优化

① 资源压缩
  - JavaScript: Terser 压缩 + Tree Shaking
  - CSS: CSS Modules / PostCSS 移除未使用样式
  - 图片: WebP/AVIF(比 PNG 小 30-50%)
  - 传输: Brotli > Gzip(Brotli 压缩率高 20%)

② 代码分割
  - 路由级分割: React.lazy() / Vue defineAsyncComponent
  - 组件级分割: 大组件懒加载
  - 库级分割: moment.js → dayjs / lodash → lodash-es(按需导入)
typescript
// 路由级分割示例 (React)
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))

// Vite 手动分包
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'chart-vendor': ['echarts'],
        }
      }
    }
  }
})
③ 资源加载策略
  关键 CSS 内联到 <head>(消除渲染阻塞)
  JS 使用 defer(不阻塞解析,DOMContentLoaded 前执行)
  非关键资源 prefetch / preload
  图片懒加载: loading="lazy" / Intersection Observer

二、图片优化

① 格式选择
  照片 → WebP / AVIF(有损,体积小)
  图标 → SVG(矢量,无限缩放)
  简单图形 → CSS 实现(渐变、阴影)
  需要透明 → WebP > PNG

② 响应式图片
html
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero" width="1200" height="600"
       loading="lazy" decoding="async"
       srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
       sizes="(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px">
</picture>
③ 其他图片优化
  - CSS Sprite → 合并小图标(HTTP/2 后不太必要)
  - Base64 内联 → 极小图片(<2KB)
  - 渐进式 JPEG → 先显示模糊图,逐步清晰
  - 骨架屏 / LQIP(低质量图片占位)→ 减少 CLS

三、渲染优化

① 关键渲染路径优化
  CSS 放 <head>(尽早开始构建 CSSOM)
  JS 放 </body> 前或用 defer
  内联关键 CSS(首屏需要的 CSS 直接写入 HTML)

② 减少回流重绘
  使用 transform 代替 top/left
  批量修改 DOM(DocumentFragment / requestAnimationFrame)
  读写分离(不要交替读取布局属性和修改样式)

③ 长列表优化
  虚拟滚动: react-window / tanstack-virtual
  只渲染可视区域 + 上下缓冲区的 DOM 节点

四、运行时优化

typescript
// ① Web Worker — 将 CPU 密集任务移出主线程
const worker = new Worker(new URL('./heavy-task.ts', import.meta.url))
worker.postMessage(largeData)
worker.onmessage = (e) => console.log(e.data)

// ② 防抖节流
const handleScroll = throttle(() => {
  // 滚动处理逻辑
}, 100)

// ③ requestIdleCallback — 空闲时执行低优先级任务
requestIdleCallback((deadline) => {
  while (deadline.timeRemaining() > 0) {
    // 执行非紧急任务(如预加载、数据上报)
  }
})

// ④ AbortController — 取消不需要的请求
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
// 用户离开页面 → controller.abort()

五、感知性能优化

不只是"快",还要"感觉快":

① 骨架屏 (Skeleton)
  页面加载时显示灰色占位块 → 减少白屏感知时间

② 乐观更新 (Optimistic UI)
  点赞 → 立即显示已点赞 → 后台发请求
  如果失败 → 回滚状态

③ 加载进度
  顶部进度条(NProgress)→ 用户知道"正在加载"
  大文件上传 → 显示百分比进度

④ 过渡动画
  页面切换 → 淡入淡出 / 滑动
  列表增删 → 动画过渡(不突兀)

⑤ 预加载下一页
  鼠标 hover 链接时 → prefetch 下一页资源
  → 点击时几乎瞬间加载

优化 Checklist

□ 使用 HTTP/2 + Brotli 压缩
□ 静态资源上 CDN + 长缓存(immutable)
□ HTML 不缓存或协商缓存
□ 图片使用 WebP/AVIF + 响应式 + 懒加载
□ JS/CSS 代码分割 + Tree Shaking
□ 关键 CSS 内联,非关键 CSS 异步加载
□ 字体 preload + font-display: swap
□ 首屏数据 SSR/SSG
□ 虚拟滚动处理长列表
□ 动画使用 transform/opacity (GPU 加速)
□ Web Worker 处理计算密集任务
□ 使用 Performance API / web-vitals 监控
□ 设置性能预算,CI 中自动检查

追问延伸

  • 如何衡量性能优化的 ROI?哪些指标和业务指标(转化率/跳出率)关联最大?
  • 性能预算(Performance Budget)怎么定?超出了怎么办?
  • 如何在大型团队中推行性能优化文化?

用心学习,用代码说话 💻