主题
浏览器与网络
说明
共 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 复用连接)每个阶段的优化方向
| 阶段 | 优化手段 |
|---|---|
| DNS | DNS 预解析 <link rel="dns-prefetch"> |
| TCP | HTTP/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-Control | HTTP/1.1,优先级高 | max-age=31536000 |
Expires | HTTP/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-Since | Last-Modified | 最后修改时间 |
If-None-Match | ETag | 内容哈希 |
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.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 传输层 | TCP | TCP | QUIC (UDP) |
| 多路复用 | ❌ 一个连接一个请求 | ✅ 多路复用 | ✅ 多路复用 |
| 队头阻塞 | ✅ 有(应用层) | 🟡 TCP 层仍有 | ❌ 无 |
| 头部压缩 | ❌ | ✅ HPACK | ✅ QPACK |
| 服务器推送 | ❌ | ✅ Server Push | ❌(已弃用) |
| 连接建立 | TCP 三次握手 + TLS | 同 1.1 | 0-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 次相同的 HeaderHTTP/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 | 跨窗口通信 API | iframe、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 = userInputXSS 防御
typescript
// ① 输出编码(最重要)
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
// 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
检查请求来源是否是本站
④ 关键操作二次验证
转账 → 输入密码或短信验证码对比
| 维度 | XSS | CSRF |
|---|---|---|
| 攻击方式 | 注入脚本,在目标站执行 | 利用已有身份,跨站发请求 |
| 信任方向 | 服务器信任了浏览器中的恶意脚本 | 服务器信任了浏览器带来的身份 |
| 获取数据 | ✅ 可以(读 Cookie、DOM) | ❌ 只能发请求,不能读响应 |
| 防御核心 | 输出编码 + CSP | SameSite 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] → 执行 2javascript
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 / setInterval | Promise.then/catch/finally |
| setImmediate (Node) | queueMicrotask |
| I/O | MutationObserver |
| UI rendering | process.nextTick (Node) |
| MessageChannel | |
| requestAnimationFrame |
追问延伸
requestAnimationFrame是宏任务还是微任务?(都不是,在渲染前执行)- Node.js 的事件循环和浏览器有什么区别?(6 个阶段 vs 2 个队列)
queueMicrotask和Promise.resolve().then有什么区别?
8. Cookie / LocalStorage / SessionStorage / IndexedDB 的区别? ⭐⭐
对比浏览器端的各种存储方案。
考察点:浏览器存储
对比
| 维度 | Cookie | LocalStorage | SessionStorage | IndexedDB |
|---|---|---|---|---|
| 容量 | ~4KB | ~5-10MB | ~5-10MB | 无限(受磁盘限制) |
| 生命周期 | 设置过期时间 | 永久 | 页面关闭即失 | 永久 |
| 作用域 | 同源 + 路径 | 同源 | 同源 + 同标签页 | 同源 |
| 随请求发送 | ✅ 自动 | ❌ | ❌ | ❌ |
| API | document.cookie | getItem/setItem | getItem/setItem | 异步事务 API |
| 适用场景 | 身份认证 | 偏好设置、缓存 | 表单临时数据 | 大量结构化数据 |
Cookie 的关键属性
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.5s | 2.5-4s | > 4s |
| INP | 交互到绘制 | ≤ 200ms | 200-500ms | > 500ms |
| CLS | 累积布局偏移 | ≤ 0.1 | 0.1-0.25 | > 0.25 |
| FCP | 首次内容绘制 | ≤ 1.8s | 1.8-3s | > 3s |
| TTFB | 首字节时间 | ≤ 0.8s | 0.8-1.8s | > 1.8s |
| TBT | 总阻塞时间 | ≤ 200ms | 200-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 曲线
- 如果曲线持续上升不回落 → 内存泄漏追问延伸
WeakRef和FinalizationRegistry是什么?有什么用?- V8 的隐藏类(Hidden Class)和内联缓存(Inline Cache)怎么优化 JS 对象访问?
- 为什么
delete obj.key比obj.key = undefined性能差?(破坏隐藏类)
12. WebSocket 和 HTTP 的区别?心跳机制如何实现? ⭐⭐
对比 WebSocket 和 HTTP,实现可靠的长连接。
考察点:实时通信、长连接
WebSocket vs HTTP
| 维度 | HTTP | WebSocket |
|---|---|---|
| 通信方向 | 单向(客户端请求,服务器响应) | 双向(服务器可主动推送) |
| 连接方式 | 短连接(每次请求新建连接) | 长连接(一次握手,持续通信) |
| 数据格式 | 文本(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-prefetch和preconnect的性能开销?滥用会怎样?- 什么是 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)怎么定?超出了怎么办?
- 如何在大型团队中推行性能优化文化?