简历写精通浏览器原理一问 URL 到渲染直接哑火...

背完"DNS→TCP→DOM→渲染"只算第一层。真正的分水岭是缓存优先级、阻塞机制、合成层划分,以及主线程和合成线程的分工。五层追问,答到第三层算合格。


一、流水线是底线,但面试官不会只问流水线

"DNS 解析 → TCP 连接 → HTTP 请求 → 解析 HTML → 构建 DOM 树 → 渲染页面。"背下这串三分钟够了,面完大概率换来一句"基础还行"。

面试官接着问:从回车到像素上屏,中间有多少坑、多少优先级、多少浏览器自己的策略?四年经验的人卡住,不稀奇。

这题从来不是一条背出来的流水线,它是浏览器原理的第一道分水岭。我拆成五层说。

flowchart TD A[地址栏回车] --> B{是 URL 还是搜索词} B -->|搜索词| C[交给默认搜索引擎] B -->|URL| D[预解析 预连接 预渲染检查] D --> E[DNS 解析] E --> F[TCP 三次握手] F --> G[TLS 握手] G --> H[HTTP 请求] H --> I[响应 HTML] I --> J[解析 HTML 构建 DOM] J --> K[构建 CSSOM] K --> L[渲染树] L --> M[布局 Layout] M --> N[绘制 Paint] N --> O[合成 Composite] O --> P[像素上屏]

流水线之外,才是真正值得聊的:

  • 地址栏里敲的可能是搜索词 。浏览器先做 URL 合法性判断------有没有 scheme、有没有空格、是不是 localhost 这类特殊主机名。不合法就丢给默认搜索引擎。所以 localhost:3000 能直接开,react hooks 不会。
  • 回车后的前置动作:中断当前页面的加载、按站点复用 Renderer 进程、把请求交给网络线程。
  • Omnibox 的预测行为:DNS 预解析、preconnect、甚至预渲染,都可能在你按下回车之前就发生了。

第一层的胜负手不是背流程,是知道每一步跑在哪个线程上、谁先谁后

二、网络层:四年经验最容易露怯的地方

DNS 缓存:hosts 永远最大

浏览器 DNS 缓存 → 系统 hosts 与 DNS 缓存 → 路由器 → 本地 DNS → 递归查询。写代码时只需要记两件事:hosts 优先级高于一切 DNS 缓存dns-prefetch 只解决域名解析,不解决 TCP 和 TLS。

握手什么时候能省掉

同域名有可用连接时,TCP 握手直接省掉(HTTP/1.1 keep-alive、HTTP/2 多路复用)。TLS 会话复用(session ticket、0-RTT)能跳过完整握手。换域、换端口、连接空闲超时,都得重来。

强缓存与协商缓存:冲突时听谁的

维度 强缓存 协商缓存
相关 header Cache-Control: max-ageExpires ETagIf-None-MatchLast-Modified
是否发请求 不发 发,服务端返回 304
优先级 Cache-Control 覆盖 Expires ETag 精度高于 Last-Modified
典型事故 max-age 设太长,发版用户拿旧包 集群多机 ETag 不一致,永远命不中

几个常被答错的点:no-cache 不是不缓存,是每次必须校验;no-store 才是完全不存;max-age=0, must-revalidate 表达的是同一个意思但语义更明确。时间戳格式的 Expires 还依赖客户端时钟,本地时间不准就废了。

301 / 302 的缓存副作用

301 会被浏览器长期缓存,而且经常不进 Network 面板,Chrome 里清了常规缓存也未必干净。线上域名迁移最惨的事故基本都来自这里。

302 默认不缓存(除非显式带 Cache-Control),临时活动页用它,永久迁移才考虑 301,而且要清楚它不可逆。

同域并发:HTTP/1.1 六条,HTTP/2 别分片

HTTP/1.1 下 Chrome 是 6 条并发连接/域名,这是域名分片(domain sharding)存在的原因。HTTP/2 改成多路复用,并发限制变成流数量,这时候再做域名分片反而有害------每个域名都要重新握手,还会打散优先级。

三、解析与加载:真功夫全在这里

script 到底阻塞了什么

HTML 解析器遇到不带属性的 <script> 会停下 DOM 构建,去下载并执行------因为它可能 document.write,解析器不敢赌。

属性 下载 执行时机 阻塞解析 顺序保证
<script> 同步 下载完立即 文档顺序
defer 异步 DOMContentLoaded 之前 文档顺序
async 异步 下载完立即,可能仍在解析中 不保证

type="module" 默认就是 defer 行为。内联脚本不参与 defer/async 语义。

CSS 不阻塞解析,但能卡死渲染

CSS 不阻塞 HTML 解析,但阻塞渲染树构建------没有样式就没法算布局。更隐蔽的是CSS 会间接卡住后面的 script :脚本可能调 getComputedStyle,浏览器必须等样式就绪。所以一个慢 CSS 能把首屏白屏时间拉得很长。

preload / prefetch / preconnect 的本质

  • preload:当前导航马上要用,高优先级,落内存缓存。必须写 as,否则可能下载两次。
  • prefetch:下一次导航可能用,低优先级,落 HTTP 缓存。
  • preconnect / dns-prefetch:只建连接或解析域名,不下载资源。

有个坑:重定向场景下 preload 会失效 。资源被 302 到另一个 URL 时,按原地址发起的 preload 就白费了,还多打一次请求。跨域 preload 的 crossorigin 不匹配,缓存也对不上号。

图片和字体,谁阻塞首屏

图片不阻塞 DOM 解析,也不阻塞首屏绘制(除非它在关键路径上撑开布局)。字体默认会阻塞文本渲染font-display: auto 有约 3 秒的 block 期;swap 先渲染兜底字体再换;optional 可能干脆不换。

中文站点还要注意 unicode-range 拆分------不拆的话,一个中文字体包轻松几 MB,字体没到,首屏文字全是空白。

四、渲染流水线:改哪个属性,触发哪一步

主流程:Parse → Style → Layout → Paint → Composite。关键在于哪一步会被哪类改动触发

操作 Layout Paint 仅 Composite
widthtopmargin 触发 触发
colorbackgroundbox-shadow 不触发 触发
transformopacity 不触发 不触发

transformopacity 能走 GPU 合成,是因为它们不需要重算几何和像素填充,合成器直接对已有图层做矩阵变换和透明混合就行。

will-change: transform 会提前把元素提升为独立合成层,但别乱加。每个合成层都吃显存,滥用会造成层爆炸,移动端尤其明显,动画结束后应该移除。

z-index 只有在元素创建了层叠上下文时才有意义:position 非 static 且 z-index 非 auto,或有 transformfilteropacity < 1 等。调层级调不出来,八成是父元素无意中开了新上下文,子元素的 z-index 只在父上下文里比大小。

content-visibility: auto 值得单独记一笔:

css 复制代码
.card {
  content-visibility: auto;
  contain-intrinsic-size: 0 300px;
}

它跳过屏幕外内容的渲染,保留占位,配合 contain-intrinsic-size 防止滚动条抖动。和 display: none(不渲染不占位)、visibility: hidden(占位不绘制)是三种不同语义,长列表优化里它的性价比最高。

五、主线程与合成线程:谁在干活,谁在卡

到了这一层,问题变成"哪些活是主线程干的,哪些交给合成线程"。

sequenceDiagram participant M as 主线程 participant C as 合成线程 participant G as GPU M->>M: JS 执行 样式计算 布局 M->>M: 生成绘制指令并分块 M->>C: 提交图层信息 C->>C: 光栅化调度 C->>G: 提交纹理 G->>G: 合成并上屏 Note over M,C: transform 和 opacity 动画由合成线程独立跑 主线程卡住也不掉帧

主线程 :JS 执行、样式计算、布局、绘制指令生成、事件分发。 合成线程:分块、光栅化调度、合成、提交 GPU。

事件循环插在哪个环节?浏览器把渲染机会放在任务队列清空之后、微任务清空之后。所以一段长时间同步 JS 会把渲染整个卡死------requestAnimationFrame 里做重活一样掉帧,它保证的是"在下一帧渲染前执行",不是"不占用渲染时间"。

图片解码会阻塞 :大图解码默认在主线程,滚动时容易掉帧。img.decode() 或者 createImageBitmap 可以提前解码。

像素对齐问题left: 0.5px、缩放遗留的非整数 transform,会让文字被重新光栅化,看起来发虚。动画结束时记得把 transform 归零。

LCP 对应整条链路的最末端------最大的内容元素完成绘制并上报。想优化它,就是把前面所有环节一起压:TTFB、资源加载、渲染阻塞、绘制。

如果只剩三十秒

这样答:

从地址栏判断到像素上屏,这条链有网络、缓存、解析、渲染、合成五段,每段都有自己的优先级和阻塞规则。真正决定首屏体验的,是 CSS 与主线程的争用,以及图层能不能交给合成线程独立跑。

写在最后

浏览器原理这类问题,最怕的不是不会,是"以为自己会"。能从地址栏一路聊到 GPU 像素输出的工程师,和只会背流水线的工程师,差距不在记忆量,在排查线上问题时能不能想到那一层。

你在真实项目里遇到过最诡异的浏览器行为是什么?我印象最深的是某次 301 被缓存了半年,测试环境怎么改配置都没用。评论区聊聊。

有用的话点个赞。

相关推荐
garmin Chen1 小时前
MySQL精简面试题
数据库·后端·mysql·面试
晚安日记wanna1 小时前
四年前端被问 Code Review90人只看得懂空格和命名
前端·面试
IPdodo_1 小时前
curl 如何测试代理 IP?HTTP、SOCKS5 与认证参数示例
前端·网络·python·https·网络调试
做前端的娜娜子1 小时前
施工蓝图:vite.config.js —— 项目的指挥中心
前端·react native·vite
宸翰1 小时前
解决uni-app 中 SVG 图片在 iOS 端显示模糊问题
前端·uni-app
数据掘金1 小时前
第三方 SDK 集体故障时,你的 App 会怎样?可落地的监控与降级方案
前端
AI分享猿1 小时前
UI设计Prompt系列(十七):团队如何管理UI Prompt——减少产品、设计与开发之间的信息损耗
前端·prompt
资讯综合1 小时前
2026全国云渲染网站排名 不同需求用户适配榜单
开发语言·前端·javascript
letisgo51 小时前
JAVA 高级进阶10篇《消息队列实战:RocketMQ/Kafka选型与“不丢不重有序“三连解》
java·面试·kafka·消息队列·rocketmq