跳到主要内容

浏览器热门面试题

使用说明

本篇共 60 道题。

浏览器题通常把网络、进程、渲染、安全和生命周期串起来。回答要有主线,不必把每个实现细节都背成固定步骤。

Q1: 从输入 URL 到页面展示经历了什么?

答案

主线是解析 URL、检查缓存/策略、DNS 与连接、发送 HTTP 请求、接收响应、解析 HTML 并发现子资源、构建 DOM/CSSOM、布局、绘制和合成。

过程中还涉及重定向、Service Worker、TLS、脚本执行和优先级。面试时按层说明关键阻塞点,再根据追问展开。

Q2: 浏览器为什么采用多进程架构?

答案

把浏览器 UI、网络、渲染、GPU 和扩展等职责隔离,可以提高稳定性、安全性和并行能力。某个页面崩溃不一定拖垮整个浏览器。

代价是进程内存和跨进程通信成本。不同站点/页面是否共享进程由安全策略和资源调度决定,不宜死背固定数量。

  • 浏览器把 UI、网络、GPU、扩展和不同站点的渲染工作拆到多个进程,核心收益是故障隔离、安全沙箱和并行执行。
  • 代价是进程内存、IPC 与数据序列化成本;站点隔离还可能让同一站点的 iframe/页面按策略共享或拆分渲染进程。

Q3: 浏览器渲染流水线是什么?

答案

HTML 和 CSS 解析成 DOM/CSSOM,生成渲染树,计算几何布局,再生成绘制记录并把图层合成到屏幕。

JavaScript 修改样式可能让部分阶段失效。transform/opacity 常能只影响合成,但图层创建和绘制仍有成本。

Q4: 什么会阻塞页面首次渲染?

答案

构建当前视图需要的 CSS 会阻塞渲染;普通同步脚本可能阻塞 HTML 解析,并可能等待已发现的样式;字体和关键图片影响最终内容呈现。

优化方向是缩短关键请求链、减少关键 CSS/JS、正确设置脚本策略和资源优先级,而不是简单把所有东西异步化。

Q5: 强缓存和协商缓存有什么区别?

答案

对比项强缓存协商缓存
是否发请求否,直接使用本地缓存是,向服务器验证
状态码200 (from cache)304 Not Modified
控制字段Cache-Control, ExpiresETag, Last-Modified
性能最优(0 网络请求)较优(只传输头部)
使用场景不常变化的静态资源需要验证更新的资源

工作流程

// 伪代码:浏览器缓存判断逻辑
function handleRequest(url: string): Response {
const cache = getLocalCache(url);

if (!cache) {
return fetchFromServer(url); // 无缓存,请求服务器
}

// 1. 检查强缓存
if (cache.cacheControl && !isExpired(cache.cacheControl.maxAge)) {
return cache.response; // 强缓存命中,直接返回
}

// 2. 强缓存失效,进入协商缓存
const headers: Record<string, string> = {};
if (cache.etag) {
headers['If-None-Match'] = cache.etag;
}
if (cache.lastModified) {
headers['If-Modified-Since'] = cache.lastModified;
}

const response = fetchFromServer(url, { headers });

if (response.status === 304) {
return cache.response; // 协商缓存命中
}

return response; // 返回新资源
}

Q6: 浏览器有哪些主要存储方案?

答案

Cookie 会随匹配请求发送,适合小型会话标识;localStorage/sessionStorage 是同步字符串存储;IndexedDB 是异步结构化数据库;Cache Storage 面向请求响应缓存。

选择看容量、同步成本、生命周期、事务和是否随请求发送。敏感信息不能因为“在本地”就默认安全。

答案

SameSite 限制跨站请求携带 Cookie,降低部分 CSRF 风险;HttpOnly 阻止脚本直接读取;Secure 只允许 HTTPS 发送。

它们应组合使用并设置正确 Domain/Path/过期时间,但不能替代服务端鉴权、CSP 和 XSS 防御。

Q8: 同源策略保护了什么?

答案

它限制一个源的脚本读取另一个源的敏感响应和 DOM/存储。源由协议、主机和端口共同决定。

它不等于“不能发跨域请求”,表单、图片等仍可发送;核心限制常是读取。跨源共享通过 CORS、postMessage 等受控机制开放。

  • 同源由协议、主机和端口共同决定。策略主要限制脚本读取跨源 DOM、响应和存储,但表单、图片、脚本等部分跨源发送或嵌入仍被允许。
  • CORS 是服务器授权浏览器读取响应,postMessage 是受控跨窗口通信;JSONP 只能 GET 且会执行第三方脚本,不应作为现代通用方案。

Q9: CORS 的简单请求和预检请求是什么?

答案

特性简单请求预检请求
请求数量1 次2 次(OPTIONS + 实际请求)
触发条件GET/HEAD/POST + 简单头其他方法或自定义头
Content-Type三种简单类型application/json

简单请求条件

// 1. 方法是 GET、HEAD、POST
// 2. 头只有:Accept, Accept-Language, Content-Language, Content-Type
// 3. Content-Type 是:text/plain, multipart/form-data, application/x-www-form-urlencoded

Q10: XSS 和 CSRF 的区别是什么?

答案

特性XSSCSRF
攻击方式注入恶意脚本伪造用户请求
攻击目标用户浏览器服务器
利用的是用户对网站的信任网站对用户的信任
是否需要登录不需要需要(利用登录状态)
能做什么任意 JS 能做的事只能发送请求

简单记忆

  • XSS:攻击者的代码在你的网站执行
  • CSRF:攻击者让你帮他发请求

Q11: CSP 能解决什么问题?

答案

Content Security Policy 通过响应头声明页面允许加载和执行哪些脚本、样式、图片、框架和网络目标,是 XSS、内容注入和数据外传的重要缓解层

高质量策略通常从以下能力入手:

  • 脚本使用 nonce 或 hash,减少 unsafe-inline
  • object-src 'none'base-uriframe-ancestors 收紧高风险入口。
  • 通过 Report-Only 和上报端点观察违规,再逐步收紧。
  • 可结合 Trusted Types 限制危险 DOM 注入 API。

CSP 不能替代输出编码、输入校验和安全 DOM API;策略允许了恶意来源或复用了可预测 nonce,仍可能被绕过。不要只写一条宽松白名单就认为“已经防住 XSS”。

Q12: 事件捕获、目标和冒泡是什么?

答案

事件从外层沿捕获阶段到目标,再沿祖先冒泡。事件委托利用冒泡在共同祖先处理动态子项,减少监听器并统一逻辑。

stopPropagation 会影响其他处理器,应谨慎使用;影子 DOM 还涉及事件是否 composed 和 retargeting。

Q13: preventDefaultstopPropagation 有什么区别?

答案

前者阻止浏览器默认行为,例如链接跳转或表单提交;后者阻止事件继续传播。二者互不等价。

被动监听器中不能取消默认滚动。业务代码不要用阻止传播掩盖组件边界不清,否则会破坏上层埋点和交互。

  • preventDefault() 取消链接跳转、表单提交等默认动作;stopPropagation() 只阻止事件继续在捕获/冒泡路径传播,二者互不替代。
  • 被动监听器中不能取消默认滚动;同一节点还有 stopImmediatePropagation() 可阻止后续监听器,但应谨慎使用。

Q14: Web Worker 适合什么任务?

答案

Worker 在独立线程运行 JavaScript,适合 CPU 密集计算、解析和数据转换,避免阻塞主线程交互。

它不能直接操作 DOM,数据通过消息和结构化克隆/Transferable 传递。小任务的启动和通信成本可能高于收益。

  • Worker 适合 CPU 密集计算、解析、压缩和离屏绘制,它有独立线程和事件循环,不能直接访问 DOM。
  • 主线程与 Worker 通过结构化克隆通信,大数据应使用 Transferable 或 SharedArrayBuffer;任务过小会被创建与通信成本抵消。

Q15: History 路由和 Hash 路由有什么区别?

答案

History 模式用 pushState/replaceState 改正常路径,需要服务端回退入口;Hash 模式使用 URL 片段,不会作为普通路径发给服务端,部署简单但 URL 语义受限。

二者都要处理前进后退、滚动恢复、状态同步和真正的 404。

Q16: 页面生命周期中 visibilitychangepagehidebeforeunload 怎么用?

答案

visibilitychange 适合页面进入后台时暂停工作或保存轻量状态;pagehide 更适合导航离开并兼容往返缓存;beforeunload 主要用于确有未保存数据时提示。

不要依赖卸载事件发送关键数据,它们不保证执行。上报可使用 sendBeacon,业务一致性仍由服务端保证。

Q17: 什么是 bfcache?

答案

bfcache(后退/前进缓存):浏览器缓存页面完整状态,后退时瞬间恢复。

// 使用 pageshow/pagehide 而非 load/unload
window.addEventListener('pageshow', (e) => {
if (e.persisted) {
// 从 bfcache 恢复
refreshData();
}
});

window.addEventListener('pagehide', (e) => {
if (e.persisted) {
// 即将进入 bfcache
}
});

// ⚠️ 避免使用 unload,会阻止 bfcache

Q18: requestAnimationFrame 为什么适合动画?

答案

特性requestAnimationFramesetTimeout/setInterval
执行时机下一帧渲染前指定延迟后
帧率与屏幕刷新同步不同步,可能掉帧
后台暂停继续(节流)
精度
回调参数时间戳

Q19: 浏览器多标签页如何通信?

答案

同源页面可用 BroadcastChannel、storage 事件、SharedWorker、Service Worker 消息或 IndexedDB 协调;有父子/打开关系时可用 postMessage。

选择看兼容性、生命周期和是否需要共享计算。所有消息都要校验来源和结构,并处理重复、乱序与页面退出。

Q20: 浏览器指纹是什么?

答案

浏览器指纹是通过收集浏览器和设备特征生成的唯一标识符。

用途

  • 反欺诈:检测恶意用户
  • 广告追踪:无 Cookie 追踪
  • 安全验证:辅助身份认证
  • 数据分析:访客统计

采集维度

  • 基础信息(UA、语言、时区)
  • Canvas/WebGL 渲染
  • 音频处理
  • 字体列表
  • 硬件信息

Q21: 什么是关键渲染路径?如何优化?

答案

关键渲染路径(Critical Rendering Path)是浏览器将 HTML、CSS、JavaScript 转换为屏幕像素的步骤序列:

  1. 构建 DOM 树:解析 HTML
  2. 构建 CSSOM 树:解析 CSS
  3. 构建渲染树:合并 DOM 和 CSSOM
  4. 布局:计算几何信息
  5. 绘制:转换为像素
  6. 合成:图层合成

优化策略

优化方向具体措施
减少关键资源内联关键 CSS、延迟非关键 JS
减少关键路径长度减少请求数量、使用 HTTP/2
减少关键字节数压缩、Tree Shaking、代码分割
优先关键内容首屏内容优先加载
<!-- 优化示例 -->
<head>
<!-- 内联关键 CSS -->
<style>
.header { /* 首屏样式 */ }
.hero { /* 首屏样式 */ }
</style>

<!-- 异步加载非关键 CSS -->
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">

<!-- 预连接关键域名 -->
<link rel="preconnect" href="https://api.example.com">

<!-- defer 加载 JS -->
<script defer src="app.js"></script>
</head>

Q22: Vary 响应头为什么会影响缓存命中?

答案

Vary 告诉缓存:除了 URL,还要把哪些请求头纳入缓存键。例如 Vary: Accept-Encoding 让 gzip 与 Brotli 响应分别缓存;Vary: Accept-Language 区分语言版本。

如果遗漏必要维度,缓存可能把错误内容返回给另一类请求;如果加入高基数或不稳定请求头,例如完整 User-Agent、Cookie,缓存键会碎片化,命中率显著下降。

使用 CDN 时还要确认边缘缓存是否遵守或重写 Vary。个性化响应优先使用明确的私有缓存策略,不要靠一个巨大的 Vary: Cookie 勉强隔离。

Q23: Cookie、localStorage、sessionStorage 的区别?

答案

对比项CookielocalStoragesessionStorage
存储大小~4KB~5MB~5MB
过期时间可设置永久会话结束
服务器通信自动携带不参与不参与
作用域可跨子域同源同源且同标签页

Q24: 把 script 放在 body 底部和使用 defer 有什么区别?

答案

<!-- 方式一:放在 body 底部 -->
<body>
<!-- 页面内容 -->
<script src="app.js"></script>
</body>

<!-- 方式二:使用 defer -->
<head>
<script defer src="app.js"></script>
</head>
对比项body 底部defer
下载时机HTML 解析到 script 标签时HTML 解析开始时(并行)
执行时机下载完立即执行HTML 解析完成后
性能较慢(串行)较快(并行下载)

结论defer 更优,因为可以并行下载,不会等到 body 底部才开始下载。

Q25: 浏览器有哪些进程?各自的作用是什么?

答案

进程主要职责
浏览器主进程管理 UI、标签页、协调其他进程、存储管理
渲染进程解析 HTML/CSS、执行 JS、计算布局、绑定绘制
GPU 进程图形渲染、硬件加速、合成页面
网络进程处理网络请求、DNS 解析、缓存管理
插件进程运行浏览器插件(已逐渐废弃)
扩展进程运行浏览器扩展的后台脚本

Q26: Navigation Timing 能怎样拆解一次页面导航?

答案

PerformanceNavigationTiming 提供一次文档导航各阶段时间点,可拆解重定向、DNS、连接/TLS、请求等待、响应下载、DOM 解析和 load 等阶段。

排查时计算区间而不是只看单个时间戳,例如:

  • DNS:domainLookupEnd - domainLookupStart
  • TCP/TLS:connectEnd - connectStart
  • TTFB:responseStart - requestStart
  • 下载:responseEnd - responseStart
  • DOM 处理:结合 domInteractivedomContentLoadedEventEnd

缓存、连接复用、跨源权限和 Service Worker 会影响部分字段。它只能解释文档导航,资源明细还要结合 Resource Timing,用户体验再结合 Core Web Vitals。

Q27: 跨源请求一定会被浏览器阻止发送吗?

答案

不一定。同源策略主要限制一个源的脚本读取另一个源的敏感响应和访问其 DOM/存储,而不是禁止所有跨源网络行为。

图片、表单、导航和部分 fetch 请求都可能实际发到服务器;对于需要预检的 fetch,浏览器会先发送 OPTIONS,预检不通过时才不发送后续实际请求。简单跨源请求通常先发送,再由 CORS 响应头决定脚本能否读取结果。

因此服务端不能把 CORS 当成鉴权或 CSRF 防线。会产生副作用的接口仍要校验身份、来源意图、CSRF Token/SameSite、权限和内容类型。

Q28: 如何实现多标签页之间的数据同步?

答案

同源场景下有 4 种主流方案:

// 方案 1:BroadcastChannel(推荐)
const bc = new BroadcastChannel('sync');
bc.postMessage({ type: 'SYNC', data: newData });
bc.onmessage = (e) => updateUI(e.data);

// 方案 2:localStorage 事件
localStorage.setItem('sync', JSON.stringify({ data: newData, t: Date.now() }));
window.addEventListener('storage', (e) => {
if (e.key === 'sync') updateUI(JSON.parse(e.newValue!));
});

// 方案 3:SharedWorker(需要中心状态)
const sw = new SharedWorker('/worker.js');
sw.port.postMessage(newData);
sw.port.onmessage = (e) => updateUI(e.data);

// 方案 4:Service Worker
navigator.serviceWorker.controller?.postMessage(newData);
navigator.serviceWorker.addEventListener('message', (e) => updateUI(e.data));

Q29: Trusted Types 解决什么问题?

答案

Trusted Types 用浏览器策略限制 innerHTMLinsertAdjacentHTML 等危险 DOM 注入点只能接收经过受信策略创建的值,帮助把 DOM XSS 风险集中到少量审计入口。

它通常通过 CSP 的 require-trusted-types-for 'script' 启用。迁移步骤是先用 Report-Only 找到现有注入点,再改用 textContent、安全 DOM API 或经过严格 Sanitizer 的策略。

Trusted Types 不是自动清洗器:如果策略函数直接返回未经校验的字符串,仍然不安全;它也不能解决服务端模板注入、越权或所有脚本逻辑漏洞。

Q30: Shadow DOM 中的事件 retargeting 是什么?

答案

事件从 Shadow DOM 穿过边界时,浏览器可能把外部监听器看到的 event.target 重定向为 Shadow Host,隐藏内部实现细节,这就是 retargeting。

事件能否穿过边界还取决于 composed;可用 event.composedPath() 查看经过封装处理后的传播路径。自定义事件默认不会自动穿出,应按组件契约显式设置 bubblescomposed

外部代码不应依赖组件内部节点结构做事件委托。组件应对外派发语义明确的事件,并校验事件数据和取消语义。

Q31: Web Worker 和主线程如何通信?

答案

通过 postMessageonmessage 进行通信:

// 主线程
worker.postMessage(data);
worker.onmessage = (e) => console.log(e.data);

// Worker 线程
self.onmessage = (e) => console.log(e.data);
self.postMessage(result);

数据传递方式

  1. 结构化克隆(默认):深拷贝数据
  2. Transferable:转移所有权,零拷贝
// 转移 ArrayBuffer
worker.postMessage(buffer, [buffer]);

Q32: Navigation API 相比直接操作 History API 有什么价值?

答案

History API 只提供 pushStatereplaceStatepopstate 等较低层能力,应用还要自行统一链接点击、前进后退、取消导航和错误处理。Navigation API 试图提供更一致的导航事件、目标条目、拦截和异步过渡模型。

它适合需要精细管理 SPA 导航生命周期的场景,但不能假设所有目标浏览器都支持。生产中应:

  • 先检查兼容性和框架集成,保留 History/完整导航回退。
  • 拦截后明确处理取消、异常、并发导航和滚动/焦点恢复。
  • 不破坏普通链接的打开方式、可访问性和服务端路由。
  • 把实验能力隔离在路由适配层,不让业务组件直接依赖。

是否采用取决于浏览器矩阵和路由框架,而不是因为 API 更新就全面重写。

Q33: DOMContentLoaded 和 load 事件的区别?

答案

特性DOMContentLoadedload
触发时机DOM 树构建完成所有资源加载完成
等待资源不等待图片、CSS等待所有资源
触发顺序先触发后触发
常用场景DOM 操作、事件绑定获取元素尺寸、性能统计
document.addEventListener('DOMContentLoaded', () => {
// DOM 操作
});

window.addEventListener('load', () => {
// 获取图片尺寸等
});

Q34: requestIdleCallback 适合什么任务?有什么限制?

答案

它让浏览器在主线程相对空闲时执行低优先级、可延后的工作,并通过 IdleDeadline.timeRemaining() 告诉回调当前预算。适合分批预计算、非关键缓存整理和遥测处理。

限制是调用时间不确定,后台页面可能被强烈节流,兼容性也不能假设覆盖所有环境。关键业务、必须在指定时限完成的保存和用户可见更新不应依赖它。

任务仍要切成小块并检查预算;需要截止时间可配置 timeout,但超时回调同样可能占用主线程。调度 API 只能选择时机,不能让重计算变便宜。

Q35: 什么是渐进增强和优雅降级?

答案

策略描述适用场景
渐进增强先保证基础功能,再为现代浏览器增强内容优先的网站
优雅降级先实现完整功能,再为旧浏览器做降级功能优先的应用
/* 渐进增强 */
.box { border: 1px solid #000; }
@supports (box-shadow: 0 0 5px #000) {
.box { box-shadow: 0 0 5px #000; }
}

/* 优雅降级 */
.box { box-shadow: 0 0 5px #000; }
@supports not (box-shadow: 0 0 5px #000) {
.box { border: 1px solid #000; }
}

Q36: 浏览器指纹为什么涉及隐私和稳定性问题?

答案

指纹通过组合 UA、语言、时区、屏幕、字体、Canvas/WebGL 等特征尝试识别设备。维度越多不一定越可靠:浏览器升级、显示器切换、隐私模式和反指纹策略都会让结果漂移,多台相似设备也可能碰撞。

它还可能形成用户未明确同意的跨站追踪。产品使用时要有合法目的、最小化采集、明确保留周期和访问权限,并评估浏览器隐私机制。

安全风控只能把指纹当作一个风险信号,不能当作强身份凭证;关键操作仍需账号认证、设备绑定、挑战和异常行为分析。

Q37: WebSocket 和 HTTP 有什么区别?

答案

特性WebSocketHTTP
连接持久连接短连接
通信全双工请求-响应
头部轻量每次请求带完整头部
协议ws/wsshttp/https
发起方双方都可只能客户端
// HTTP: 每次请求都要建立连接
fetch('/api/data'); // 连接 → 请求 → 响应 → 关闭

// WebSocket: 一次连接,持续通信
const ws = new WebSocket('wss://...');
ws.send('message1');
ws.send('message2'); // 复用同一连接

Q38: 重排和重绘有什么区别?如何减少?

答案

对比项重排(Reflow)重绘(Repaint)
触发条件几何属性变化视觉属性变化
影响范围可能影响整个页面只影响当前元素
性能开销
包含步骤Layout → Paint → CompositePaint → Composite

减少重排的方法

// 1. 批量修改样式
element.style.cssText = 'width: 100px; height: 100px;';
element.classList.add('new-class');

// 2. 避免强制同步布局
// 先读后写,不要交替
const width = element.offsetWidth;
element.style.width = width + 10 + 'px';

// 3. 使用 transform 代替位置属性
element.style.transform = 'translateX(100px)';

// 4. 脱离文档流操作
element.style.display = 'none';
// 多次 DOM 操作
element.style.display = 'block';

// 5. 使用 DocumentFragment
const fragment = document.createDocumentFragment();
// 批量添加节点
document.body.appendChild(fragment);

Q39: Cache-Control 的 no-cache 和 no-store 有什么区别?

答案

指令含义是否缓存使用场景
no-cache使用缓存前必须向服务器验证✅ 缓存HTML 入口文件
no-store完全禁止缓存❌ 不缓存敏感数据(银行页面)
// no-cache:每次都要验证,但会存储缓存
// 适用于需要确保获取最新版本,但可以利用 304 优化的场景
res.setHeader('Cache-Control', 'no-cache');

// no-store:完全不缓存,每次都重新下载
// 适用于敏感数据,如银行账户信息
res.setHeader('Cache-Control', 'no-store');

// 常见误解
// ❌ 错误理解:no-cache 表示不缓存
// ✅ 正确理解:no-cache 表示缓存但需验证

流程对比

Q40: 什么时候用 IndexedDB?容量可以假设为固定值吗?

答案

IndexedDB 适合大量结构化数据、索引查询、事务和离线应用,也可在 Worker 中使用。小型同步配置用 localStorage 更简单,但不能在主线程存取大型数据。

浏览器存储配额不是固定的“5MB”。它受浏览器、设备剩余空间、站点参与度、隐私模式和持久化授权影响。应使用 Storage API 的 navigator.storage.estimate() 观察估算值,并在关键离线场景评估 persist()

应用必须处理配额不足、事务失败、Schema 升级、多标签页版本冲突和数据淘汰;服务端仍是重要数据的最终来源。

Q41: async 和 defer 同时使用会怎样?

答案

<script async defer src="script.js"></script>
  • 如果浏览器支持 async,使用 async 行为
  • 如果不支持 async(老浏览器),降级使用 defer
  • 这是一种兼容性写法

Q42: Site Isolation 和进程沙箱分别解决什么问题?

答案

进程沙箱限制渲染进程即使被攻破后可访问的系统能力;Site Isolation 尽量把不同站点的文档放到不同渲染进程,降低跨站数据在同一进程内被旁路读取的风险。

二者是互补的纵深防御。站点隔离会增加进程和内存成本,也让跨源 iframe 通信更多依赖 IPC;浏览器会根据平台和资源情况调整进程分配,不能死背“一标签一进程”。

前端仍要正确使用 COOP/COEP、CSP、iframe sandbox 和 postMessage 来源校验,不能把所有隔离责任交给浏览器进程模型。

Q43: 如何优化从 URL 到页面展示的性能?

答案

网络层优化

阶段优化策略
DNSDNS 预解析、使用 CDN
TCPKeep-Alive、HTTP/2 多路复用
TLSTLS 1.3、Session 复用
HTTPGzip 压缩、缓存策略、资源合并

渲染层优化

阶段优化策略
HTML减少 DOM 深度和节点数
CSS避免复杂选择器、减少重排
JavaScript代码分割、延迟加载、Web Worker
布局避免强制同步布局
绘制使用 transform/opacity 动画
// 关键渲染路径优化
// 1. 内联关键 CSS
<style>
/* 首屏关键样式 */
.header { ... }
.hero { ... }
</style>

// 2. 延迟非关键 CSS
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">

// 3. 延迟 JavaScript
<script defer src="app.js"></script>

// 4. 预加载关键资源
<link rel="preload" href="hero.jpg" as="image">

Q44: postMessage 有哪些安全风险?如何防范?

答案

风险防范措施
消息伪造严格验证 event.origin
代码注入不要 eval 消息内容
信息泄露不要用 targetOrigin: '*'
原型污染用 Zod 等校验消息结构
// 安全的 postMessage 处理
const ALLOWED_ORIGINS = ['https://trusted.com', 'https://app.trusted.com'];

window.addEventListener('message', (event) => {
// 1. 验证来源
if (!ALLOWED_ORIGINS.includes(event.origin)) return;

// 2. 校验数据格式
const result = z.object({
type: z.enum(['INIT', 'UPDATE', 'CLOSE']),
payload: z.unknown(),
}).safeParse(event.data);

if (!result.success) return;

// 3. 按类型处理
handleMessage(result.data);
});

Q45: 如何防御 XSS 攻击?

答案

  1. 输出编码:对用户输入进行 HTML 实体编码
// 将 < > " ' 等转义
escapeHtml(userInput);
  1. 使用安全 API
element.textContent = userInput;  // 不是 innerHTML
  1. CSP(内容安全策略)
Content-Security-Policy: default-src 'self'; script-src 'self'
  1. HttpOnly Cookie:防止脚本读取 Cookie

Q46: event.target 和 event.currentTarget 的区别?

答案

  • event.target触发事件的元素(被点击的那个)
  • event.currentTarget绑定事件的元素(监听器所在的)
// <ul onclick="...">
// <li>Item</li> ← 点击这里
// </ul>

list.addEventListener('click', (e) => {
// 点击 li 时:
e.target; // li(实际点击的)
e.currentTarget; // ul(绑定事件的)
});

Q47: Web Worker、Shared Worker、Service Worker 的区别?

答案

特性Web WorkerShared WorkerService Worker
作用域单页面多页面共享整个源
生命周期页面关闭销毁连接关闭销毁独立于页面
主要用途CPU 密集计算跨页面通信离线缓存、推送
可访问 DOM
可拦截请求

Q48: History 模式刷新页面为什么会 404?如何解决?

答案

原因:刷新时浏览器向服务器请求 /user/123,服务器没有这个路径的文件。

解决方案:服务端配置所有路由回退到 index.html

# Nginx
location / {
try_files $uri $uri/ /index.html;
}
// Express
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'dist', 'index.html'));
});

Q49: 如何正确检测页面是否可见?

答案

// 使用 visibilitychange 事件
document.addEventListener('visibilitychange', () => {
const isVisible = document.visibilityState === 'visible';
console.log('页面可见:', isVisible);
});

// 也可以检查 document.hidden
if (document.hidden) {
console.log('页面不可见');
}

应用场景

  • 暂停视频播放
  • 停止动画
  • 暂停轮询请求
  • 统计页面停留时间

Q50: requestAnimationFrame 动画如何做到与刷新率无关?

答案

关键是使用 rAF 回调提供的时间戳计算时间差,而不是“每帧固定移动几个像素”。例如速度为每秒 120 像素时,可以按 position += 120 * deltaSeconds 更新,这样 60Hz、120Hz 屏幕上的运动速度基本一致。

实现时通常还要注意:

  1. 保存上一帧时间戳,第一帧只初始化,不直接累加。
  2. 页面从后台恢复时,时间差可能很大,应对 delta 设置合理上限,避免对象瞬间跳跃。
  3. 物理模拟对稳定性要求更高时,使用固定时间步长累计计算,渲染时再插值;普通 UI 动画按实际时间推进即可。
  4. 不要假设刷新率是 60Hz,也不要用帧数代表持续时间。

rAF 只决定视觉更新时机。回调本身过重仍会掉帧,因此计算、布局、绘制成本也需要控制。

Q51: Polyfill 和 Transpiler 的区别?

答案

类型作用示例
Polyfill运行时补充缺失的 APIPromisefetchArray.includes
Transpiler编译时转换语法箭头函数 → 普通函数,async/await → Promise
// Polyfill: 添加缺失的方法
if (!Array.prototype.includes) {
Array.prototype.includes = function() { /* ... */ };
}

// Transpiler: 转换语法
// 源码: const fn = () => {};
// 编译后: var fn = function() {};

Q52: Canvas 指纹的原理是什么?

答案

Canvas 指纹利用不同设备渲染图像的微小差异

  1. 使用 Canvas 绘制文本和图形
  2. 提取像素数据
  3. 生成哈希值

差异来源

  • GPU 型号和驱动
  • 字体渲染引擎
  • 抗锯齿算法
  • 操作系统
const ctx = canvas.getContext('2d');
ctx.fillText('Hello', 10, 10);
const dataURL = canvas.toDataURL(); // 不同设备结果不同

Q53: WebSocket 如何实现心跳检测?

答案

class HeartbeatWebSocket {
private ws: WebSocket;
private heartbeatTimer: number | null = null;
private pongTimeout: number | null = null;

connect(url: string): void {
this.ws = new WebSocket(url);

this.ws.onopen = () => {
this.startHeartbeat();
};

this.ws.onmessage = (e) => {
if (e.data === 'pong') {
this.clearPongTimeout();
}
};

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

private startHeartbeat(): void {
this.heartbeatTimer = window.setInterval(() => {
this.ws.send('ping');

// 等待 pong 响应
this.pongTimeout = window.setTimeout(() => {
console.log('心跳超时,断开连接');
this.ws.close();
}, 5000);
}, 30000);
}

private clearPongTimeout(): void {
if (this.pongTimeout) {
clearTimeout(this.pongTimeout);
this.pongTimeout = null;
}
}

private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
this.clearPongTimeout();
}
}

Q54: transformopacity 动画为什么通常更流畅?

答案

它们不改变其他元素的几何布局,并且在元素已被合适分层和栅格化时,浏览器可能只更新合成参数,跳过每帧 Layout 和 Paint。

“通常”很重要:首次绘制、大面积透明层、滤镜、图层提升和纹理上传仍有成本,元素也不保证永远独占合成层。will-change 会消耗资源,不应长期全局开启。

应通过 Performance 和 Layers 工具确认每帧实际发生了什么,关注主线程时间、Paint、显存和过度绘制,而不是把“GPU 加速”当作免费优化。

Q55: 如何设计一个最优的前端缓存策略?

答案

核心原则

  1. HTML 文件:使用协商缓存(no-cache),确保入口始终最新
  2. 静态资源:使用内容哈希 + 长期强缓存(immutable
  3. API 响应:根据业务需求设置,敏感数据使用 no-store

完整配置示例

// server.ts - Express 缓存配置
import express from 'express';
import path from 'path';

const app = express();

// 设置不同资源的缓存策略
app.use((req, res, next) => {
const url = req.url;

if (url.endsWith('.html') || url === '/') {
// HTML:协商缓存,确保获取最新版本
res.setHeader('Cache-Control', 'no-cache');
} else if (/\.[a-f0-9]{8,}\.(js|css)$/.test(url)) {
// 带 hash 的 JS/CSS:强缓存 1 年 + immutable
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
} else if (/\.(js|css)$/.test(url)) {
// 不带 hash 的 JS/CSS:协商缓存
res.setHeader('Cache-Control', 'no-cache');
} else if (/\.(png|jpg|gif|svg|ico|woff2?|ttf|eot)$/.test(url)) {
// 图片和字体:强缓存 1 年
res.setHeader('Cache-Control', 'public, max-age=31536000');
}

next();
});

// API 路由
app.use('/api', (req, res, next) => {
// API 默认不缓存
res.setHeader('Cache-Control', 'no-store');
next();
});

app.use(express.static('dist'));

Nginx 配置参考

# HTML 文件
location ~* \.html$ {
add_header Cache-Control "no-cache";
}

# 带 hash 的静态资源
location ~* \.[a-f0-9]{8}\.(js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

# 图片和字体
location ~* \.(png|jpg|gif|svg|woff2?)$ {
add_header Cache-Control "public, max-age=31536000";
}

# API
location /api/ {
add_header Cache-Control "no-store";
}

Q56: SharedWorker 的生命周期和适用场景是什么?

答案

同源的多个页面可以连接到同一个 SharedWorker,通过各自的 MessagePort 通信,因此它适合共享 WebSocket、统一后台计算或跨标签页协调状态。

Worker 是否继续存活取决于是否还有页面连接和浏览器策略,不能把它当成永久后台服务。页面需要处理重新连接、端口关闭、消息版本和 Worker 重启后的状态恢复。

它的支持范围和调试体验不如普通 Worker 普遍。只需要广播小消息时优先 BroadcastChannel;需要拦截网络和离线缓存时使用 Service Worker;需要共享计算/长连接时再评估 SharedWorker。

Q57: 动态创建的 script 默认是什么行为?

答案

const script = document.createElement('script');
script.src = 'app.js';
document.body.appendChild(script);

动态创建的脚本默认是 async 行为,如果需要按顺序执行:

const script = document.createElement('script');
script.src = 'app.js';
script.async = false; // 改为同步,按顺序执行
document.body.appendChild(script);

Q58: 渲染进程中有哪些线程?主线程阻塞会有什么影响?

答案

渲染进程的主要线程

线程职责
主线程执行 JS、解析 HTML/CSS、样式计算、Layout、Paint 指令生成、事件派发——前端所有性能问题基本都在这。所谓"JS 引擎线程和 GUI 渲染线程互斥"本质是因为两者就在同一个主线程上交替执行。
合成线程处理滚动、CSS 动画等合成操作
光栅化线程将绘制指令转换为位图
Worker 线程执行 Web Workers

主线程阻塞的影响

// 阻塞主线程的示例
function blockMainThread(): void {
const start = Date.now();
while (Date.now() - start < 5000) {
// 阻塞 5 秒
}
}

// 影响:
// 1. 页面无法响应用户点击、滚动等交互
// 2. 动画卡顿(除了 CSS transform/opacity 动画)
// 3. 输入框无法输入
// 4. 新的网络请求回调无法执行

解决方案

// 1. 使用 Web Worker 处理耗时计算
const worker = new Worker('heavy-task.js');
worker.postMessage(data);
worker.onmessage = (e) => console.log(e.data);

// 2. 将大任务拆分为小任务
async function processInChunks(items: unknown[]): Promise<void> {
for (const item of items) {
processItem(item);
// 每处理完一项,让出主线程
await new Promise(resolve => setTimeout(resolve, 0));
}
}

// 3. 使用 requestIdleCallback 在空闲时执行
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0) {
doBackgroundWork();
}
});

Q59: 现代页面应该怎样安排 CSS 和 JavaScript 的加载?

答案

“CSS 一律放 head、JavaScript 一律放 body 底部”是历史经验,不是完整规则。

  • 首屏所需样式应尽早发现和加载,避免无样式闪烁;非关键样式可按路由拆分,但要控制样式切换和布局偏移。
  • 应用脚本通常使用 type="module"defer,让下载与 HTML 解析并行,并保持可预测执行顺序。
  • 独立且无依赖的第三方脚本才适合 async
  • 关键资源可按测量结果使用 preload、preconnect 或 fetchpriority,但过量提示会争抢带宽。
  • 最终根据关键请求链、LCP、主线程执行和依赖顺序验证,而不是只移动标签位置。

Q60: 跨域请求能发出去吗?

答案

能发出去! 跨域请求的限制是浏览器不让你读取响应,而不是阻止请求发送。

这也是为什么 CSRF 攻击能成功——请求确实发送了,服务端也处理了。

相关链接