浏览器原理面试题精选

2026年 阅读约 11 分钟 面试指南 · 前端面试 · 浏览器

浏览器原理面试题精选:关键渲染路径、回流重绘与合成层优化、事件循环宏微任务、强缓存与协商缓存、同源策略与CORS、FCP/LCP性能指标与白屏排查实战,系统梳理浏览器渲染高频考点,附代码示例。

一句话总结

浏览器渲染的本质是一条流水线:HTML → DOM、CSS → CSSOM,两者合成渲染树 → 布局(几何信息)→ 绘制(像素指令)→ 合成(GPU 上屏)。JS 是唯一能随时插入打断这条流水线的能力——通过事件循环调度。理解渲染的关键是记住三件事:① 回流(Layout/Reflow)代价最高,改几何信息会触发其后所有阶段;② 重绘(Paint)次之,只影响像素;③ transform/opacity 在合成阶段完成,不走主线程布局绘制,所以动画首选它们。

# 一条 URL 到屏幕像素的完整链路(面试必考"输入 URL 发生了什么"): # DNS 解析 → TCP 握手 →(HTTPS)TLS 握手 → HTTP 请求响应 # → HTML 解析构建 DOM(遇到 CSS 阻塞渲染、遇到 JS 阻塞解析) # → DOM + CSSOM → 渲染树 → 布局 → 绘制 → 合成上屏 → 首次内容绘制 FCP → 最大内容绘制 LCP

初级理解

关键渲染路径五步

阶段输入输出说明
解析HTML 字节DOM 树字节→字符→词法→DOM 节点,容错解析
样式CSS 字节CSSOMCSSOM 构建会阻塞后续 JS 与渲染
样式计算DOM+CSSOM渲染树display:none 的节点不进渲染树
布局 Layout渲染树几何信息每个节点的坐标与尺寸——发生"回流"
绘制 Paint + 合成 Composite布局结果屏幕像素分层绘制,合成器把层交给 GPU 上屏

回流、重绘、合成的区别

# 回流(Reflow / Layout):几何信息变了——宽高、位置、增删节点、读 offsetHeight 强制同步 # 代价最高:本节点后续所有阶段重来,且可能波及兄弟/祖先节点 # 重绘(Paint):外观变了但几何没变——color、background、visibility # 代价中等:重新生成绘制指令,不需要重新布局 # 合成(Composite):只动图层的 transform / opacity # 代价最低:在合成器线程 + GPU 完成,不触发主线程布局与绘制 # 触发回流的高频操作:style.width = '100px'、className 整体替换、 # appendChild、window.resize、读 getBoundingClientRect(强制布局)

script 标签四种加载方式

方式下载执行时机阻塞解析
<script>并行立即,阻塞后续解析是
async并行下载完立即执行(顺序不定)是
defer并行DOM 解析完后、DOMContentLoaded 前按序执行否
type=module并行同 defer(默认)+ESM 语义否
提示:CSS 在 head、JS 在 body 底部(或 defer)是经典组合:CSS 不阻塞解析但阻塞渲染(白屏),JS 阻塞解析所以放最后。

中级深入

事件循环:宏任务与微任务的执行顺序

console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4); // 输出:1 4 3 2 // 规则:同步代码 → 清空微任务队列(promise.then / MutationObserver / queueMicrotask) # → 取一个宏任务(setTimeout / setInterval / IO / UI 事件)执行 # → 再清空微任务 →(浏览器可能渲染一帧)→ 下一个宏任务 // 面试进阶:async/await 的等待点 async function f() { console.log('a'); await g(); // await 之后的代码等价于 .then 回调(微任务) console.log('b'); // 微任务,先于下一个 setTimeout } f(); setTimeout(() => console.log('c'), 0); // a b c

浏览器缓存:强缓存与协商缓存

# 强缓存(不发请求,本地直接用): # Cache-Control: max-age=31536000 (优先级高) # Expires: Wed, 21 Oct 2026 07:28:00 GMT (HTTP/1.0 遗留) # 命中时状态码 200,from disk cache / memory cache # 协商缓存(发请求问服务器"变没变"): # ETag / If-None-Match(内容指纹,优先) → 没变返回 304 # Last-Modified / If-Modified-Since(秒级时间戳) # 注意:304 依然要走一次网络往返,只是没有响应体 # 实践口径:HTML 用 no-cache(必须协商),带 hash 的 js/css/img 用 max-age=31536000, immutable # no-cache ≠ no-store:no-cache 是"可以存但用前必须验证",no-store 是"根本不许存"

CSS 优化为什么能加速渲染

# 1. 选择器代价:类选择器 O(1) 定位,后代链/通配符/:nth-child 回溯,避免长选择器 # 2. 重排成本远大于重绘:批量修改先读布局属性、改完一次性写(避免读写交错的强制同步布局) # 3. 缓存几何值:循环里反复读 offsetWidth 会触发 N 次强制布局,提出循环外只读一次 # 4. containment:contain: layout paint 让元素自成渲染上下文,改动不外溢 # 5. will-change: transform 提前升层——但滥用会吃显存,用完记得移除

高级进阶

合成器线程与滚动/手势优化

# 主线程(渲染进程):解析、JS、样式、布局、绘制 # 合成器线程:图层管理、把绘制瓦片(tile)交给 GPU # 为什么 transform 动画流畅: # transform/opacity 只需合成器重新变换已有瓦片 → 不碰主线程 → 不被长 JS 任务卡住 # 为什么滚动可以"丝滑": # 页面可滚动时滚动由合成器线程直接处理(不等主线程) # 但 JS 里同步读 scrollLeft 会强制主线程与合成器同步 → 掉帧 # passive 事件监听器:承诺"不 preventDefault",合成器就不必等 JS element.addEventListener('touchstart', onScroll, { passive: true });

同源策略与 CORS

# 同源 = 协议 + 域名 + 端口 全同。限制的是"读":DOM、Cookie、localStorage、Ajax 响应 # 不限制"写":表单提交、script/img 标签加载可以跨源(只是拿不到内容) # CORS 流程(fetch/XHR): # 简单请求(GET/HEAD/POST + 简单头)→ 直接带 Origin 请求头 # 响应需 Access-Control-Allow-Origin: https://xxx(不能同时 * 和 credentials) # 预检请求 OPTIONS(自定义头、PUT/DELETE、content-type: application/json 等) # 服务器回 Access-Control-Allow-Methods / Headers / Max-Age 后才发真实请求 # 面试追问 CSRF 与 XSS: # XSS:恶意脚本注入页面执行 → 防:输出转义、CSP、HttpOnly Cookie # CSRF:诱导已登录用户发"跨站但带 Cookie"的请求 → 防:SameSite Cookie、Token 校验、检查 Origin

性能指标与度量

指标含义参考值
FCP首次内容绘制(白屏结束)< 1.8s 良好
LCP最大内容绘制(主体可见)< 2.5s 良好
CLS累计布局偏移(视觉稳定性)< 0.1 良好
INP交互到下一帧绘制(响应性,已取代 FID)< 200ms 良好
// Performance API 直接取值(RUM 上报常用) const paint = performance.getEntriesByType('paint'); const fcp = paint.find(e => e.name === 'first-contentful-paint')?.startTime; const lcp = new PerformanceObserver(list => { console.log(list.getEntries().at(-1).startTime); }).observe({ type: 'largest-contentful-paint', buffered: true });

实战场景

白屏排查流程

# 1. Network 面板看三个时间:HTML 是否秒回、静态资源是否排队(连接数上限)、是否大量 304/200 # 2. Performance 面板录一段:定位 FCP/LCP 对应事件,看主线程长任务(红条)是谁 # 3. 常见归因: # - CSS/JS 过大 → 压缩、拆包、defer # - 首屏接口串行 → 接口聚合、骨架屏先渲染 # - 字体 FOUT/FOIT → font-display: swap + 预加载关键字体 # - 第三方脚本(统计/广告)→ 异步加载、延迟到 onload 后

长任务与大列表优化

// 长任务(>50ms)会阻塞渲染与交互,切片执行 function sliceWork(total: number) { const chunk = () => { const deadline = (window as any).performance?.idleCallback ? undefined : undefined; let end = performance.now() + 12; // 每片最多 12ms while (performance.now() < end) { /* do a unit of work */ } if (还有活) requestAnimationFrame(chunk); // 或 requestIdleCallback }; requestAnimationFrame(chunk); } // 大列表:首屏虚拟列表(只渲染可视区 ± 缓冲区);图片 loading="lazy" + 宽高占位防 CLS

防抖与节流的场景选择

// 防抖 debounce:等"停手"后执行一次 —— 搜索联想、表单校验、窗口 resize 结束 // 节流 throttle:固定频率执行 —— 滚动埋点、拖拽动画、鼠标移动 const debounce = (fn, wait) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), wait); }; }; const throttle = (fn, wait) => { let last = 0; return (...a) => { const now = Date.now(); if (now - last >= wait) { last = now; fn(...a); } }; };

面试模拟

面试官:从在地址栏输入 URL 到页面显示,浏览器做了什么?

你:分网络和渲染两段。网络段:DNS 解析拿 IP → TCP 三次握手 → HTTPS 再加 TLS 握手 → 发 HTTP 请求,服务器返回 HTML。渲染段:浏览器把字节解析成 DOM;遇到 CSS 构建 CSSOM 并阻塞渲染和后续 JS;两者合成渲染树,经布局算出几何信息、绘制生成绘制指令、合成器把层交给 GPU 上屏,得到 FCP;随后 LCP 资源加载完主体可见。JS 通过事件循环穿插其中,defer 脚本在 DOM 解析完后执行。

面试官:回流和重绘有什么区别?怎么减少回流?

你:回流是元素几何信息(尺寸、位置)变化触发的重新布局,代价最高且可能波及周边节点;重绘只是外观变化重新生成像素,不涉及布局。减少回流:批量读写分离(先读后写,避免读写交错触发强制同步布局);用 transform/opacity 做动画;样式操作聚合(一次改 class 而不是循环改 style);频繁变化的元素脱离文档流(fixed/absolute);对复杂子树加 contain 约束影响范围。

面试官:说一下浏览器缓存的完整决策链。

你:先看 Cache-Control 的 max-age 是否在有效期内——在就走强缓存,直接读本地,不发请求,状态 200 from cache。过期后带 If-None-Match(或 If-Modified-Since)发协商请求,服务器对比 ETag 没变返回 304 继续用缓存,变了返回 200 + 新资源。策略上:带内容 hash 的静态资源 max-age 一年 + immutable,HTML 用 no-cache 强制每次协商,敏感数据 no-store。no-cache 是"存但用前须验证",和 no-store 不是一回事。