浏览器与网络高频面试题
1. 从输入 URL 到页面显示发生了什么?⭐⭐⭐
这是最经典的综合题,考察对整个网络请求和浏览器渲染流程的理解。
完整流程(8 大步骤)
| 阶段 | 步骤 | 关键细节 |
|---|---|---|
| ① DNS 解析 | 域名 → IP | 浏览器缓存 → OS 缓存 → hosts → 路由器 → ISP DNS → 根 → 顶级 → 权威 |
| ② TCP 连接 | 三次握手 | SYN → SYN+ACK → ACK |
| ③ TLS 握手 | HTTPS 才有 | 证书验证 + 密钥协商(TLS 1.3 仅 1-RTT) |
| ④ HTTP 请求 | 发送请求报文 | 请求行 + 请求头 + 请求体 |
| ⑤ 服务器响应 | 返回资源 | 状态码 + 响应头 + 响应体 |
| ⑥ 解析构建 | DOM + CSSOM | HTML → DOM 树,CSS → CSSOM 树 |
| ⑦ 渲染绘制 | Layout → Paint → Composite | 布局 → 绘制 → 合成层 |
| ⑧ JS 执行 | 可能触发回流重绘 | script 阻塞解析(async/defer 可优化) |
ini
TCP 三次握手(建立连接):
客户端 → 服务器:SYN=1, seq=x (我想连接)
服务器 → 客户端:SYN=1, ACK=1, seq=y, ack=x+1 (好的,我准备好了)
客户端 → 服务器:ACK=1, seq=x+1, ack=y+1 (收到,开始通信)
TCP 四次挥手(断开连接):
客户端 → 服务器:FIN=1 (我发完了)
服务器 → 客户端:ACK=1 (知道了,我可能还没发完)
服务器 → 客户端:FIN=1 (我也发完了)
客户端 → 服务器:ACK=1 (好的,等 2MSL 后关闭)
html
<!-- script 标签对解析的影响 -->
<script src="app.js"></script> <!-- ❌ 阻塞 HTML 解析 -->
<script src="app.js" async></script> <!-- ✅ 下载不阻塞,下载完立即执行(可能在DOMContentLoaded前后) -->
<script src="app.js" defer></script> <!-- ✅ 下载不阻塞,DOMContentLoaded 前按顺序执行 -->
💡 面试加分点:三次握手是为了防止已失效的连接请求到达服务器;四次挥手中客户端要等 2MSL(最大报文段生存时间)是为了确保最后一个 ACK 能到达服务器。
2. HTTP 和 HTTPS 的区别?⭐⭐⭐
| 特性 | HTTP | HTTPS |
|---|---|---|
| 端口 | 80 | 443 |
| 安全性 | ❌ 明文传输,可被窃听/篡改 | ✅ SSL/TLS 加密传输 |
| 证书 | 不需要 | 需要 CA 颁发的数字证书 |
| 性能 | 较快 | 略慢(TLS 1.3 已大幅优化) |
| SEO | 权重低 | ✅ Google/百度优先收录 |
| 数据完整性 | ❌ 可被中间人篡改 | ✅ 防篡改(MAC 校验) |
TLS 1.2 握手流程(2-RTT)
markdown
1. Client Hello:支持的加密套件 + 随机数 A + 支持的 TLS 版本
2. Server Hello:选定的加密套件 + 随机数 B + 数字证书(含公钥)
3. 客户端验证证书(CA 链验证),生成预主密钥(Pre-Master Secret),用公钥加密发送
4. 双方用 随机数A + 随机数B + 预主密钥 → 生成会话密钥(对称密钥)
5. 后续通信使用对称加密(AES)→ 速度快
TLS 1.3 的改进
diff
TLS 1.2:需要 2-RTT 握手
TLS 1.3:仅需 1-RTT 握手(甚至 0-RTT 恢复会话)
改进点:
- 移除不安全的加密算法(RSA 密钥交换、RC4、3DES 等)
- 握手过程从 2-RTT 优化到 1-RTT
- 支持 0-RTT 恢复(复用之前的会话密钥)
- 密钥交换只保留 ECDHE
💡 面试加分点:HTTPS 并非整个过程都用非对称加密,而是用非对称加密协商对称密钥,后续传输用对称加密(AES),因为对称加密速度快 100+ 倍。
3. HTTP/1.1、HTTP/2、HTTP/3 的区别?⭐⭐⭐
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 底层协议 | TCP | TCP | QUIC(基于 UDP) |
| 多路复用 | ❌ 队头阻塞 | ✅ 多路复用 | ✅ 无队头阻塞 |
| 头部压缩 | ❌ | ✅ HPACK | ✅ QPACK |
| 服务器推送 | ❌ | ✅ Server Push | ✅ |
| 传输格式 | 文本 | ✅ 二进制分帧 | ✅ 二进制 |
| 连接建立 | TCP 三次握手 | TCP 三次握手 | ✅ 0-RTT/1-RTT |
| 队头阻塞 | 应用层 + 传输层 | 仅传输层 | ✅ 彻底解决 |
arduino
HTTP/1.1 的问题:
├── 队头阻塞:一个请求卡住,后续请求排队等待
├── 头部冗余:每次都发送大量重复头部(Cookie等)
├── 文本传输:解析效率低
└── 只能客户端主动请求
HTTP/2 解决了应用层问题:
├── 多路复用:一个 TCP 连接并发多个流
├── HPACK 头部压缩:静态表 + 动态表 + 霍夫曼编码
├── 二进制分帧:Header帧 + Data帧
├── 服务器推送:主动推送关联资源
└── 流优先级:重要资源优先传输
HTTP/3 解决了传输层问题:
├── QUIC 协议:基于 UDP,内置 TLS 1.3
├── 0-RTT 连接:复用会话,首个请求就能带数据
├── 独立流:一个流丢包不影响其他流
└── 连接迁移:切换网络(WiFi→4G)连接不断
💡 面试加分点:HTTP/2 虽然解决了应用层队头阻塞,但 TCP 层的队头阻塞仍然存在(一个 TCP 包丢失会阻塞所有流),HTTP/3 用 QUIC(UDP) 彻底解决了这个问题。
4. 常见的 HTTP 状态码?⭐⭐⭐
按分类记忆
| 分类 | 范围 | 含义 |
|---|---|---|
| 1xx | 信息响应 | 请求已接收,继续处理 |
| 2xx | 成功 | 请求已成功处理 |
| 3xx | 重定向 | 需要进一步操作 |
| 4xx | 客户端错误 | 请求有问题 |
| 5xx | 服务器错误 | 服务器处理出错 |
高频状态码详解
| 状态码 | 名称 | 含义 | 常见场景 |
|---|---|---|---|
| 200 | OK | 请求成功 | GET 获取数据成功 |
| 201 | Created | 资源创建成功 | POST 创建数据成功 |
| 204 | No Content | 成功但无内容返回 | DELETE 删除成功 |
| 206 | Partial Content | 部分内容 | 断点续传/分片下载 |
| 301 | Moved Permanently | 永久重定向 | 域名迁移,浏览器会缓存 |
| 302 | Found | 临时重定向 | 短链接跳转 |
| 304 | Not Modified | 资源未修改,使用缓存 | 协商缓存命中 |
| 307 | Temporary Redirect | 临时重定向(保持方法) | HSTS 跳转 |
| 308 | Permanent Redirect | 永久重定向(保持方法) | API 版本迁移 |
| 400 | Bad Request | 请求参数错误 | 表单验证失败 |
| 401 | Unauthorized | 未认证 | Token 过期/未登录 |
| 403 | Forbidden | 无权限 | 非管理员访问后台 |
| 404 | Not Found | 资源不存在 | URL 拼写错误 |
| 405 | Method Not Allowed | 方法不允许 | 用 GET 调用 POST 接口 |
| 413 | Payload Too Large | 请求体过大 | 上传文件超过限制 |
| 429 | Too Many Requests | 请求频率过高 | 触发限流/反爬 |
| 500 | Internal Server Error | 服务器内部错误 | 后端代码报错 |
| 502 | Bad Gateway | 网关错误 | Nginx 无法连接后端 |
| 503 | Service Unavailable | 服务不可用 | 服务器维护/过载 |
| 504 | Gateway Timeout | 网关超时 | 后端响应超时 |
sql
❌ 常见混淆:
301 vs 302:301 浏览器会缓存跳转,302 每次都会请求原地址
301 vs 308:301 可能将 POST 改为 GET,308 严格保持原方法
401 vs 403:401 是"你是谁?"(未认证),403 是"你不能进"(已认证但无权限)
💡 面试加分点 :
304的完整流程 → 浏览器发请求带If-None-Match(ETag)或If-Modified-Since→ 服务器对比发现资源未变 → 返回 304(不带响应体)→ 浏览器使用本地缓存。
5. 浏览器缓存机制?⭐⭐⭐
缓存判断流程
csharp
浏览器请求资源
│
├── 强缓存命中?(检查 Cache-Control / Expires)
│ ├── ✅ 命中 → 直接使用缓存(200 from cache)
│ └── ❌ 未命中 ↓
│
├── 协商缓存?(发请求到服务器验证)
│ ├── 服务器返回 304 → 使用本地缓存
│ └── 服务器返回 200 → 使用新资源,更新缓存
│
└── 无缓存 → 正常请求资源
两种缓存机制对比
| 类型 | 强缓存 | 协商缓存 |
|---|---|---|
| 是否发请求 | ❌ 不发请求 | ✅ 发请求到服务器 |
| HTTP 头(优先) | Cache-Control: max-age=3600 |
ETag / If-None-Match |
| HTTP 头(备选) | Expires(绝对时间,已过时) |
Last-Modified / If-Modified-Since |
| 状态码 | 200 (from disk/memory cache) |
304 Not Modified |
| 适用场景 | 不常变化的静态资源 | 需要验证是否更新的资源 |
实战缓存策略
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|woff2)$ {
add_header Cache-Control "public, max-age=2592000"; # 30天
}
# ✅ API 接口:不缓存
location /api/ {
add_header Cache-Control "no-store";
}
arduino
Cache-Control 常用指令速查:
┌──────────────────┬──────────────────────────────┐
│ no-store │ 完全不缓存(连协商缓存都不要) │
│ no-cache │ 可以缓存,但每次都要向服务器验证 │
│ max-age=3600 │ 缓存有效期 3600 秒 │
│ public │ 任何中间节点都可以缓存(CDN等) │
│ private │ 只有浏览器能缓存 │
│ immutable │ 有效期内不验证(配合hash使用) │
│ s-maxage=3600 │ CDN 缓存有效期(覆盖 max-age) │
│ must-revalidate │ 过期后必须验证,不能用过期缓存 │
└──────────────────┴──────────────────────────────┘
💡 面试加分点 :
ETag比Last-Modified更精确,因为 Last-Modified 只精确到秒,如果 1 秒内修改了多次文件,Last-Modified 无法感知;而且有些文件定期重新生成但内容不变,Last-Modified 变了但 ETag 不变。
6. 什么是跨域?如何解决?
同源策略:协议 + 域名 + 端口 完全相同才算同源。
javascript
// 解决方案
// 1. CORS(后端设置响应头)
// 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)
// 2. 代理服务器(开发环境)
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
}
}
}
}
// 3. JSONP(只支持 GET,已过时)
function jsonp(url, callback) {
const script = document.createElement('script')
const callbackName = `jsonp_${Date.now()}`
window[callbackName] = (data) => {
callback(data)
delete window[callbackName]
script.remove()
}
script.src = `${url}?callback=${callbackName}`
document.head.appendChild(script)
}
// 4. postMessage(跨窗口通信)
// 发送
window.parent.postMessage({ type: 'login', data: userInfo }, 'https://parent.com')
// 接收
window.addEventListener('message', (e) => {
if (e.origin !== 'https://child.com') return
console.log(e.data)
})
// 5. Nginx 反向代理(生产环境)
// location /api/ {
// proxy_pass http://backend-server/;
// proxy_set_header Host $host;
// }
7. XSS 和 CSRF 攻击是什么?如何防御?
javascript
// XSS(跨站脚本攻击):注入恶意脚本
// 攻击:<script>document.cookie</script> 注入到页面
// 防御:
// 1. 转义 HTML 特殊字符
const escapeHtml = (str) => str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
// 2. CSP(内容安全策略)
// Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxx'
// 3. HttpOnly Cookie(JS 无法读取)
// Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict
// CSRF(跨站请求伪造):利用用户已登录的身份发起请求
// 攻击:恶意网站发起请求到目标网站,携带用户 Cookie
// 防御:
// 1. CSRF Token(每次请求携带随机 token)
// 2. SameSite Cookie
// Set-Cookie: token=xxx; SameSite=Strict
// 3. 验证 Referer/Origin 请求头
// 4. 双重 Cookie 验证
8. 前端性能优化有哪些方法?
javascript
// 1. 减少 HTTP 请求
// - 合并 CSS/JS 文件
// - 使用雪碧图(HTTP/1.1)
// - 内联小图片(base64)
// 2. 资源压缩
// - Gzip/Brotli 压缩
// - 图片压缩(WebP 格式)
// - 代码压缩(minify)
// 3. 缓存策略(见上方)
// 4. 懒加载
// 图片懒加载
// <img loading="lazy" src="image.jpg" alt="..." />
// Intersection Observer 实现懒加载
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
observer.unobserve(img)
}
})
})
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img))
// 5. 代码分割
// React:React.lazy + Suspense
// Vue:defineAsyncComponent
// Webpack:import() 动态导入
// 6. CDN 加速
// 将静态资源部署到 CDN,就近访问
// 7. 预加载/预连接
// <link rel="preload" href="critical.css" as="style" />
// <link rel="prefetch" href="next-page.js" />
// <link rel="preconnect" href="https://api.example.com" />
// <link rel="dns-prefetch" href="https://cdn.example.com" />
// 8. 减少重排重绘(见 HTML 部分)
// 9. Web Worker:将耗时计算移到后台线程
const worker = new Worker('worker.js')
worker.postMessage({ data: bigArray })
worker.onmessage = (e) => console.log('计算结果:', e.data)
9. 什么是 WebSocket?与 HTTP 的区别?
javascript
// WebSocket:全双工通信协议,服务器可以主动推送消息
// 建立连接
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.onclose = (event) => {
console.log('连接关闭:', event.code, event.reason)
// 自动重连
setTimeout(() => reconnect(), 3000)
}
ws.onerror = (error) => {
console.error('连接错误:', error)
}
// 发送消息
ws.send(JSON.stringify({ type: 'message', content: 'Hello' }))
// 关闭连接
ws.close(1000, '正常关闭')
// HTTP vs WebSocket
// HTTP:请求-响应模式,单向,无状态
// WebSocket:全双工,持久连接,低延迟
// 适用场景:聊天室、实时通知、在线游戏、股票行情
10. 什么是 Service Worker?
javascript
// Service Worker:运行在浏览器后台的脚本,可以拦截网络请求、实现离线缓存
// 注册 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW 注册成功:', reg.scope))
.catch(err => console.error('SW 注册失败:', err))
}
// sw.js
const CACHE_NAME = 'my-app-v1'
const STATIC_ASSETS = ['/', '/index.html', '/app.js', '/style.css']
// 安装:缓存静态资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
)
})
// 激活:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)))
)
)
})
// 拦截请求:缓存优先策略
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
if (cached) return cached // 返回缓存
return fetch(event.request).then(response => {
const clone = response.clone()
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone))
return response
})
})
)
})
11. 什么是 CDN?工作原理是什么?
markdown
CDN(内容分发网络)工作原理:
1. 用户请求 cdn.example.com/image.jpg
2. DNS 解析返回最近的 CDN 节点 IP
3. 用户从最近的 CDN 节点获取资源
4. 若 CDN 节点没有缓存,则从源站拉取并缓存
优势:
- 就近访问,减少延迟
- 减轻源站压力
- 提高可用性(多节点冗余)
- 防御 DDoS 攻击
前端使用 CDN 的最佳实践:
- 静态资源(JS/CSS/图片)使用 CDN
- 文件名包含 hash(内容变化则 URL 变化,避免缓存问题)
- 设置长期缓存(max-age=31536000)
- 使用 SRI(子资源完整性)验证
html
<!-- SRI:防止 CDN 资源被篡改 -->
<script
src="https://cdn.example.com/app.abc123.js"
integrity="sha384-xxxxx"
crossorigin="anonymous"
></script>
12. 什么是 RESTful API?
javascript
// REST 原则:
// 1. 无状态:每次请求包含所有必要信息
// 2. 统一接口:使用 HTTP 方法表示操作
// 3. 资源导向:URL 表示资源,不包含动词
// RESTful API 设计规范
// GET /users 获取用户列表
// GET /users/:id 获取单个用户
// POST /users 创建用户
// PUT /users/:id 完整更新用户
// PATCH /users/:id 部分更新用户
// DELETE /users/:id 删除用户
// 嵌套资源
// GET /users/:id/posts 获取用户的文章列表
// POST /users/:id/posts 为用户创建文章
// DELETE /users/:id/posts/:postId 删除用户的某篇文章
// 查询参数
// GET /users?page=1&size=10&sort=createdAt&order=desc&status=active
// 响应格式
const response = {
code: 0,
message: 'success',
data: {
list: [],
total: 100,
page: 1,
size: 10,
}
}
// 错误响应
const errorResponse = {
code: 400,
message: '参数错误',
errors: [
{ field: 'email', message: '邮箱格式不正确' }
]
}
13. 什么是 JWT(JSON Web Token)?
javascript
// JWT 结构:Header.Payload.Signature
// Header:算法和类型
// Payload:用户信息(不要存敏感数据)
// Signature:签名(防篡改)
// 示例 JWT
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
// eyJ1c2VySWQiOjEsIm5hbWUiOiLlvKDkuIkiLCJpYXQiOjE2MDAwMDAwMDAsImV4cCI6MTYwMDAwMzYwMH0.
// xxxxx
// 前端使用 JWT
const login = async (credentials) => {
const { data } = await axios.post('/api/auth/login', credentials)
localStorage.setItem('token', data.token)
axios.defaults.headers.Authorization = `Bearer ${data.token}`
}
// 请求拦截器自动添加 token
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// JWT 优缺点
// 优点:无状态、可扩展、跨域友好
// 缺点:无法主动失效(需要黑名单机制)、payload 可被解码(不要存敏感信息)
14. 什么是 OAuth 2.0?
markdown
OAuth 2.0 授权码流程(最安全):
1. 用户点击"使用微信登录"
2. 跳转到微信授权页面
3. 用户同意授权
4. 微信重定向回应用,携带 code
5. 应用后端用 code + client_secret 换取 access_token
6. 用 access_token 获取用户信息
7. 应用生成自己的 token 返回给前端
前端实现:
javascript
// 第一步:跳转到授权页面
const handleWechatLogin = () => {
const params = new URLSearchParams({
appid: 'wx_app_id',
redirect_uri: encodeURIComponent('https://myapp.com/callback'),
response_type: 'code',
scope: 'snsapi_userinfo',
state: generateRandomState(), // 防 CSRF
})
window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?${params}`
}
// 第二步:处理回调
const handleCallback = async () => {
const params = new URLSearchParams(window.location.search)
const code = params.get('code')
const state = params.get('state')
// 验证 state 防 CSRF
if (state !== sessionStorage.getItem('oauth_state')) {
throw new Error('Invalid state')
}
// 发送 code 到后端换取 token
const { data } = await axios.post('/api/auth/wechat', { code })
localStorage.setItem('token', data.token)
}
15. 浏览器的渲染进程有哪些线程?
markdown
浏览器渲染进程包含以下线程:
1. GUI 渲染线程:
- 解析 HTML/CSS,构建 DOM/CSSOM
- 布局、绘制
- 与 JS 引擎线程互斥(JS 执行时 GUI 暂停)
2. JS 引擎线程(V8):
- 执行 JavaScript 代码
- 与 GUI 渲染线程互斥
3. 事件触发线程:
- 管理事件队列
- 将事件回调放入任务队列
4. 定时器触发线程:
- setTimeout/setInterval 计时
- 计时完成后将回调放入任务队列
5. 异步 HTTP 请求线程:
- 处理 XMLHttpRequest/fetch
- 请求完成后将回调放入任务队列
这就是为什么 JS 执行时间过长会导致页面卡顿(GUI 渲染被阻塞)
javascript
// 解决 JS 阻塞渲染的方案
// 1. 将耗时任务分片(时间切片)
function processLargeArray(array, callback) {
const chunkSize = 1000
let index = 0
function processChunk() {
const end = Math.min(index + chunkSize, array.length)
for (; index < end; index++) {
callback(array[index])
}
if (index < array.length) {
requestAnimationFrame(processChunk) // 下一帧继续
}
}
requestAnimationFrame(processChunk)
}
// 2. 使用 Web Worker 在后台线程处理
const worker = new Worker('heavy-task.js')
worker.postMessage(largeData)
worker.onmessage = (e) => console.log('处理完成:', e.data)
// 3. 使用 requestIdleCallback 在空闲时执行
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
processTask(tasks.shift())
}
})