浏览器渲染页面的过程是怎样的?
浏览器从收到 HTML 到画出画面,走的是这条流水线:DOM 树 → CSSOM 树 → Render 树 → Layout → Paint → Composite。面试时把这六个词按顺序说出来,再展开每步做了什么,就够了。
-
解析 HTML → DOM 树:字节流 → 字符 → Token → 节点 → DOM 树。遇到
<script>暂停解析,下载执行完 JS 再继续——因为 JS 可能改 DOM。CSS 和图片不阻塞 DOM 构建,但 CSS 会阻塞后续 JS 执行:JS 可能读getComputedStyle(),浏览器必须等 CSSOM 好了才让 JS 跑。所以 CSS 放 head 不只是避免 FOUC(无样式内容闪烁),还防止 JS 等待 CSS 造成的卡顿。 -
解析 CSS → CSSOM 树:样式表从右向左匹配选择器(
.a .b p先找所有p再逐级向上匹配),构建 CSSOM。CSS 不阻塞 DOM 构建,但阻塞渲染——CSSOM 没好之前页面白屏。 -
DOM + CSSOM → Render 树:只包含可见节点。
display: none连 Render 树都进不了(不占空间),visibility: hidden占位不可见,还在树里。伪元素::before/::after也会进 Render 树。 -
Layout(重排/回流):计算每个可见节点的精确位置和尺寸。首次叫 Initial Layout,后续改动触发 Reflow。一个元素的几何属性变了,可能级联触发整个子树重排——这就是为什么频繁操作 DOM 性能差。
-
Paint(重绘):把 Layout 结果光栅化成像素。改
color、background只触发重绘,不触发 Layout。重排一定触发重绘,反过来不会。 -
Composite(合成):浏览器把页面分成多个图层(层叠上下文、
will-change、3D transform 等会创建新层),GPU 合成各层。transform和opacity的变化只走合成,跳过 Layout 和 Paint,所以动画性能最好——即使主线程卡死也能保持流畅。
追问
CSS 放 head,JS 放底部——还有更好的方案吗?
CSS 必须放 head,没商量。JS 有三个选择:放底部(简单粗暴)、defer(下载不阻塞解析,DOM 构建完按顺序执行,推荐)、async(下载不阻塞,下载完立刻执行,顺序不可控,适合统计脚本)。defer 和 async 只对外部 <script src> 有效,内联脚本不支持。
重排和重绘哪个更贵?怎么减少重排?
重排贵得多——要重算布局,可能级联影响子树;重绘只更新像素。减少重排的实战方法:用 class 切换代替逐条改 style、离线操作 DOM(DocumentFragment 克隆节点改完再插回去)、读写分离(先把 offsetWidth 等布局信息读完缓存到变量,再批量写样式,避免强制同步布局)。Chrome DevTools Performance 面板里紫色 Layout 块高频出现,就说明重排有问题。
为什么 transform 动画比 top/left 流畅?
transform 和 opacity 在合成器线程(Compositor Thread)处理,跟主线程无关。主线程被 JS 堵住时合成器照样跑,动画不卡。top/left 动画走 Layout → Paint → Composite 全流程,绑死主线程。一句话:动画用 transform,别用 top/left。
关键渲染路径如何优化?
目标是缩短首屏白屏时间。三件事:减少关键资源数量(CSS 内联首屏样式、JS 用 defer)、减少关键资源体积(压缩 + Brotli)、减少关键路径往返(<link rel="preload"> 预加载关键资源)。用 Lighthouse 看 FCP 和 LCP 两个指标,直接反映渲染路径优化效果。
哪些操作会触发重排?
改几何属性:width/height/padding/margin/top/left。改 DOM 结构:appendChild/removeChild。读布局信息也会:offsetWidth/scrollTop/getComputedStyle()——浏览器被迫立刻算出最新值,强制同步布局。最坑的是循环里交替读写:for 里先读 offsetWidth 再改 style.width,每次迭代都触发一次重排,性能灾难。