当你写下 grid-template-columns: 1fr 1fr 1fr 时,你是否真正理解那三个 fr 是如何瓜分容器宽度的?当你发现设置了 minmax(200px, 1fr) 的列在某些情况下却只有100px时,你是否知道浏览器在背后做了什么?

CSS Grid的强大之处在于它将复杂的二维布局抽象成了几行声明式的代码。但这份优雅背后,隐藏着一个精密运转的计算引擎——Track Sizing Algorithm(轨道尺寸分配算法)。这套算法是整个Grid布局的核心,它决定了每一行、每一列最终会变成多大。

从一个反直觉的现象说起

考虑这样一个常见的场景:

.container {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 20px;
  width: 600px;
}

直觉告诉我们,三列应该是等宽的,每列约200px。但如果你在某一列里放了一个超长的单词,比如"supercalifragilisticexpialidocious",你会发现这一列明显变宽了,其他两列被挤压。

为什么会这样?因为 fr 单位分配的是剩余空间,而不是容器的总宽度。当内容撑开了某一列,剩余空间就变小了,其他列自然跟着收缩。

这只是冰山一角。真正理解Grid的尺寸计算,需要深入到浏览器引擎的执行流程中。

轨道尺寸函数:三种类型的行为差异

在讨论算法之前,必须先理解Grid轨道的尺寸函数(Track Sizing Function)有哪些类型。W3C规范定义了三类:

固定尺寸:使用 pxemrem 等绝对单位,或百分比值。浏览器可以直接计算出精确尺寸。

弹性尺寸:使用 fr 单位。这种尺寸函数会参与剩余空间的分配。

内容尺寸:使用 automin-contentmax-contentfit-content()。这种尺寸函数需要根据Grid项目的内容来计算。

这三种类型的计算成本差异巨大。根据Igalia团队在Chromium实现中的性能测试数据,使用固定尺寸的100×20网格布局可以达到约1400次布局/秒,而使用内容尺寸的同等网格只有约650次布局/秒——性能差距超过一倍。

Track Sizing Algorithm:四步核心算法

W3C规范定义的Track Sizing Algorithm可以分解为四个核心步骤。这套算法会执行两次:第一次计算列宽,第二次计算行高(行高可能依赖于列宽)。

第一步:初始化轨道尺寸

算法首先为每个轨道初始化两个值:baseSize(基础尺寸)和growthLimit(增长上限)。

/* 伪代码表示初始化逻辑 */
for (track in tracks) {
  track.baseSize = computeUsedBreadthOfMinLength(minTrackBreadth);
  track.growthLimit = computeUsedBreadthOfMaxLength(maxTrackBreadth, track.baseSize);
}

对于固定尺寸,baseSize 直接取指定值。对于内容尺寸函数,baseSize 初始化为0。对于 fr 单位,baseSize 为0,growthLimit 为无穷大。

关键点在于:如果轨道使用 minmax(min, max),则 min 值用于设置 baseSizemax 值用于设置 growthLimit。这就是为什么 minmax(200px, 1fr) 能确保列宽至少为200px。

第二步:解析内容尺寸函数

这是算法中最复杂、最耗时的部分。对于使用 automin-contentmax-contentfit-content() 的轨道,浏览器需要:

  1. 遍历所有Grid项目
  2. 计算每个项目对轨道的贡献值
  3. 对于跨多列/行的项目,需要特殊处理

考虑这个例子:

<div class="grid">
  <div class="item" style="grid-column: 1 / 3;">长文本内容...</div>
  <div class="item">短文本</div>
</div>

跨两列的项目会影响第1列和第2列的尺寸计算。规范要求将这些跨轨道项目按跨度大小排序,然后依次处理。

对于每个跨轨道项目,算法会计算其 min-contentmax-content 贡献值,然后将这些值按比例分配到跨越的轨道中。

第三步:最大化轨道尺寸

完成内容解析后,算法会检查是否有剩余空间。如果有,会尝试让轨道从 baseSize 增长到 growthLimit

这个步骤会多次迭代,直到剩余空间被耗尽或所有轨道都达到上限。

第四步:分配弹性空间

最后,处理 fr 单位。算法会计算所有 fr 轨道的总弹性系数,然后将剩余空间按比例分配。

公式如下:

$$ \text{fr值} = \frac{\text{剩余空间}}{\sum \text{fr系数}} $$

如果设置了 grid-template-columns: 1fr 2fr,总弹性系数是3。假设剩余空间是300px,则第一列得到100px,第二列得到200px。

但有一个关键前提:剩余空间是在扣除固定尺寸、内容尺寸和间隙(gap)之后计算的。

fr单位的数学陷阱

fr 单位的设计初衷是简化比例分配,但它有几个容易被忽视的行为:

陷阱一:gap会优先扣除

.container {
  grid-template-columns: 1fr 1fr 1fr;
  gap: 20px;
  width: 600px;
}

正确计算:间隙占用 $20px \times 2 = 40px$,剩余空间 $600px - 40px = 560px$,每列约 $186.67px$。

陷阱二:fr与固定尺寸混用

.container {
  grid-template-columns: 200px 1fr 1fr;
  width: 600px;
}

第一列固定200px,剩余400px由两个 fr 平分,各200px。

陷阱三:fr与auto混用

.container {
  grid-template-columns: auto 1fr auto;
}

两边的 auto 列会先根据内容确定尺寸,中间的 1fr 占据剩余空间。这就是实现经典"圣杯布局"的简洁方式。

minmax函数:约束的艺术

minmax(min, max) 是Grid中最强大的尺寸控制工具,但它的行为比看起来更复杂。

auto的特殊含义

minmax() 中,auto 作为最小值时,意味着轨道的最小尺寸由内容决定(考虑 min-width/min-height 和内容的最小尺寸)。

但有一个重要的例外:如果Grid项目的 overflow 不是 visible,则其 auto 最小尺寸为0。这就是为什么添加 min-width: 0overflow: hidden 可以让Grid项目正确收缩。

minmax与fr的交互

grid-template-columns: minmax(200px, 1fr);

这里的 1fr 作为最大值参与计算。如果容器很宽,这列会与其他 fr 列共享剩余空间;如果容器很窄,这列至少保持200px。

但问题来了:当容器宽度不足时,这列真的能保持200px吗?

答案是否定的——当容器宽度不足以容纳所有轨道的最小尺寸之和时,会发生溢出。浏览器不会强制让某个轨道保持最小值而牺牲其他约束。

一个常见的陷阱

很多开发者认为 minmax(0, 1fr)1fr 是等价的,但它们有一个关键区别:

  • 1fr 等价于 minmax(auto, 1fr)
  • auto 最小值会让轨道至少能容纳内容
  • minmax(0, 1fr) 允许轨道收缩到0

这就是为什么当内容过长时,1fr 列会撑大容器导致溢出,而 minmax(0, 1fr) 列会正确收缩。

auto-fill与auto-fit:看起来一样,行为截然不同

repeat(auto-fill, minmax(200px, 1fr)) 是实现响应式Grid的经典写法。但 auto-fillauto-fit 的区别经常让人困惑。

考虑这个例子:

.container {
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  width: 800px;
}

.container-fit {
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  width: 800px;
}

假设容器内只有3个项目。

auto-fill的行为:浏览器会计算能放下多少个200px的轨道。$800px / 200px = 4$,所以创建4个轨道。多余的空轨道保留,但视觉上不可见。

auto-fit的行为:同样创建4个轨道,但空的轨道会被"折叠",其尺寸变为0。剩余空间重新分配给有内容的轨道。

这意味着当内容较少时,auto-fit 会让现有项目变得更宽。这在某些场景下是期望的行为,但如果项目内有图片,图片可能会被拉伸变形。

auto-fill与auto-fit的区别

图片来源: Ahmad Shadeed - CSS Grid minmax

内容尺寸的精确计算

min-contentmax-contentfit-content() 是基于内容的尺寸函数,它们的计算逻辑直接影响轨道宽度。

min-content

min-content 代表内容的"最小内容贡献"。对于文本,这是最长单词的宽度(不允许断词时)。对于图片,这是图片的原始宽度。

grid-template-columns: min-content 1fr;

第一列会收缩到刚好能容纳最长单词。

max-content

max-content 代表内容的"最大内容贡献"。对于文本,这是整段文本不换行的宽度。

grid-template-columns: max-content 1fr;

第一列会扩展到能完整显示所有内容。

fit-content()

fit-content() 是一个混合函数,结合了 min-contentmax-content 的特点:

grid-template-columns: fit-content(300px) 1fr;

这等价于 min(max-content, max(min-content, 300px))。列宽会在 min-content 和300px之间取值,但不超过 max-content

性能考量:什么会让Grid变慢

Track Sizing Algorithm的复杂度与轨道数量、项目数量以及尺寸函数类型密切相关。

内容尺寸是性能杀手

根据Chromium团队的性能测试,使用 auto 尺寸的100×20网格布局比固定尺寸的同等布局慢约60%。原因是内容尺寸函数需要:

  1. 对每个项目执行布局计算
  2. 处理跨轨道项目的贡献分配
  3. 可能需要多次迭代

嵌套Grid的性能代价

每个Grid容器都会独立执行Track Sizing Algorithm。如果嵌套多层Grid,计算量会指数级增长。

优化策略

  1. 优先使用固定尺寸:如果知道内容的大致范围,使用固定尺寸或百分比
  2. 限制内容尺寸的使用:只在确实需要时使用 automin-contentmax-content
  3. 使用 min-width: 0:对于需要收缩的项目,设置 min-width: 0 可以避免不必要的布局计算
  4. 考虑使用 contain 属性:对于大型Grid,使用 contain: layout 可以帮助浏览器隔离布局计算

常见陷阱与解决方案

陷阱:长内容撑破布局

.grid {
  grid-template-columns: 1fr 1fr;
}
.long-content {
  /* 很长的URL或单词 */
}

问题:长内容会让第一列变宽,破坏等分布局。

解决方案:

.grid-item {
  min-width: 0; /* 允许收缩 */
  overflow: hidden; /* 或使用 text-overflow: ellipsis */
  text-overflow: ellipsis;
}

陷阱:百分比与不确定尺寸

当Grid容器的高度不确定时(如没有设置 height),行轨道的百分比尺寸可能无法正确解析。

.grid {
  display: grid;
  grid-template-rows: 100px 50% 100px; /* 50% 可能解析为 auto */
}

解决方案:确保容器有确定的高度,或者使用 fr 单位替代百分比。

陷阱:隐式网格的意外行为

当项目被放置在显式定义的网格之外时,会创建隐式轨道。默认情况下,隐式轨道的尺寸是 auto

.grid {
  grid-template-columns: 100px 100px;
  /* 项目被放置在第3列 */
}
.item {
  grid-column: 3; /* 创建隐式轨道 */
}

隐式轨道的尺寸由 grid-auto-columnsgrid-auto-rows 控制。如果需要精确控制,应该显式设置这些属性。

从规范到浏览器实现

Track Sizing Algorithm在浏览器引擎中的实现是一个持续的优化过程。

Chromium的LayoutNG架构将Grid布局的实现分为几个阶段:

  1. 输入处理:解析CSS属性,构建Grid布局的内部表示
  2. 尺寸计算:执行Track Sizing Algorithm
  3. 对齐处理:应用 justify-contentalign-content 等属性
  4. 输出:生成最终的布局结果

这个架构将布局计算与其他渲染阶段解耦,使得性能优化和bug修复更加容易。

Firefox和Safari使用不同的实现架构,但核心算法遵循相同的W3C规范。这确保了跨浏览器的一致性。

写在最后

CSS Grid的Track Sizing Algorithm是一个精密设计的系统,它需要在多种约束条件之间寻找平衡:内容尺寸、容器尺寸、开发者意图、性能开销。理解这套算法,不仅能帮助你在遇到布局问题时快速定位原因,还能让你在设计响应式布局时做出更明智的决策。

下次当你写下 1frminmax() 时,希望你能想起浏览器在背后执行的那四步算法:初始化、内容解析、最大化、弹性分配。这份理解会让你的CSS代码更加精确,也更少意外。


参考资料

  1. CSS Grid Layout Module Level 1 - W3C
  2. CSS Grid Layout Module Level 2 - W3C
  3. Performance analysis of Grid Layout - Igalia
  4. A Deep Dive Into CSS Grid minmax() - Ahmad Shadeed
  5. Auto-placement in grid layout - MDN
  6. Basic concepts of grid layout - MDN
  7. CSS Grid Layout – make everything intensely - Planet Igalia
  8. Grid Layout automatic placement and packing modes - Rachel Andrew
  9. An Interactive Guide to CSS Grid - Josh W. Comeau
  10. A Complete Guide to CSS Grid Layout - CSS-Tricks
  11. Understanding min-content, max-content, and fit-content in CSS - LogRocket
  12. Overflow Issues In CSS - Smashing Magazine
  13. CSS Grid Layout Module Level 3 - W3C
  14. LayoutNG - Chromium Documentation
  15. Chromium Blink Grid Implementation Source