9、布局(Layout)原理:布局概念与触发条件、Block 与 Inline 布局、Flexbox 与 Grid 布局实现、布局性能优化

布局,说白了就是浏览器怎么把 CSS 规则和 DOM 树结合起来,算出每个元素该在屏幕的哪个位置、占多大地方。很多同学觉得布局就是「排个版」,其实没那么简单。WebKit 的布局引擎要处理各种 display 类型、书写模式、包含块关系,还要考虑性能——嗯,这里面的坑我踩过不少。

9.1 布局的核心概念与触发条件

布局在 WebKit 里叫 Layout,也叫 Reflow。它发生在样式计算(Style)之后,绘制(Paint)之前。我习惯把布局理解成「给每个元素发一张房产证」——上面写着它的坐标、宽高、边距这些信息。

什么时候会触发布局?我总结了几种常见情况:

  • 初始布局:页面第一次加载,所有元素都要算一遍
  • DOM 变化:增删节点、修改文本内容
  • 样式变化:尤其是 width、height、margin、padding、display、position 这些几何属性
  • 窗口 resize:viewport 变了,布局得重算
  • 获取某些属性:比如 offsetTop、scrollHeight、getComputedStyle——这些会强制同步布局
注意:强制同步布局是性能杀手。我曾经在项目中看到一段代码,循环里反复读 offsetHeight,结果页面卡得不行。后来改成先批量读、再批量写,帧率直接翻倍。

WebKit 的布局是递归的。从根元素(通常是 body)开始,自上而下计算每个元素的大小和位置。但这里有个关键点:子元素的大小可能会影响父元素,比如父元素没设高度,靠子元素撑开。这就涉及「自底向上」的传递。所以布局其实是两趟:先向下确定约束,再向上反馈尺寸。

核心概念:布局的输入是 DOM 树 + 计算后的样式,输出是每个元素的几何信息(Rect)。这些信息存在 RenderObject 上,后续绘制直接读取。

9.2 Block 与 Inline 布局

Block 和 Inline 是 CSS 布局的基石。WebKit 里对应的是 RenderBlock 和 RenderInline 两个类。

Block 布局:元素从上到下排列,每个占一整行。宽度默认是包含块的 100%,高度由内容撑开。我刚开始做浏览器内核时,以为 Block 布局很简单,直到遇到「margin collapse」——相邻块级元素的上下 margin 会合并,取最大值。这个特性让很多人头疼。

/* 两个 div 之间的 margin 会合并 */
div { margin: 20px 0; }
/* 实际间距是 20px,不是 40px */

Inline 布局:元素从左到右排列,宽度由内容决定。WebKit 处理 Inline 布局时,会先计算每个字符的宽度,然后决定换行位置。这里有个细节:inline-block 和 inline-flex 虽然是内联级,但内部是块级格式化上下文。

我记得有一次排查一个 bug:两个 inline-block 之间总有 4px 的间隙。查了半天,原来是 HTML 里的换行符被解析成了空白字符。解决方案很简单:父元素设置 font-size: 0,或者把标签写在一行。

小技巧:Block 和 Inline 的布局算法在 WebKit 里是高度优化的。比如连续的同类型 Block 元素,WebKit 会用「快速路径」跳过一些不必要的计算。但如果你混用各种 display 类型,就会走慢速路径。

9.3 Flexbox 与 Grid 布局实现

Flexbox 和 Grid 是现代布局的两大法宝。WebKit 对它们的实现非常成熟,但底层逻辑完全不同。

Flexbox 布局:一维布局,要么横着排,要么竖着排。WebKit 的 Flexbox 实现分三步:

  1. 确定主轴方向(flex-direction)
  2. 计算每个 flex 项目的原始尺寸
  3. 分配剩余空间(flex-grow / flex-shrink)

这里有个容易忽略的点:flex-basis 的优先级高于 width/height。我见过有人同时设了 width 和 flex-basis,结果 width 不生效,一脸懵。

.item {
  flex: 1 1 200px; /* flex-grow: 1, flex-shrink: 1, flex-basis: 200px */
  width: 300px;    /* 这个不生效,因为 flex-basis 优先级更高 */
}

Grid 布局:二维布局,行列同时控制。WebKit 的 Grid 实现比 Flexbox 复杂得多。它需要先解析 grid-template-columns 和 grid-template-rows,然后计算每个网格线的位置,最后把元素放到对应的网格单元里。

Grid 的「自动填充」功能很强大,但也容易出性能问题。比如 fr 单位的计算,需要先算出所有固定尺寸的列,再用剩余空间分配 fr 列。如果网格很大(比如 100 行 100 列),布局时间会显著增加。

经验之谈:我在优化一个数据大屏页面时,发现 Grid 布局占了总布局时间的 40%。后来把一些嵌套的 Grid 改成 Flexbox,布局时间降到了 15%。不是 Grid 不好,而是要根据场景选工具。

9.4 布局性能优化

布局性能优化,说白了就是「少触发布局,触发后快结束」。我总结了几个实战经验:

  • 减少布局次数:用 class 切换代替逐个修改样式。用 requestAnimationFrame 批量更新。
  • 避免强制同步布局:不要在修改样式后立即读取几何属性。如果必须读,先批量读,再批量写。
  • 使用 contain 属性:contain: layout 告诉浏览器,这个元素的布局不影响外部。WebKit 会把它独立处理,减少重排范围。
  • 优化选择器:虽然选择器不直接影响布局,但复杂的后代选择器会让样式计算变慢,间接拖慢布局。
  • 减少 DOM 深度:嵌套越深,布局递归的层数越多。能扁平化就扁平化。

我曾经踩过的坑:一个表格页面,每次滚动都卡。查了半天,发现是滚动事件里调用了 getBoundingClientRect,触发了强制布局。改成用 IntersectionObserver 后,问题解决。

WebKit 内部还有一个优化叫「增量布局」。如果只改了某个元素的尺寸,WebKit 会标记这个元素为 dirty,然后只重新布局它和它的子元素,不会波及整个文档。但如果你改了 body 的宽度,那不好意思,全量重排。

最后说一个工具:Chrome DevTools 的 Performance 面板。录制一段操作,看 Layout 事件的时间。如果 Layout 时间超过 10ms,就该优化了。我一般会看 Layout 的「Layout Root」——如果每次都是 document,说明触发了全量布局,需要排查。

性能指标参考:

布局时间体验建议
< 5ms流畅保持
5-15ms可接受关注
15-50ms卡顿优化
> 50ms严重卡顿必须优化
WebKit 布局流程与优化策略 DOM + 计算样式 布局触发条件 初始布局 | DOM变化 | 样式变化 | resize | 强制同步 布局类型 Block 布局 Inline 布局 Flexbox / Grid 布局性能优化策略 减少布局次数 避免强制同步 contain 属性 减少 DOM 深度 增量布局 Performance 分析 元素几何信息 (Rect)

布局是浏览器渲染流程中最耗时的环节之一。理解了它的原理,你就能写出更流畅的页面。下次遇到卡顿,别急着怀疑网络,先看看是不是布局在作祟。


公众号:蓝海资料掘金营,微信deep3321