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——这些会强制同步布局
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 实现分三步:
- 确定主轴方向(flex-direction)
- 计算每个 flex 项目的原始尺寸
- 分配剩余空间(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 | 严重卡顿 | 必须优化 |
布局是浏览器渲染流程中最耗时的环节之一。理解了它的原理,你就能写出更流畅的页面。下次遇到卡顿,别急着怀疑网络,先看看是不是布局在作祟。
公众号:蓝海资料掘金营,微信deep3321