这道题的核心不是背一串名词,而是说清两次转换:先把地址变成响应字节,再把字节变成屏幕像素。 前者经过导航策略、缓存、DNS、连接与 HTTP;后者经过 HTML/CSS 解析、脚本执行、布局、绘制与合成。 这些步骤有串行依赖,也有大量并行,不能背成「DNS → TCP → 下载 HTML → 等全部资源 → 显示页面」。
这篇文章用 8 道面试题走完整个链路。每题都回答四件事:输入是什么、输出是什么、依赖谁、容易慢在哪里。最后再把 FCP、LCP、DOMContentLoaded、load 和可交互分开,形成一段能直接用于面试的完整回答。
先把地图画出来:8 个阶段,不是 8 次串行等待
窄屏下八阶段图可横向滑动查看完整链路;下面的交互卡用上一站 / 下一站逐站展开完整说明。
INTERACTIVE · NAVIGATION TIMELINE
首屏加载的 12 个面试站点
切换场景,再用上一站 / 下一站按顺序走完 12 个站点;每一站的输入、输出、依赖、瓶颈与考点都直接平铺在卡片里。
冷启动:DNS、TCP、TLS 都要现付。预算按每个网络阶段 1 次往返估算,不含服务端排队与并行子资源。
首次访问:到首字节前的往返预算(示意)。不含并行子资源、服务端排队与真实网络抖动。
切换场景后,用卡片头部的上一站 / 下一站逐站查看;输入、输出、依赖、瓶颈与面试考点都直接平铺在卡内。 RTT 只是建立数量级直觉的教学预算,不是性能基准。
八个阶段的交接关系可以先压成这张表,后面逐题展开:
| 阶段 | 输入 | 输出 | 关键依赖 | 常见瓶颈 |
|---|---|---|---|---|
| 地址解析 | 地址栏文本 | 绝对 URL | URL 规则、HSTS、导航策略 | 错误地址、多余跳转 |
| 连接建立 | 主机、端口 | 可用安全连接 | DNS、TCP/QUIC、TLS | 高 RTT、丢包、完整握手 |
| 请求响应 | HTTP 请求 | 响应头与正文 | 缓存、CDN、应用与数据库 | 回源、排队、慢查询 |
| 资源下载 | HTML/CSS 中的引用 | CSS、JS、图片、字体 | 资源发现、优先级、连接池 | 发现晚、体积大、第三方慢 |
| 文档解析 | HTML 文本流 | DOM | 解码、树构建、脚本时机 | 同步脚本、超大 DOM |
| 样式脚本 | DOM、CSS、JS | CSSOM 与计算样式 | 关键 CSS、脚本依赖 | 渲染阻塞、长任务 |
| 页面渲染 | Render Tree | 栅格化后的帧 | 样式、布局、绘制、合成 | 布局抖动、重绘、层过多 |
| 首屏呈现 | 已提交的帧 | 可见且逐步可交互的页面 | LCP 资源、字体、水合 | 首图慢、CLS、INP 高 |
Q1 · 输入网址后,一定会先做 DNS 吗
面试官: 在地址栏输入一个网址并回车,浏览器第一步做什么?随后一定进行 DNS 查询吗?
先给结论。 第一步是把输入文本解析成导航目标,不是立刻 DNS。浏览器先判断它是搜索词还是 URL,补全协议与路径,并应用 HSTS、安全策略等规则;真正发请求前,还可能被 bfcache、Service Worker 或 HTTP 缓存直接回答。
输入、输出与依赖。 输入是地址栏文本和浏览器状态,输出是一份规范化后的绝对 URL 与导航上下文。URL 中的 scheme、host、port 决定 origin;path + query 进入 HTTP 请求目标;#fragment 只留在浏览器,不会发给服务器。
/、处理 Unicode 主机与百分号编码,并按协议使用默认端口。 常见瓶颈不在解析本身。 真正昂贵的是错误地址导致的二次导航、http → https → www 这类长重定向链,以及本可命中却没有命中的缓存。回答时要强调:「输入网址 → DNS」只是冷启动且未命中任何捷径的分支,不是唯一流程。
追问:no-cache 是不是完全不缓存? 不是。no-cache 允许存储,但每次使用前必须验证;no-store 才是不允许存储。协商缓存命中 304 也发生过网络请求,只是没有重新下载正文。
Q2 · DNS、TCP/QUIC、TLS 分别解决什么问题
面试官: 域名解析之后发生了什么?TCP 三次握手、TLS 握手和 HTTP 请求是什么关系?HTTP/3 改变了什么?
先给结论。 DNS 解决「去找哪个 IP」,TCP 或 QUIC 解决「建立可传输数据的连接」,TLS 解决「确认对方身份并保护通信内容」,最后 HTTP 才在这条连接上表达「我要哪份资源」。
- DNS: 输入主机名,输出 A / AAAA 等记录与 TTL。浏览器、系统、递归解析器任一层的缓存命中,都能省去后续查询。
- TCP: 通过 SYN → SYN + ACK → ACK 建立连接,通常花费 1 个 RTT。它提供有序、可靠字节流,但不验证网站身份。
- TLS: 校验证书链与主机名、协商密钥,并通过 ALPN 确定应用协议。TLS 1.3 完整握手通常再花 1 个 RTT。
- QUIC: 把可靠流、拥塞控制和 TLS 1.3 合并进基于 UDP 的传输协议,首次握手通常 1 RTT;恢复会话可支持 0-RTT,但要处理重放风险。
冷启动网络预算(示意) ≈ DNS 1 RTT + TCP 1 RTT + TLS 1.3 1 RTT + 请求/响应 1 RTT
这是无缓存、无复用、串行且不计服务端处理时间的示意。DNS 的真实耗时取决于缓存层级与记录链;HTTP/3 把 TCP 与独立 TLS 两段合并为 QUIC 握手;真实页面还会并行、复用或重定向。
常见瓶颈: 高延迟网络会放大每一次 RTT;DNS 的 CNAME 链会追加查询;TCP 丢包会影响同一条连接上的所有 HTTP/2 流;证书链错误则不是「慢」,而是直接失败。浏览器会维护连接池,不是每个请求都重新握手。
追问:HTTP/2 已经多路复用了,为什么还要 HTTP/3? HTTP/2 解决的是应用层队头阻塞,但多个流仍共享一条 TCP 连接,一个 TCP 段丢失可能让所有流等待重传。HTTP/3 的 QUIC 流在丢包恢复上彼此独立,不过仍共享网络拥塞,不代表永远更快。
Q3 · HTTP 请求与响应里,哪些信息会继续影响首屏
面试官: 浏览器发出的导航请求长什么样?200、301、304 分别把流程带到哪里?TTFB 慢就是后端慢吗?
先给结论。 请求把方法、路径、Cookie、可接受的编码和缓存验证器交给服务器;响应除了状态码与 HTML 正文,还通过响应头决定缓存、压缩、安全策略和后续资源行为。
GET /interview/browser HTTP/1.1
Host: example.com
Accept: text/html
Accept-Encoding: gzip, br
If-None-Match: "html-v2"| 结果 | 输出 | 后续路径与性能含义 |
|---|---|---|
200 | 响应头 + 新正文 | 进入解压、解码与流式解析;正文大小和服务端等待直接影响首屏。 |
304 | 验证结果,无正文 | 复用本地缓存正文;省去下载,但已经付出一次请求/响应往返。 |
301 / 308 | 永久跳转地址 | 浏览器跟随 Location;新 origin 可能重新 DNS、建连与 TLS。 |
302 / 307 | 临时跳转地址 | 同样增加导航链路;307 保持原方法和正文。 |
| 4xx / 5xx | 错误文档或失败 | 仍可能有可渲染的错误页;不要把「收到响应」等同于「业务成功」。 |
依赖关系要说到响应头之后。 Content-Encoding 决定如何解压,Content-Type 决定 MIME 与字符集,Cache-Control / ETag 决定下次能否复用,CSP 等策略会影响资源能否执行或加载。它们都是首屏流程的输入,不是与渲染无关的元数据。
TTFB 的口径容易答错。 DevTools 的 Waiting (TTFB) 通常从请求发出算到首个响应字节,不包含此前的 DNS、TCP 和 TLS。TTFB 高可能是后端计算,也可能是 CDN 未命中、回源、服务器排队或网络传输;需要结合 Server-Timing 与后端 trace 定位。
Q4 · HTML 还没下载完,CSS、JS 和图片会开始下载吗
面试官: 浏览器什么时候发现子资源?为什么有些图片很晚才出现在 Network 面板?
先给结论。 会,而且通常应该尽早并行。HTML 是流式解析的,不必等整份文档下载完成;主解析器构建 DOM 的同时,预加载扫描器会向前扫描字节流,提前发现 CSS、JS、部分图片等资源并发起请求。
输入与输出。 输入是持续到达的 HTML 字节,以及随后解析出的 CSS;输出是一组带优先级的子资源请求。资源发现的早晚取决于它写在哪里:
| 资源 | 通常如何被发现 | 常见瓶颈 |
|---|---|---|
| CSS | HTML 中的 link / style | @import 串行追加请求;关键 CSS 过大。 |
| JS | HTML 中的 script、模块依赖图 | 同步脚本阻塞解析;动态 import 或客户端路由发现更晚。 |
| 图片 | img、source、CSS 背景图 | 由客户端 JS 插入或藏在大 CSS 中,发现晚;候选尺寸错误会多下载。 |
| 字体 | CSS 规则与实际文字需要该字体时 | 发现晚、字体文件大、不可见文本期推迟首屏。 |
预加载扫描器不是第二个 DOM 构建器。 它只提前发现资源,不执行脚本,也不能理解所有由 JS 动态生成的依赖。<link rel="preload"> 可以显式提示「首屏关键但发现较晚」的资源,preconnect 则提前为关键第三方完成 DNS、连接与 TLS;两者都不能无差别滥用,否则会抢带宽和连接。
常见瓶颈的完整表达: 不只是「网速慢」,还包括发现晚、优先级低、连接排队、缓存未命中、压缩不足、第三方响应慢。优化前先确认是哪一段,不要看到资源大就只会开懒加载。
Q5 · HTML 如何变成 DOM,脚本为什么会卡住解析
面试官: DOM 是怎么构建的?普通 script、defer、async、module 的执行时机有什么区别?
先给结论。 响应先按 Content-Encoding 解压,再按字符集解码为文本;Tokenizer 将文本切成标签、文本与注释,Tree Construction 按 HTML 规则生成 DOM。解析是流式的,但经典脚本可能让主解析器暂停。
DOM 构建的输出不是「源代码的复制」,而是一棵可被 JS 实时修改的节点树。HTML 解析器会补隐式节点、容错和修复结构,所以「查看源代码」与 DevTools Elements 里的 DOM 不一定相同。
| 脚本方式 | 下载是否阻塞解析 | 执行时机 | 面试时要补的限制 |
|---|---|---|---|
经典 script | 是 | 下载后立即执行,再继续解析 | 执行也占主线程;可能读取样式而等待 CSS。 |
defer | 否 | 解析完成后、DOMContentLoaded 前 | 多个 defer 按文档顺序执行。 |
async | 否 | 下载完成后尽快执行 | 不保证顺序,也可能打断解析;适合独立脚本。 |
type="module" | 否 | 默认按 defer 语义执行 | 还要等模块依赖图就绪;加 async 可改变时机。 |
DOMContentLoaded 的边界: 它在 HTML 解析完成、deferred 脚本与默认 deferred 的模块脚本执行后触发,不等待普通图片全部下载。async 脚本不参与阻塞它,可能在事件之后才执行;它也不表示首帧已出现,更不保证 SPA 已完成水合。
常见瓶颈: 头部同步脚本会串联「脚本下载 → 执行 → 后续 HTML 解析」;过大的同步 JS 会形成长任务;庞大 DOM 又增加树构建、样式匹配与布局成本。defer 只移动执行时机,不会把执行成本变成零。
Q6 · CSS 和 JS 到底谁在阻塞谁
面试官: CSS 会阻塞 DOM 解析吗?什么是关键渲染路径?怎样减少首屏阻塞?
先给结论。 CSS 通常不直接阻塞 DOM 树构建,但会阻塞首屏渲染;已经解析且仍在加载的样式表,还可能阻塞后续脚本执行。经典脚本则可直接阻塞 HTML 解析,并通过 DOM 未就绪间接拖住首帧。
为什么脚本可能等 CSS? 脚本可以调用 getComputedStyle() 或读取布局信息。如果它依赖的样式表尚未就绪,浏览器就无法给出可靠结果,因此会让相应脚本等待。于是可能出现「HTML 等 JS、JS 等 CSS、CSS 等网络」的串联。
INTERACTIVE · CRITICAL PATH LAB
阻塞关系实验:谁在拖住第一帧?
固定资源大小与顺序,只改变脚本、首图和字体的加载策略。数值是相对单位,不是毫秒基准;看的是位移方向,不是绝对快慢。
defer 让下载与解析并行,执行排到解析之后、DOMContentLoaded 之前;有依赖顺序的脚本优先用它。 首屏主图保持 eager;它是 LCP 候选,别让 lazy 或低优先级拖后腿。 swap 先画后备字体,字体到达后替换;视觉上更快,但要用尺寸匹配减少 CLS。
模型固定了 HTML、CSS、JS、图片与字体的相对大小和先后顺序,只改变阻塞关系。真实浏览器还会受到连接复用、缓存、优先级、CPU 与字体回退策略影响;请把它当作因果演示,而不是性能基准。
切换普通 script / defer / async、首图 eager / lazy 与字体策略,观察 FCP、LCP、DOMContentLoaded 的相对位移。 模型只演示阻塞关系,不要把相对单位当成真实毫秒。
优化要对应依赖: 精简并拆分关键 CSS,避免用 @import 串行加载;有依赖顺序的脚本优先 defer 或模块;独立第三方脚本才考虑 async;首屏 LCP 图保持 eager,必要时提高优先级;字体用合适的 font-display 与后备字体尺寸匹配兼顾可见性和 CLS。
Q7 · 从 Render Tree 到屏幕,中间经过哪些步骤
面试官: 浏览器渲染流水线是什么?回流、重绘、合成有什么差别?
先给结论。 样式计算得到可见节点与计算样式,Layout 计算位置和尺寸,Paint 生成绘制指令,Raster 把绘制变成瓦片位图,Composite 将图层合并并交给 GPU 上屏。
Render Tree 不是 DOM 的镜像。 display: none 不进入渲染树,也不占布局空间;visibility: hidden 仍保留几何位置;伪元素会产生参与渲染的盒子。首帧只要求视口内的内容完成必要的样式、布局与绘制,不等整页资源加载完毕;但布局计算仍可能覆盖整个文档。
常见瓶颈有三类:
- 强制同步布局与布局抖动。 在同一帧里交替读取
offsetHeight/getBoundingClientRect()和修改几何样式,浏览器被迫反复布局。 - 昂贵绘制。 大面积阴影、滤镜、复杂背景会增加 Paint 与 Raster 成本。
- 图层失控。 给大量元素滥用
will-change或 3D transform,会增加显存、栅格化与合成管理成本。
追问:transform 动画一定不触发布局和绘制吗? 只能说通常可以走合成路径。元素是否升层、文字是否重新栅格化、滤镜与其他属性是否同时变化,都会影响真实流水线;要用 Performance 与 Layers 验证,不能把经验规则当保证。
Q8 · 什么时候才算「首屏加载完成」
面试官: 首屏时间用什么指标衡量?DOMContentLoaded 或 load 能代表加载完成吗?
先给结论。 「首屏加载完成」不是浏览器唯一标准事件,必须先定义产品目标:第一次出现内容看 FCP,主要内容出现看 LCP,布局稳定看 CLS,操作响应看 INP。DOMContentLoaded 和 load 是生命周期事件,都不能单独代表用户已经完成阅读或交互。
| 指标 / 事件 | 回答的问题 | 常见瓶颈 |
|---|---|---|
| TTFB | 请求发出后,多久收到首字节? | CDN 回源、服务端等待、慢查询。 |
| FCP | 第一段文字或图像何时画出来? | 渲染阻塞 CSS、同步脚本、字体不可见。 |
| LCP | 视口内最大内容元素何时完成绘制? | 首图发现晚 / 太大、TTFB 高、渲染延迟。 |
| DOMContentLoaded | HTML 解析与 deferred 脚本执行完了吗? | 阻塞解析脚本、defer / 模块脚本执行。 |
| load | 页面及图片、样式、iframe 等相关资源完成了吗? | 资源多且慢;它仍不等于水合完成或可交互。 |
| INP | 用户操作后,下一帧响应快不快? | 长任务、水合、第三方同步 JS。 |
| CLS | 内容出现后有没有意外跳动? | 图片 / 广告无占位、字体换入、顶部动态插入。 |
LCP 可粗略拆成:TTFB + 资源发现延迟 + 资源下载时长 + 元素渲染延迟
先定位时间花在哪一段,再决定优化缓存、资源发现、图片体积还是主线程;不要只靠一个「减少请求数」解释所有首屏问题。
SSR 也不是终点。 服务端返回的 HTML 可以很快可见,但如果框架还要下载并执行大量 JS、完成水合,页面仍然可能「看得见却点不动」。因此完整回答要从 FCP 继续说到 LCP、CLS 和 INP,并用真实用户监控的 p75 验证,而不是只报一次本地 Lighthouse 分数。
一页速查
| 面试追问 | 一句话回答 | 不要掉进这些坑 |
|---|---|---|
| 第一步是什么? | 解析并规范化导航目标,再判断缓存等捷径。 | 把「输入 URL」直接等同于 DNS。 |
| 为什么 HTTPS 慢? | 冷启动要支付 DNS、连接、TLS 等 RTT,复用后可显著减少。 | 认为每个子资源都重新握手。 |
| 缓存一定省网络? | 强缓存可零请求;协商缓存仍需请求,304 只省正文。 | 把 no-cache 说成不缓存。 |
| HTML 下完才解析? | 不是,HTML 流式解析,预加载扫描器并行发现资源。 | 把首屏想成严格串行流水线。 |
| CSS 阻塞什么? | 主要阻塞渲染,并可能阻塞相关脚本执行。 | 笼统说 CSS 阻塞所有 DOM 构建。 |
| defer 与 async? | 都不阻塞下载期解析;defer 有序且在解析后,async 下载完尽快执行。 | 认为 async 能保证执行顺序。 |
| 首屏完成看什么? | 按目标组合看 FCP、LCP、CLS、INP,而非单一 load。 | 把可见、资源完成与可交互混为一谈。 |
| 怎么定位慢? | 先拆导航、网络、解析、渲染,再用 Network / Performance / RUM 对照。 | 没有证据就加 preload、懒加载或 will-change。 |
30 秒版本的口头回答:
浏览器先把输入解析成绝对 URL,并检查 bfcache、Service Worker、HTTP 缓存与重定向;需要联网时,再通过 DNS 得到 IP,建立并复用 TCP 或 QUIC 连接,HTTPS 下完成 TLS,然后发送 HTTP 请求。响应字节到达后解压、解码并流式解析 HTML,同时预加载扫描器并行发现 CSS、JS、图片等资源。DOM 与 CSSOM 汇合形成渲染树,经布局、绘制、栅格化和合成提交首帧。这里 CSS 主要阻塞渲染,经典脚本阻塞解析,defer 和 async 改变脚本时机。最后要区分 FCP、LCP、DOMContentLoaded、load 与可交互,用 LCP、CLS、INP 和真实用户数据判断首屏体验,而不是说所有资源下载完才算结束。
面试官可能继续追问
为什么说「三次握手之后还要 TLS」只在部分协议路径成立?
HTTP/1.1 与 HTTP/2 通常跑在 TCP + TLS 上,因此可以先描述 TCP 三次握手,再描述 TLS 握手。HTTP/3 使用 QUIC,TLS 1.3 已合并进 QUIC 握手,不能再机械地画成「TCP → TLS → HTTP/3」。回答前先声明协议路径。
强缓存、协商缓存与 Service Worker 谁先生效?
已注册且控制当前页面的 Service Worker 能在 fetch 事件中决定返回缓存、网络响应或合成响应,因此它会改变普通 HTTP 缓存路径;没有 SW 接管时,再按 HTTP 缓存新鲜度决定直接复用还是条件验证。前进后退还可能更早命中 bfcache,整页恢复而不重新走资源加载。面试中先说明导航类型与 SW 是否控制页面,比背固定顺序更准确。
preload 和 preconnect 能随便多加吗?
不能。preload 提高指定资源的发现与加载优先级,preconnect 为某个 origin 提前完成 DNS、连接与 TLS;两者都会消耗带宽、连接与 CPU。只对首屏关键且确实会被使用的资源做提示,并验证它没有被重复下载或挤占更重要的 LCP 资源。
为什么首屏主图不应该 loading=lazy?
懒加载的目的是推迟首屏之外、滚动后才需要的图片。若把视口内最大的主图也 lazy,浏览器会推迟它的请求与绘制,通常直接拖慢 LCP。首屏主图应 eager,写准宽高或宽高比,并通过 srcset / sizes 选择合适资源;是否 preload 还要看它原本的发现时机。
Network 面板里应该按什么顺序排查?
先看主文档是否重定向、缓存命中以及 DNS / Initial connection / SSL / Waiting / Content Download 各段耗时,再看关键请求链和资源优先级。若 Waiting 高,继续查 CDN 与后端;若资源早已下载但首帧仍晚,就转到 Performance 查脚本长任务、样式、布局与绘制,不要一直只在 Network 里找答案。
怎样把这道开放题答得比「背流程」更好?
主动补三个边界:第一,缓存、连接复用和 HTTP/3 会改变路径;第二,下载、解析、渲染存在并行与阻塞依赖,不是严格串行;第三,首屏完成要先定义指标。最后用一次真实排查作收尾,例如「我先看主文档 TTFB,再看关键请求链,最后用 Performance 确认是否是主线程长任务」。