浏览器热门面试题
使用说明
本篇共 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, Expires | ETag, 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 面向请求响应缓存。
选择看容量、同步成本、生命周期、事务和是否随请求发送。敏感信息不能因为“在本地”就默认安全。
Q7: Cookie 的 SameSite、HttpOnly 和 Secure 分别有什么用?
答案:
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 的区别是什么?
答案:
| 特性 | XSS | CSRF |
|---|---|---|
| 攻击方式 | 注入恶意脚本 | 伪造用户请求 |
| 攻击目标 | 用户浏览器 | 服务器 |
| 利用的是 | 用户对网站的信任 | 网站对用户的信任 |
| 是否需要登录 | 不需要 | 需要(利用登录状态) |
| 能做什么 | 任意 JS 能做的事 | 只能发送请求 |
简单记忆:
- XSS:攻击者的代码在你的网站执行
- CSRF:攻击者让你帮他发请求
Q11: CSP 能解决什么问题?
答案:
Content Security Policy 通过响应头声明页面允许加载和执行哪些脚本、样式、图片、框架和网络目标,是 XSS、内容注入和数据外传的重要缓解层。
高质量策略通常从以下能力入手:
- 脚本使用 nonce 或 hash,减少
unsafe-inline。 - 用
object-src 'none'、base-uri、frame-ancestors收紧高风险入口。 - 通过 Report-Only 和上报端点观察违规,再逐步收紧。
- 可结合 Trusted Types 限制危险 DOM 注入 API。
CSP 不能替代输出编码、输入校验和安全 DOM API;策略允许了恶意来源或复用了可预测 nonce,仍可能被绕过。不要只写一条宽松白名单就认为“已经防住 XSS”。
Q12: 事件捕获、目标和冒泡是什么?
答案:
事件从外层沿捕获阶段到目标,再沿祖先冒泡。事件委托利用冒泡在共同祖先处理动态子项,减少监听器并统一逻辑。
stopPropagation 会影响其他处理器,应谨慎使用;影子 DOM 还涉及事件是否 composed 和 retargeting。
Q13: preventDefault 和 stopPropagation 有什么区别?
答案:
前者阻止浏览器默认行为,例如链接跳转或表单提交;后者阻止事件继续传播。二者互不等价。
被动监听器中不能取消默认滚动。业务代码不要用阻止传播掩盖组件边界不清,否则会破坏上层埋点和交互。
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: 页面生命周期中 visibilitychange、pagehide 和 beforeunload 怎么用?
答案:
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 为什么适合动画?
答案:
| 特性 | requestAnimationFrame | setTimeout/setInterval |
|---|---|---|
| 执行时机 | 下一帧渲染前 | 指定延迟后 |
| 帧率 | 与屏幕刷新同步 | 不同步,可能掉帧 |
| 后台 | 暂停 | 继续(节流) |
| 精度 | 高 | 低 |
| 回调参数 | 时间戳 | 无 |
Q19: 浏览器多标签页如何通信?
答案:
同源页面可用 BroadcastChannel、storage 事件、SharedWorker、Service Worker 消息或 IndexedDB 协调;有父子/打开关系时可用 postMessage。
选择看兼容性、生命周期和是否需要共享计算。所有消息都要校验来源和结构,并处理重复、乱序与页面退出。
Q20: 浏览器指纹是什么?
答案:
浏览器指纹是通过收集浏览器和设备特征生成的唯一标识符。
用途:
- 反欺诈:检测恶意用户
- 广告追踪:无 Cookie 追踪
- 安全验证:辅助身份认证
- 数据分析:访客统计
采集维度:
- 基础信息(UA、语言、时区)
- Canvas/WebGL 渲染
- 音频处理
- 字体列表
- 硬件信息
Q21: 什么是关键渲染路径?如何优化?
答案:
关键渲染路径(Critical Rendering Path)是浏览器将 HTML、CSS、JavaScript 转换为屏幕像素的步骤序列:
- 构建 DOM 树:解析 HTML
- 构建 CSSOM 树:解析 CSS
- 构建渲染树:合并 DOM 和 CSSOM
- 布局:计算几何信息
- 绘制:转换为像素
- 合成:图层合成
优化策略:
| 优化方向 | 具体措施 |
|---|---|
| 减少关键资源 | 内联关键 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 的区别?
答案:
| 对比项 | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| 存储大小 | ~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 处理:结合
domInteractive、domContentLoadedEventEnd
缓存、连接复用、跨源权限和 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 用浏览器策略限制 innerHTML、insertAdjacentHTML 等危险 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() 查看经过封装处理后的传播路径。自定义事件默认不会自动穿出,应按组件契约显式设置 bubbles 和 composed。
外部代码不应依赖组件内部节点结构做事件委托。组件应对外派发语义明确的事件,并校验事件数据和取消语义。
Q31: Web Worker 和主线程如何通信?
答案:
通过 postMessage 和 onmessage 进行通信:
// 主线程
worker.postMessage(data);
worker.onmessage = (e) => console.log(e.data);
// Worker 线程
self.onmessage = (e) => console.log(e.data);
self.postMessage(result);
数据传递方式:
- 结构化克隆(默认):深拷贝数据
- Transferable:转移所有权,零拷贝
// 转移 ArrayBuffer
worker.postMessage(buffer, [buffer]);
Q32: Navigation API 相比直接操作 History API 有什么价值?
答案:
History API 只提供 pushState、replaceState 和 popstate 等较低层能力,应用还要自行统一链接点击、前进后退、取消导航和错误处理。Navigation API 试图提供更一致的导航事件、目标条目、拦截和异步过渡模型。
它适合需要精细管理 SPA 导航生命周期的场景,但不能假设所有目标浏览器都支持。生产中应:
- 先检查兼容性和框架集成,保留 History/完整导航回退。
- 拦截后明确处理取消、异常、并发导航和滚动/焦点恢复。
- 不破坏普通链接的打开方式、可访问性和服务端路由。
- 把实验能力隔离在路由适配层,不让业务组件直接依赖。
是否采用取决于浏览器矩阵和路由框架,而不是因为 API 更新就全面重写。
Q33: DOMContentLoaded 和 load 事件的区别?
答案:
| 特性 | DOMContentLoaded | load |
|---|---|---|
| 触发时机 | 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 有什么区别?
答案:
| 特性 | WebSocket | HTTP |
|---|---|---|
| 连接 | 持久连接 | 短连接 |
| 通信 | 全双工 | 请求-响应 |
| 头部 | 轻量 | 每次请求带完整头部 |
| 协议 | ws/wss | http/https |
| 发起方 | 双方都可 | 只能客户端 |
// HTTP: 每次请求都要建立连接
fetch('/api/data'); // 连接 → 请求 → 响应 → 关闭
// WebSocket: 一次连接,持续通信
const ws = new WebSocket('wss://...');
ws.send('message1');
ws.send('message2'); // 复用同一连接
Q38: 重排和重绘有什么区别?如何减少?
答案:
| 对比项 | 重排(Reflow) | 重绘(Repaint) |
|---|---|---|
| 触发条件 | 几何属性变化 | 视觉属性变化 |
| 影响范围 | 可能影响整个页面 | 只影响当前元素 |
| 性能开销 | 高 | 中 |
| 包含步骤 | Layout → Paint → Composite | Paint → 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 到页面展示的性能?
答案:
网络层优化:
| 阶段 | 优化策略 |
|---|---|
| DNS | DNS 预解析、使用 CDN |
| TCP | Keep-Alive、HTTP/2 多路复用 |
| TLS | TLS 1.3、Session 复用 |
| HTTP | Gzip 压缩、缓存策略、资源合并 |
渲染层优化:
| 阶段 | 优化策略 |
|---|---|
| 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 攻击?
答案:
- 输出编码:对用户输入进行 HTML 实体编码
// 将 < > " ' 等转义
escapeHtml(userInput);
- 使用安全 API:
element.textContent = userInput; // 不是 innerHTML
- CSP(内容安全策略):
Content-Security-Policy: default-src 'self'; script-src 'self'
- 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 Worker | Shared Worker | Service 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 屏幕上的运动速度基本一致。
实现时通常还要注意:
- 保存上一帧时间戳,第一帧只初始化,不直接累加。
- 页面从后台恢复时,时间差可能很大,应对
delta设置合理上限,避免对象瞬间跳跃。 - 物理模拟对稳定性要求更高时,使用固定时间步长累计计算,渲染时再插值;普通 UI 动画按实际时间推进即可。
- 不要假设刷新率是 60Hz,也不要用帧数代表持续时间。
rAF 只决定视觉更新时机。回调本身过重仍会掉帧,因此计算、布局、绘制成本也需要控制。
Q51: Polyfill 和 Transpiler 的区别?
答案:
| 类型 | 作用 | 示例 |
|---|---|---|
| Polyfill | 运行时补充缺失的 API | Promise、fetch、Array.includes |
| Transpiler | 编译时转换语法 | 箭头函数 → 普通函数,async/await → Promise |
// Polyfill: 添加缺失的方法
if (!Array.prototype.includes) {
Array.prototype.includes = function() { /* ... */ };
}
// Transpiler: 转换语法
// 源码: const fn = () => {};
// 编译后: var fn = function() {};
Q52: Canvas 指纹的原理是什么?
答案:
Canvas 指纹利用不同设备渲染图像的微小差异:
- 使用 Canvas 绘制文本和图形
- 提取像素数据
- 生成哈希值
差异来源:
- 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: transform 和 opacity 动画为什么通常更流畅?
答案:
它们不改变其他元素的几何布局,并且在元素已被合适分层和栅格化时,浏览器可能只更新合成参数,跳过每帧 Layout 和 Paint。
“通常”很重要:首次绘制、大面积透明层、滤镜、图层提升和纹理上传仍有成本,元素也不保证永远独占合成层。will-change 会消耗资源,不应长期全局开启。
应通过 Performance 和 Layers 工具确认每帧实际发生了什么,关注主线程时间、Paint、显存和过度绘制,而不是把“GPU 加速”当作免费优化。
Q55: 如何设计一个最优的前端缓存策略?
答案:
核心原则:
- HTML 文件:使用协商缓存(
no-cache),确保入口始终最新 - 静态资源:使用内容哈希 + 长期强缓存(
immutable) - 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 攻击能成功——请求确实发送了,服务端也处理了。