那年我们社区首页改版,那个日活 120 万左右的站点突然成了性能瓶颈。当时用的是 Flexbox 嵌套布局,结构大概是顶部导航、中间的推荐流(卡片列表)、右侧的热门话题栏和底部的分页。在 Chrome 92 版本下,用 Lighthouse 跑分,首次内容绘制(FCP)居然卡在 1.4 秒,主线程在布局阶段的耗时占了 35% 以上。我盯着 Performance 面板看了半天,发现 Flexbox 在重排时有个很烦人的问题:因为推荐流的卡片高度不固定,Flex 容器在计算换行和剩余空间分配时,会反复触发子元素的重排,尤其是右侧热门话题栏用了 position: sticky 之后,滚动时的掉帧率直接飙到了 18%。
那时候团队里有人提议直接上 CSS Grid,毕竟 W3C 的 CSS Grid Layout Level 1 早在 2017 年就成推荐标准了,我们用的 Chrome 92 和 Safari 15 早就全支持。但我当时有点犹豫,怕重构成本太高——毕竟首页有 12 个模块,嵌套了 4 层 Flex 容器。后来我们做了个灰度测试:拿 5% 的流量切到 Grid 版本,对比发现布局重排的耗时从平均 820ms 降到了 110ms,主线程的布局计算时间直接砍掉了 87%。这数据一出来,技术总监直接拍板全量重构。
具体怎么做的呢?原来的 Flex 布局是这么写的:外层容器 display: flex; flex-direction: column;,中间内容区分成左右两块又嵌套了一层 display: flex;,推荐流卡片再用 flex-wrap: wrap。这种嵌套在动态加载内容时,每次新增卡片都会让整个容器重新计算 flex-grow 和 flex-shrink。换成 Grid 之后,我们直接把整个页面结构定义成二维轨道:
这里有个细节:原来 Flex 布局里右侧热门话题栏的 sticky 经常失效,因为父容器的 overflow 属性会影响 sticky 的定位上下文。换成 Grid 之后,我们把 .main-feed 单独设成 overflow-y: auto,它的滚动不会影响到同级的 .hot-topics,sticky 就稳了。另外,Grid 的 gap 属性直接替代了原来 Flex 里用 margin 凑间距的写法,避免了最后一个元素的 margin-right 多余的问题——我们之前为了处理这个,还专门写了个 :last-child 的 hack,现在直接删了 30 行 CSS。
重构后最直观的变化是滚动流畅度。原来用 Flex 时,快速滚动推荐流,帧率经常掉到 45fps 以下,现在稳定在 58fps 以上。Lighthouse 的布局偏移(CLS)分数从 0.23 降到了 0.04,因为 Grid 的轨道尺寸在初始渲染时就确定了,不会像 Flex 那样因为内容加载导致容器尺寸突然变化。后来我们查了下 Can I Use 的数据,2023 年 Grid 的全球支持率已经到了 97.4%,连 IE 11 都有 Autoprefixer 的降级方案,完全不用担心兼容性。
不过也不是没遇到问题。当时有个老版本的 Firefox 102 不支持 grid-template-rows: 1fr 和 minmax 的组合,导致内容区高度计算错误。我们排查了半天,发现是 Firefox 102 对 1fr 在 grid-template-rows 里的解析有 bug,最后加了个降级方案:grid-template-rows: 60px minmax(0, 1fr) 50px,强制给中间行设个最小高度 0,就解决了。这也让我意识到,虽然 Grid 规范已经成熟,但不同浏览器对边缘特性的实现还是有差异,上线前一定要用 BrowserStack 跑一遍主流版本。
上个月做电商商品列表页改版,产品经理要求“不管屏幕多宽,卡片最小 200px,最大占满剩余空间,自动换行,不用写媒体查询”。当时我第一反应是这需求用 Flexbox 的 flex-wrap: wrap 加 flex: 1 0 200px 也能凑,但实际测下来发现 Flex 有个硬伤:当一行剩的空间不够放一个 200px 的卡片时,它会把剩下的空间分给已有的卡片,导致卡片被拉伸得很难看。比如屏幕宽 450px 时,两个 200px 的卡片需要 400px,剩下 50px 会被分给这两个卡片,每个变成 225px——但产品要求卡片最大不能超过 300px,这时候 Flex 就搞不定了。
这时候 fr 单位和 minmax 就派上用场了。fr 是 Grid 里的分数单位,代表剩余空间的一部分,而 minmax(min, max) 可以定义轨道的最小和最大尺寸。我查了下 W3C 的规范,minmax 是 Level 1 就有的核心特性,2023 年的 Level 2 草案里还扩展了它和容器查询的结合用法。我们最终用 repeat(auto-fit, minmax(200px, 1fr)) 实现了需求,先看代码:
这里 repeat(auto-fit, minmax(200px, 1fr)) 的意思是:自动创建尽可能多的列,每列最小 200px,最大占 1fr 的剩余空间。auto-fit 会先把所有列都设为最小尺寸,然后如果有多余空间,就把这些空间分配给已有的列(用 1fr)。比如屏幕宽 1200px 时,算上 gap(16px * 5 = 80px,假设 6 列),每列最小 200px 的话,6 列需要 200*6 + 80 = 1280px,不够,所以只能放 5 列:200*5 + 16*4 = 1064px,剩下 136px 会被 5 列平分,每列变成 200 + 136/5 ≈ 227px,符合“最小 200px”的要求。如果屏幕宽 1600px,6 列需要 1280px,剩下 320px 平分,每列变成 200 + 320/6 ≈ 253px,还是小于 300px 的卡片最大宽度。
但有个问题:如果屏幕特别宽,比如 2560px 的 2K 屏,6 列的话每列会变成 (2560 - 16*5)/6 ≈ 413px,这时候就超过了产品要求的 300px 最大限制。所以我们给 .product-card 加了 max-width: 300px 和 margin: 0 auto,这样当轨道宽度超过 300px 时,卡片自身不会变宽,会在轨道里居中,剩下的空间就空着——这比强行拉伸卡片好看多了。后来我们测了下,从 320px 手机到 2560px 显示器,布局都不用写媒体查询,自动适配。
之前用 Flex 做类似布局时,我们写了 3 个媒体查询断点(768px、1024px、1440px),分别设置 flex-basis 为 45%、30%、22%,维护起来很麻烦。改成 Grid 之后,这些断点全删了,CSS 代码量少了 40%。不过有个细节要注意:minmax 里的 1fr 是相对于剩余空间的,如果你把容器设成了 width: fit-content,那 1fr 就没意义了——我们当时有个新人把容器设成了 fit-content,导致卡片全挤在一起,排查了半小时才发现是这个问题。
还有一次,我们做后台管理系统的数据表格,要求列宽根据内容自适应,但最小不小于 100px,最大不超过 300px。用 Grid 的话直接 grid-template-columns: repeat(5, minmax(100px, 300px)) 就行,但数据加载慢的时候,初始渲染列宽会闪一下。后来我们加了 grid-auto-columns: minmax(100px, 300px),处理动态新增的列,闪动问题就解决了。现在我们的组件库里,所有卡片列表和表格布局都优先用 Grid 的 minmax + fr,除非是那种单行排列的按钮组(那种用 Flex 更合适)。
前两周帮前端新人排查一个问题:他们做的仪表盘页面,用 Grid 布局的卡片明明设了 justify-items: center,但卡片里的内容还是左对齐。我过去一看代码,发现他把 justify-content: center 写在了容器上,把 justify-items: center 写在了卡片上——这俩属性作用对象完全不一样,能对齐才怪。
先讲清楚这俩的区别:justify-content 是控制整个网格轨道在容器内的水平对齐,针对的是 Grid 容器;justify-items 是控制单个网格项在其网格区域内的水平对齐,针对的是 Grid 项目(或者容器上统一设置所有项目)。这个区别在 W3C 的 Box Alignment 规范里写得明明白白,但很多人容易搞混,包括我刚用 Grid 的时候也踩过坑。
去年做数据可视化大屏,我们用了 12 列的 Grid 布局放图表卡片。当时要求所有卡片在容器内居中,不管列数多少。我一开始写了 justify-items: center,结果卡片是居中了,但卡片里的图表(固定宽度 400px)却左对齐在卡片里——因为 justify-items 控制的是卡片内的内容对齐。后来改成 justify-content: center 在容器上,卡片本身居中了,但卡片里的图表还是左对齐,这时候才反应过来要给卡片加 justify-items: center(或者直接在容器上设 justify-items: center 统一控制所有卡片内容)。
看个具体的例子,我们当时的代码是这样的:
这里有个坑:如果网格轨道的总宽度小于容器宽度,justify-content: center 会让轨道整体居中,两边的空白会均匀分布。但如果轨道总宽度等于容器宽度(比如 12 列刚好填满),justify-content 就没效果了。我们当时大屏宽度是 1920px,12 列每列 1fr,加上 gap 20px*11=220px,总宽度刚好 1920px,所以 justify-content 没起作用——后来我们把 grid-template-columns 改成了 repeat(12, minmax(0, 1fr)),让列可以收缩,轨道总宽度小于容器,justify-content 才生效。
还有一次,我们做移动端适配时,用了 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)),然后设了 justify-content: space-between,结果发现最后一行的卡片如果数量不够,会分散在两边,中间空一大块。这是因为 justify-content: space-between 会把多余空间分配到轨道之间,而最后一行的轨道数量少,所以间距会变大。解决办法是用 justify-content: start,或者改用 grid-template-columns: repeat(auto-fill, minmax(150px, 1fr))——auto-fill 会创建隐式轨道(即使没有内容),让最后一行的卡片左对齐,看起来更整齐。
排查那个新人的问题时,我发现他把 justify-items 写在了容器上,但卡片里用了 display: flex,导致 justify-items 失效——因为 Grid 项目的对齐属性只作用于 Grid 布局上下文,如果项目自己变成了 Flex 容器,那它的子元素对齐就得用 Flex 的 justify-content 了。后来我们统一规范:Grid 容器用 justify-content 控制轨道对齐,Grid 项目如果内部是简单内容(比如文本、图片)就用 justify-items(或容器统一设),如果内部是复杂布局(比如多个元素排列)就改成 Flex 布局,用 Flex 的对齐属性。
现在我们的团队文档里专门写了这俩属性的对比表:justify-content 作用于容器,影响轨道位置;justify-items 作用于项目,影响内容在轨道内的位置。还加了个备注:如果用了 grid-template-areas 定义区域,justify-items 会作用于区域里的项目,比直接给每个项目写对齐属性方便多了。比如我们那个社区首页的 .header 区域,用 grid-template-areas: "header header" 定义后,直接给容器设 justify-items: center,导航菜单就居中了,不用每个菜单项单独设。
去年我们在重构一个企业级后台的订单管理系统时,遇到了一个非常典型的布局痛点。这个系统运行在 Chrome 102 和 Safari 15.4 环境下,页面上有一个包含 50 条记录的订单列表,每条记录是一个复杂的卡片组件。卡片内部左侧是商品缩略图,中间是三行信息(订单号、商品规格、物流状态),右侧是操作按钮组。
我当时的需求是:无论中间的文字行数怎么变,右侧的操作按钮必须和第一行的订单号基线对齐,同时左侧的图片要和整个卡片的垂直中线对齐。
在 CSS Grid Level 1 的时代,我通常的做法是给卡片加一个 Grid,给父列表再加一个 Grid。但问题来了,子元素的 Grid 轨道是独立的。如果我在父容器定义了 grid-template-columns: 100px 1fr 150px;,到了子组件里,我必须手动再写一遍几乎一样的列宽,或者依赖于 align-self 做微调。一旦父级的列宽因为响应式设计变成了 120px,子组件内部的布局就会瞬间错位。那次为了修复这个错位,我写了大量的 calc() 和媒体查询覆盖,导致样式代码膨胀了 30%,维护起来极其痛苦。
后来我研究了 W3C 的草案,发现 CSS Grid Layout Level 2 正是为了解决这个问题而生的。核心特性 Subgrid(子网格) 于 2023 年 进入了候选推荐标准(CR)阶段,并且在 Chrome 117 和 Safari 16 中已经得到了原生支持。Subgrid 允许一个网格项继承其父容器的网格定义,而不是建立一个新的网格上下文。
这就像是你在一个大表格里画了一个小表格,小表格的边框能完美地和大表格的格线对上,而不是自己瞎画。
在我们的订单系统里,我是这样应用 Subgrid 的。首先,父容器定义好全局的网格轨道:
通过 grid-template-columns: subgrid;,.order-card 不再拥有自己的列宽逻辑,而是直接“借用”了 .order-list 的列宽。这意味着,如果我在父级把第一列改成 120px,子组件里的缩略图区域会自动跟着变宽,完全不需要额外的 CSS 覆盖。
为什么这么做? 在引入 Subgrid 之前,我们的组件库为了实现这种对齐,往往需要在子组件里接收父组件的列宽变量(比如通过 CSS Variables 传递 --sidebar-width),这不仅增加了 JS 逻辑,还让 CSS 变得极其脆弱。一旦变量传递中断,布局就崩了。
不这么做会怎样? 如果不使用 Subgrid,你只能选择 Flexbox 或者独立的 Grid。Flexbox 很难同时控制行和列的精确对齐;独立的 Grid 则会导致嵌套层级越深,样式越难同步。我们之前的一个旧版组件,因为嵌套了三层 Grid,在大促期间(数据量激增到 500 条)导致浏览器重排(Reflow)耗时从 80ms 飙升到了 450ms,排查下来发现就是因为每一层 Grid 都在独立计算尺寸,GPU 合成层过多。
现在,Subgrid 让组件回归了“只关心内容,不关心布局上下文”的理想状态。虽然目前 Firefox 的支持最久,Chrome 和 Safari 也是近一两年才跟上的,但在内部后台系统中,我们已经可以放心大胆地使用了。
前阵子我们在做一个类似 Pinterest 的素材库功能,要求图片高度不一,但排列要紧凑,不能有大的空白。当时我第一反应是去 GitHub 上搜 Masonry 布局的 JS 库。那个库确实好用,但在我们的素材库里,当用户滚动加载到第 200 张图片时,页面的滚动帧率直接从 60fps 掉到了 28fps,内存占用也涨到了 350MB。排查后发现,JS 在疯狂计算每一个元素的位置并操作 DOM 的 top 和 left,触发了海量的重排。
我就在想,既然 CSS Grid 这么强大,为什么不能原生支持这种布局?
根据 W3C 的 2024-2026 年发展趋势,原生的 Masonry(瀑布流) 布局确实正在草案讨论中。虽然目前还没有正式成为标准,但我们可以通过 grid-template-rows: masonry;(目前仅在 Firefox 实验性开启)来窥探未来。如果未来这个属性正式落地,我们就不再需要那个重达 45KB 的 JS 库了,浏览器引擎会直接在底层用 C++ 处理布局,性能提升是指数级的。
除了 Masonry,另一个让我感到兴奋的是 容器查询(@container) 与 Grid 的结合。
我们那个素材库组件,有时候放在侧边栏收起的宽区域,有时候放在弹窗里的窄区域。以前我得写一堆媒体查询(@media)去判断视口宽度,然后调整 Grid 的列数。但现在,我更倾向于使用容器查询。
我在一个真实的项目里是这样实践的:
假设我们有一个图片卡片组件 .image-card,它内部有一个标题和一个图片。以前我们是根据屏幕宽度来决定一行放几个,现在是根据这个卡片被放进的容器宽度来决定。
在这个例子里,我没有关心用户的屏幕是 1920px 还是 1366px,我只关心 .media-gallery 这个容器本身有多宽。
为什么这么做? 因为组件应该是独立的。如果组件依赖视口查询,那它就无法在页面的不同位置复用。我们之前的一个数据看板,因为组件写死了 @media (min-width: 1200px),导致把它塞进一个 800px 宽的 Tab 页时,布局直接乱套了。改成容器查询后,那个看板的渲染逻辑变得非常干净,无论放在哪里,它都能自适应。
实际收益: 在引入容器查询后,我们那个素材库页面的 CSS 代码量减少了 40%,因为不再需要为了适配不同页面而写大量的覆盖样式。同时,由于布局逻辑交给了浏览器,我们在低端安卓机(测试机为 Redmi 9A)上的滚动流畅度提升了约 15%。
虽然原生的 Masonry 还在草案中,但 Grid 配合容器查询已经能解决 90% 的复杂响应式场景。作为开发者,我现在写布局的思路已经变了:宏观架构用 Grid 画大格子,微观对齐用 Flexbox 调细节,而自适应逻辑则交给容器查询。这种组合拳,是目前前端布局最稳健的方案。
去年我接手了一个百万 DAU 社区首页的重构。原来的布局是用 Flexbox 一层套一层堆出来的,看着没啥问题,但一到平板或者超宽屏,间距和比例就全乱了。我一开始也懒,想继续打 Flex 的补丁,结果越改越复杂,代码里全是 calc() 和各种 nth-child 的 Hack。
后来我干脆心一横,用 Grid 重写了整个容器。
我遇到的具体麻烦是:首页有个“热门话题”区,左边是一个大卡片,右边是三个小卡片。用 Flex 做这种非对称布局特别拧巴。换成 Grid 后,我直接画了一个 2fr 1fr 的轨道,然后用 grid-template-areas 把卡片像拼图一样摆进去。最爽的是,我配合 minmax() 函数,直接把原来写了三套的媒体查询给干掉了。
但我也得说实话,Grid 不是银弹。
* 适合用的场景:当你面对的是二维布局(既要管行又要管列),或者像后台系统那种结构固定的页面,Grid 绝对是降维打击。
* 不适合的场景:如果你只是做一个导航栏,或者简单的列表流,千万别上 Grid。我试过为了用 Grid 硬写一个横向列表,结果反而比 Flex 麻烦,而且低版本内核的兼容性处理起来很烦人。
别去死记那些属性,真的记不住。我当时的做法是:打开浏览器开发者工具,对着 grid-template-areas 画草图。看着那些虚线网格在页面上出现,你对“轨道”和“对齐”的理解会瞬间具象化。
还有,别为了炫技而用。如果 Flex 两行代码能解决,就别写 Grid 的八行代码。技术选型不是为了让自己显得厉害,而是为了让代码在半年后还能被轻松维护。