做前端这些年,瀑布流大概是让我又爱又恨的布局需求之一。浏览图片站点时那种错落有致的卡片墙确实好看,可真到自己动手实现时,总能掉进各种布局陷阱里:Flexbox拉不开错落感,column会把卡片顺序打乱,JS定位方案又是成堆坐标计算。而这两年终于不一样了,CSS 开始把瀑布流当作原生能力引入,圈内直接把它称为瀑布流布局的“终极解决方案”,也就是 CSS 网格布局里的 masonry(砖石)模式。这篇文章我会把它的原理、写法、回退方案和实际踩坑都梳理一遍,帮你判断它到底能不能用、怎么用才稳。
1. 传统方案的困局:瀑布流为什么会难住前端
1.1 Flexbox的“等高陷阱”
先说Flexbox。很多人第一次做瀑布流,会下意识用display: flex; flex-wrap: wrap;然后发现效果完全不对,原因是 flex 本质上是一维布局,它的核心逻辑是“排队”。一排排完之后换行,但每一行的高度由这一排里最高的项目决定,其余项目会被拉伸到同样的高度,这就出现了瀑布流最忌讳的“等高”。
你可能马上会想到用align-items: flex-start让项目顶部对齐,确实可以解决拉伸问题,但底部依然是一条参差不齐的曲线,而且下一排会从上一排最高的那个项目底部开始排。结果就是整体布局看起来像“台阶”,不是“瀑布”。我用生活类比给你解释:flex 像排队领餐,不管队伍里的人高矮胖瘦,排尾基本在一条直线上;瀑布流则是把每个人安排到不同队伍里,谁矮了后面的人就顶上,最终所有队伍的底边尽量齐平。这是两个完全不同的布局模型,Flexbox 在底子上就不适合做这件事。
1.2 column布局的顺序陷阱
既然 Flexbox 不行,很多人会转向多列布局columns。这确实能做出错落效果,而且代码很短,核心思路是把卡片塞进多个纵向列里,配合break-inside: avoid防止卡片在列中间被截断。但它的致命问题是:阅读顺序不对。
当浏览器把内容分列时,它会先把第一列填满,再填第二列。举个例子,DOM 顺序是卡片 A、B、C、D、E、F,在 3 列布局下,第一列大概率是 A、B,第二列是 C、D,第三列是 E、F。用户从左往右扫视页面时,看到的顺序却是 A、C、E,完全打乱了原始内容顺序。如果内容是图片墙或灵感收藏,用户不太在意先后;但如果是一个按时间排列的动态流,这个缺点就非常要命。
另外,columns还有一个隐性问题:它要求容器有确定的高度,否则分列逻辑会变得不可预测。你在响应式页面里很难精确控制每列的高度,一旦内容增多,列之间的平衡也不由你说了算。所以 column 方案只能算“视觉上的瀑布流”,不是结构上的瀑布流。
1.3 JS定位方案的性能与维护代价
既然纯 CSS 做不到理想效果,很长时间里大家只能抱紧 JS 的大腿。思路也很直白:把容器设为相对定位,卡片全部绝对定位,然后维护一个“每列当前高度”的数组,每插入一张卡片,就把它放进当前最短的那一列。听上去不难,但实际写起来全是细节:图片没加载完高度是未知的,你得监听每张图片的load事件再重新布局;窗口尺寸变化时要重新计算列数和所有卡片坐标;新插入内容时又要重排全部节点。
这些逻辑不是不能写,只是每一条都是维护负担。我还踩过更隐蔽的坑:当页面里同时存在懒加载、动态过滤、排序功能时,JS 布局代码很容易和状态管理打架。比如用户筛选了某个条件后,卡片少了几个,如果忘记重新计算列高,后续卡片就会叠在一起或者出现大块空白。第三方库能解决一部分问题,但当你需要定制跨列卡片、自定义动画、服务端渲染布局时,库反而成了限制。这也是为什么社区一直期待浏览器原生能力的原因。
2. 新特性解析:原生masonry的关键语法
2.1 核心语法:一行代码改变网格规则
masonry 模式基于 CSS Grid Layout Level 3,代码形态非常朴素。你不需要引入任何库,也不需要计算坐标,只需要在一个 Grid 容器上把其中一个轨道轴设置为masonry。先看最典型的竖向瀑布流:
.masonry-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); grid-template-rows: masonry; }就这么两行核心配置:grid-template-columns负责定义列的数量和宽度,grid-template-rows: masonry让行方向不再使用刚性的网格轨道,而是根据项目内容的高度自动堆积。第一次看到这个写法,你可能会觉得“这也太简单了吧”,但恰恰是这种简单,暴露了它和 JS 方案的本质区别:布局规则由浏览器内部算法完成,不再需要开发者手动维护任何状态。
这个模式在规范里叫“砖石布局”,灵感来自砌墙时砖块自然错落的形态。你可以把它理解成“带列的网格”:列是有明确边界的,行方向则像流体一样跟着内容走。如果你反过来写grid-template-rows: repeat(3, 1fr); grid-template-columns: masonry;,就能得到一个横向瀑布流,常用于时间轴、横向卡片墙这类需要水平滚动的场景。
2.2 从两个维度理解排列逻辑
要真正用对 masonry,得理解它的排列算法。常规 Grid 是“先定义行列轨道,再把项目填进格子”,项目必须落在某一行和某一列的交叉点;masonry 则把其中一个轴从刚性轨道释放出来,让项目沿着可用轨道“自然坠落”。
拿竖向瀑布流来说,浏览器会维护每一列当前的堆叠高度。第一个项目放进第一列,第二个项目放第二列,第三个项目放第三列,第四个项目来的时候,它会自动选择当前高度最低的那一列,而不是简单地按 DOM 顺序一行行排。这个过程完全发生在浏览器内部,不需要 JavaScript 参与。这正是瀑布流真正的核心:让每一列的底边尽量齐平。
我习惯把这个机制类比成“倒沙子”:你往几个并排的杯子里倒沙子,沙子总是先填满最低的那个杯子。masonry 就是浏览器帮你把每个“项目颗粒”按最短列原则排布。这也解释了为什么它比 column 更适合瀑布流,因为 column 是“先倒满第一杯,再倒第二杯”,项目最终归属哪一列取决于出现顺序,而不取决于列的高度。
2.3 和其它布局能力的组合空间
masonry 并不是脱离 Grid 独立存在的,它本质上仍然是一个 Grid 容器,所以很多你已经熟悉的属性可以继续用。比如gap控制卡片间距,grid-template-columns的repeat、auto-fill、minmax等函数照常生效。项目本身也可以设置grid-column: span 2实现跨列效果,这在普通 Grid 和 masonry 模式下都成立。
但要注意,跨行属性grid-row: span n在 masonry 模式下的行为并不稳定,因为行方向已经没有固定的网格线了,浏览器很可能直接忽略它。我的建议是:在 masonry 容器里只做跨列,不做跨行,想控制某个卡片的高度,直接给内容设置宽高比或者min-height,效果远好过依赖行轨道。
3. 实操上手:构建一个生产可用的瀑布流组件
3.1 先搭HTML卡片结构
纸上谈兵没有意思,下面我带你从头搭一个图片卡片瀑布流。HTML 结构我保持最简单,每个卡片包括一个媒体区、一个标题和一段描述,你完全可以照着扩展到自己的业务场景里。
<main class="masonry-grid"> <figure class="card"> <div class="media media--tall"></div> <figcaption> <h2>山间清晨</h2> <p>雾气还没散,山脊线若隐若现,适合早起徒步的一条路线。</p> </figcaption> </figure> <figure class="card"> <div class="media media--square"></div> <figcaption> <h2>街角咖啡店</h2> <p>开在梧桐树下的小店,手冲和可颂都不错。</p> </figcaption> </figure> <figure class="card card--wide"> <div class="media media--4x3"></div> <figcaption> <h2>江边长廊</h2> <p>傍晚散步的好去处,灯亮起来之后氛围很舒服。</p> </figcaption> </figure> <!-- 继续添加更多卡片 --> </main>我刻意让媒体区通过类名区分不同宽高比,因为瀑布流的美感来自“卡片高度参差”,而不是宽度参差。如果你的卡片在普通 Grid 里会显得呆板,问题往往出在你把每张卡片的配图都做成了同样的宽高比。
3.2 用两段CSS完成渐进增强
接下来是 CSS。这里我推荐一种“渐进增强”写法,而不是直接无脑上 masonry,因为现在还没有到所有浏览器都默认支持的程度。先写一个普通 Grid 作为基线,再在支持 masonry 的环境里开启瀑布流:
.masonry-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 20px; } @supports (grid-template-rows: masonry) { .masonry-grid { grid-template-rows: masonry; } } .card { border-radius: 12px; overflow: hidden; background: #fff; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .card img { display: block; width: 100%; height: auto; } .media { background: linear-gradient(135deg, #f0f0f0, #e2e2e2); } .media--4x3 { aspect-ratio: 4 / 3; } .media--square { aspect-ratio: 1 / 1; } .media--tall { aspect-ratio: 3 / 4; }这段代码的思路很关键:在支持grid-template-rows: masonry的浏览器里,卡片会变成错落瀑布流;在不支持的浏览器里,它仍然是一个普通网格,卡片可能高度不齐,但排列规整、没有重叠,体验不会崩塌。@supports本质上是浏览器能力检测,只有真正理解这个属性值的浏览器才会命中里面的规则,老浏览器会直接忽略它,回到基线网格。
auto-fill和minmax的搭配是我强烈推荐的列定义方式。它能让列数随容器宽度自动变化:容器宽,列数多;容器窄,列数少。你不需要写一堆媒体查询,也不需要监听 resize。唯一需要注意的是240px这个最小值决定了一列的最小宽度,你可以按卡片最小允许宽度调整。
3.3 响应式列数与跨列卡片
如果产品需求要求“手机上 1 列、平板上 2 列、桌面上 3 列”,我建议直接用媒体查询覆盖列数定义,而不是用 JS 去算。原因很简单:CSS 能搞定的事就不该交给 JS,尤其是 resize 监听在移动端频繁触发,写不好还会掉帧。
.masonry-grid { grid-template-columns: 1fr; } @media (min-width: 600px) { .masonry-grid { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 1024px) { .masonry-grid { grid-template-columns: repeat(3, 1fr); } }跨列卡片在瀑布流场景里非常常见,比如某张卡片想突出展示,占两列宽度。实现方式也简单:
.card--wide { grid-column: span 2; }注意,在普通 Grid 里,跨列往往会在其他列留下一条空白行;但在 masonry 模式下,后续项目会自动填充到最短列,跨列造成的空隙会被后续内容补偿一部分,所以视觉上比普通 Grid 自然很多。不过也不要把跨列卡片放得太密集,跨列越多,列底部的齐平难度越大,可能出现局部空洞。
3.4 用图片占位减少布局抖动
瀑布流最烦人的一件事就是图片加载时布局跳动。图片没有加载完成时,容器高度为 0,浏览器以为这个卡片很矮,把它排进最短列;等图片加载完,卡片突然变高,整个布局又要重排。原生 masonry 虽然由浏览器计算布局,但也不能预知未来图片的尺寸,这个抖动问题依然存在。
解决方案就是给媒体区一个宽高比占位,也就是代码里的aspect-ratio。这一步是在告诉浏览器:这块区域的高度是宽度的 3/4 或者 4/3,请在图片加载前就预留出空间。配合图片自身的width、height属性和loading="lazy"属性,能做到基本无跳动:
<img src="photo-1.jpg" alt="山间清晨" width="800" height="600" loading="lazy" decoding="async" />width和height是 HTML 原生属性,浏览器可以根据它们算出宽高比,即使aspect-ratio没生效,也能尽量预留空间。loading="lazy"让屏幕外的图片延迟加载,decoding="async"避免图片解码阻塞页面渲染。这几项加起来,瀑布流的体验会稳定很多。
3.5 无限滚动场景的懒加载实践
图片站点基本躲不开无限滚动。一次性渲染上千个卡片节点,即便原生布局性能再好,DOM 数量太多还是会拖慢页面。通用的做法是分段加载:用 IntersectionObserver 观察一个底部哨兵元素,当它进入视口附近时,再去请求下一页数据并插入卡片。
<div class="sentinel"></div>const sentinel = document.querySelector('.sentinel'); const grid = document.querySelector('.masonry-grid'); let page = 1; const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { loadMore(page++); } }, { rootMargin: '200px 0px' }); observer.observe(sentinel); async function loadMore(page) { const items = await fetch(`/api/items?page=${page}`).then(res => res.json()); const fragment = document.createDocumentFragment(); items.forEach(item => { fragment.appendChild(createCard(item)); }); grid.appendChild(fragment); }这里的DocumentFragment值得一提。如果你直接循环调用appendChild,浏览器每插入一张卡片都会触发一次布局计算;换成DocumentFragment批量追加,浏览器只会重排一次,性能差距在高频滚动时非常明显。这个方法跟用不用 masonry 没关系,属于无限滚动场景里的通用基本功。
4. 踩坑记录与性能排查
4.1 顺序问题:瀑布流到底该按什么排序
这是我在实际项目里被问得最多的问题。masonry 模式下,视觉顺序并不完全等同于 DOM 顺序。第一个卡片在左上角,第二个卡片可能跑到右上角,第三个回到第二列顶端,整体形成一个“横向优先”的视觉流。如果你做的是图片社区、商品墙、灵感收藏夹,这个顺序问题用户基本感知不到,甚至觉得更有浏览感。
但如果你是做一个按发布时间排列的动态列表,或者一个“第一步、第二步、第三步”的教程卡片,我建议想都不要想瀑布流。用户已经习惯了从上到下、从左到右的阅读顺序,强行让他蛇形扫视,会产生理解负担。这个坑不是 masonry 没做好,而是你选错了场景。
4.2 跨列卡片为什么会产生空隙
我在 3.3 里说过跨列会让后续项目补偿空隙,但补偿不等于完全消失。假设你有三列,第二个位置放了一个跨两列的宽卡片,它占用了列 1 和列 2 的空间,此时列 3 很短,下一个普通卡片就会填到列 3 顶部。视觉效果是:第一行有一个宽卡片,旁边的列顶部有一张孤零零的图片。这在某些审美要求高的页面里是硬伤。
我的建议是,跨列卡片尽量放在瀑布流开头,并且数量不要多。如果你想做“首图大卡 + 后续小卡”的效果,把宽卡片放在第一张,后面的普通卡片会均匀填充剩余列,效果通常比穿插跨列好得多。
4.3 图片加载后布局跳变
这个问题在 3.4 已经聊过,从排查角度再说一次。如果你发现页面在滚动过程中卡片位置不断变化,优先检查两件事:一是图片有没有设置宽带比占位;二是卡片容器有没有最小高度。很多写得不规范的懒加载组件会把图片高度设为auto,但容器没有aspect-ratio,图片加载前和加载后的高度差会很大。你可以在 DevTools 里截停网络请求,看布局是否稳定,这是最直接的排查手段。
4.4 浏览器兼容性的排查思路
masonry 的浏览器支持目前还没有完全统一。以我自己的使用感受来说,火狐浏览器已经在较新的稳定版里默认开启了这个能力,另外几款主流浏览器还在实验阶段,需要手动开启实验开关才能看到效果。所以生产环境里“直接裸用 masonry”仍然有风险,我的底线是必须走@supports渐进增强。
排查时可以先用一个最简单的页面测试:
.test-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: masonry; }如果浏览器支持,容器里的项目会明显错落;如果不支持,grid-template-rows: masonry会被忽略,项目变成规整网格。打开开发者工具里的 Computed 面板,看grid-template-rows的计算值,也能快速确认当前浏览器是否生效。注意不要只看 Sources 里的样式是否高亮,计算值才是最终结果。
4.5 方案对比:原生masonry、column与JS定位
我把三种方案放在一张表里对比,方便你做技术选型:
| 维度 | 原生 masonry | column 多列 | JS 绝对定位 |
|---|---|---|---|
| 内容顺序 | 保持 DOM 顺序 | 列优先,顺序被打乱 | 完全可自定义 |
| 实现复杂度 | 极低,两行 CSS | 低,但控制力弱 | 高,需维护列高数组 |
| 响应式列数 | 自动或媒体查询 | 需要重置 column-count | 需要 resize 监听 |
| 性能 | 浏览器内部布局算法 | 一般 | 节点多时计算压力大 |
| 图片加载处理 | 需配合 aspect-ratio | 需配合 break-inside | 必须监听图片加载 |
| 适用场景 | 图片墙、卡片流 | 简单的文字分栏 | 复杂交互和自定义排序 |
这张表其实回答了一个问题:为什么大家都说 masonry 是终极方案。不是说它十全十美,而是在“错落布局”这个细分赛道上,它第一次让 CSS 具备了原生的、符合直觉的、高性能的表达能力,不再需要开发者用各种“模拟手段”去凑效果。
5. 实践建议与个人体会
5.1 我推荐的落地组合
如果你现在要在一个新项目里做瀑布流,我的建议是不要直接梭哈进 masonry,也不要因为它兼容性还没普及就放弃。最优解是“普通 Grid 基线 + @supports 渐进增强 + 宽高比占位 + 懒加载”。这套组合在任何浏览器里都能呈现可接受的布局,在支持 masonry 的浏览器里则能获得接近完美的瀑布流体验。
具体代码可以参考前面 3.2 的写法,它兼顾了两个方向:从兼容性来说,老环境不会崩;从布局效果来说,新环境足够惊艳。我的习惯是再补一条全局样式,把图片的默认尺寸行为统一,避免不同卡片比例差异过大造成的视觉混乱。比如给所有卡片配图统一设置object-fit: cover,这样即使图片原始比例不同,裁切后也能保持整齐的边界。
5.2 选型前需要想清楚的三个问题
第一,你的内容有没有强顺序需求?有就老老实实用列表或普通 Grid,没有才考虑瀑布流。第二,你的用户浏览器环境是否支持@supports和aspect-ratio?这两项现代 CSS 的普及率已经很高,但如果你服务的用户群体使用较老的系统浏览器,还是要做好基线体验设计。第三,你是不是真的需要“无限滚动 + 瀑布流”的组合?无限滚动会放大布局复杂度,尤其是跨列+懒加载+筛选同时存在时,建议先简化数据交互,再谈视觉效果。
我个人在实际操作中最深的体会是,masonry 减少的不只是代码量,更是心智负担。以前用 JS 方案时,每次新需求进来我都会提心吊胆,生怕哪个状态没同步导致卡片重叠。换成原生 masonry 以后,容器只负责声明“按列尽量填满”,剩下的交给浏览器。对于我这种能少写 JS 就少写 JS 的人来说,这确实是我目前见过最接近“终极解决方案”的 CSS 能力。等它进入更多浏览器的稳定基线之后,我大概率会把项目里的所有图片流页面都切过来。如果你也想尝鲜,建议先拿一个内部项目或者 demo 页试水,感受一下“两行 CSS 搞定瀑布流”究竟有多顺手。