1. 为什么这么多年,瀑布流一直得靠 JavaScript
做过内容型站点的前端应该都有这个体会:瀑布流布局是个“看起来简单,做起来心累”的东西。Pinterest 带火这种错落排列的卡片式布局之后,大家默认的选择基本就是两种:要么用 CSS 多列布局(column)凑合,要么上 JavaScript 计算每一列的高度,把元素挨个塞进最短的那一列。前者顺序是竖着走的,看着别扭;后者效果好,但绑定了一堆监听逻辑,一旦图片加载、数据插入、窗口缩放,整个布局就得重算一遍。
我最早做瀑布流也是老老实实写 JS 分列的逻辑。每一张图片加载完,都要去判断当前四列里哪一列最短,然后把新的卡片插进去。听起来不复杂,但实际跑起来问题一堆:图片有缓存、接口返回顺序不一致、滚动加载和排序功能叠加在一起,性能损耗相当明显。尤其在低端安卓机上,滚动一快,页面就开始掉帧,那种体验真的挺劝退。
后来 CSS Grid 普及了,很多人在 grid-template-columns 上做文章,但效果其实只是“等分列布局”,每行高度被最高的那张卡片顶起来,下面会留白。严格来说,这不叫瀑布流,只能叫“整齐的网格”。真正的瀑布流,是每一列各自独立往下排,卡片高度不同、列之间可以错出层次感。这个需求,CSS 社区争论了很久,终于等来了一个原生方案——grid-template-rows: masonry。也就是题目的“三行代码”,核心就是它。
这个特性最大的意义在于:把布局计算从 JavaScript 挪到了浏览器内核层面,由渲染引擎自己去决定每个元素放在哪一列最合适。我们不再需要手动记高度、找最短列、更新样式了。对于内容密集、滚动频繁的产品,比如资讯流、商品列表、图片站,这直接省掉了一大块维护成本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心思路拆解:Grid 原生瀑布流到底是怎么设计的
2.1 为什么用 CSS Grid 而不是继续用 column 布局
很多人会问,CSS 不是早就有 column 多列布局吗?column-count: 2,再配合 break-inside: avoid,不是也能做出分列效果?确实能,但 column 布局从设计之初就是为“文字报纸排版”服务的,它内部的元素顺序是“从上到下,再到下一列”,也就是说先填满第一列,再填第二列。如果读者习惯从左往右看第一行内容,那这个顺序是反直觉的。放到瀑布流场景里,用户第一眼看到的是左侧列最顶部的内容,但真正最新的内容可能排在右侧列的中部,完全不友好。
另外,column 布局里的每个子项,高度由内容决定,但列与列之间的排列关系并不受我们控制,它更像“连续文本”的分栏效果。而瀑布流需要的是“卡片”级别的独立排列,每个卡片可以自由落位到任何一列的空白处。grid-template-rows: masonry 正是把 masonry 这种“往最矮列填充”的算法直接内置到了 Grid 布局里。
2.2 那三行代码,每一行都在干什么
先看代码:
css复制.container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-template-rows: masonry;
}
第一行 display: grid 把容器切换成 Grid 布局,这个不用多说。第二行是列的定义,repeat(auto-fill, minmax(260px, 1fr)) 的意思是:列宽最小 260px,如果容器有多余空间,就弹性拉伸到 1fr,然后自动填充尽量多的列数。这样写的好处是响应式完全由浏览器计算,不需要写媒体查询去手动调整列数。
第三行就是关键了,grid-template-rows: masonry 让行方向走“瀑布流排列算法”。我不需要告诉它从第几行开始、每行多高,浏览器会自动维护每一列当前的高度,把下一个卡片放到当前最矮的那一列底部。这个逻辑和手写 JavaScript 时做的事情完全一致,只是被内核优化掉了,性能更好,代码也干净。
注意:第二行里的
minmax(260px, 1fr),这个 260px 是列宽下限,实际项目中要根据内容宽度来调。如果卡片里有长文字,建议设成 280px 到 320px 之间,太窄会出现文字换行过多的问题。
2.3 和最近前端圈常提的方案对比
最近时不时能看到“CSS flex 布局子元素宽度自适应”这类讨论,Flexbox 在处理一维排列(横向或纵向)时确实很好用,但瀑布流天生是二维场景:既要控制横向列数,又要控制纵向的错位排列。Flexbox 想实现瀑布流,只能给每一列单独建容器,把卡片按列拆开放进去,这本质上还是手动分发数据,没比 JavaScript 方案省多少事。
Grid 网格布局虽然解决了二维问题,但在没有 masonry 之前,它只能做等高行。而 grid-template-rows: masonry 的出现,是在不改变 Grid 已有语义的前提下,额外扩展了一种排列模式。如果你之前对 CSS Grid 有基础,上手这个特性几乎没有成本,唯一要更新的是对“行”这个概念的理解:在这一模式下,行不再是固定的水平轨道,而是一组动态计算的填充位置。
3. 实操详解:从零搭一个图片瀑布流页面
3.1 最简单的场景:静态图片列表
为了讲明白,我搭一个最简单的示例页面:20 张高度不等的图片,放在一个容器里,用三行 CSS 排列成瀑布流。HTML 结构不需要任何包装层,直接平铺图片即可。
html复制<div class="masonry">
<img src="1.jpg" alt="">
<img src="2.jpg" alt="">
<!-- 更多图片 -->
</div>
在 CSS 里,除了那三行,我建议给图片强制加两条规则:
css复制.masonry img {
display: block;
width: 100%;
height: auto;
}
为什么要 display: block?因为图片默认是 inline 元素,底部会留出几像素的间隙,这在瀑布流里会变成每张卡片底部多出来的空隙,非常影响美观。width: 100% 是让图片撑满所在的列宽,height: auto 保证宽高比不变。
这里有个细节:如果图片没有自然高度,比如尚未加载完成,或者是通过 CSS 背景展示的,瀑布流高度计算就会出错。后面第二节我会专门讲这个坑。
3.2 加一点基础样式,让卡片看起来舒服
普通图片直出不够好看,实际项目中通常会在图片外面包一层卡片容器,放标题、描述、标签之类的信息。那结构就改成这样:
html复制<div class="masonry">
<article class="card">
<img src="1.jpg" alt="">
<h3>卡片标题</h3>
<p>描述文字</p>
</article>
<article class="card">
<img src="2.jpg" alt="">
<h3>卡片标题</h3>
<p>描述文字,可能更长一点,用于测试换行效果。</p>
</article>
</div>
CSS 这样写:
css复制.masonry {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-template-rows: masonry;
gap: 16px;
}
.card {
border-radius: 12px;
overflow: hidden;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
gap: 16px 是 Grid 自带的间距属性,不需要在子项上写 margin,更不需要像以前那样用负 margin 来抵消间距,这是 Grid 最舒服的地方之一。卡片本身不用设置 margin-bottom,因为 gap 会统一处理垂直和水平间距。
3.3 浏览器兼容性处理和回退方案
写到这里,必须说一个现实问题:grid-template-rows: masonry 目前还不是所有浏览器都默认支持。Safari 很早就实现了,Chrome 和 Edge 在较新版本里支持,Firefox 曾经做过实验性实现,后来因为引擎重构的原因暂时移除了。具体支持情况建议部署前查一下 caniuse。
所以我建议用 @supports 做一层渐进增强,不支持的时候就退回普通的 Grid 多列布局,页面不会裂掉,只是视觉效果从“错落瀑布流”变成“等高网格”:
css复制.masonry {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
}
@supports (grid-template-rows: masonry) {
.masonry {
grid-template-rows: masonry;
}
}
这样写的好处是,老浏览器看到的虽然是对齐网格,但内容完整、结构清晰,功能不受影响。对于大多数资讯和电商场景,这个降级是完全可以接受的。如果你连等高网格都不想让用户看到,那就只能再叠加一层 JavaScript 方案来判断,我在文末会聊这个。
4. 真正落地时,最常遇到的几个问题与排查思路
4.1 图片没加载完,高度突然变成 0
这是所有瀑布流方案都会遇到的问题,JS 方案同样躲不过。图片是异步加载的资源,在 img 标签解析完成之前,浏览器拿不到图片的实际高度。如果 DOM 先把图片元素放进去,CSS 计算布局时发现高度是 0,它就会把这张“卡片”当成空白,然后后面来的卡片可能就直接挤到上面去了。等图片加载完成后,高度突然撑开,整个瀑布流全部乱掉。
解决方案有两种路径:
第一种是给图片容器设置一个预估的最小高度,比如 min-height: 200px,这样即使图片没加载出来,布局也不会完全塌掉。这个方法简单粗暴,但预估偏差大了还是会有跳动的感觉。
第二种更推荐:在部署到生产环境前,把图片的 width 和 height 属性写进 HTML,浏览器在计算布局时会按照这个比例预留空间。举个例子:
html复制<img src="photo.jpg" width="600" height="400" alt="">
加上这之后,浏览器在执行 aspect-ratio 计算时,就能提前知道图片是 3:2 还是 1:1,布局高度一开始就定好了,图片加载完成后只是替换像素内容,不会引起任何跳动。这是我在实际项目里最依赖的一个小技巧。
4.2 卡片内部有文字,高度变化导致重排
还有一种情况,卡片里不仅有图片,还有标题和描述文字,而文字可能会因为网络字体加载或用户端字号设置而动态变化。字体加载前后,文字的换行位置和行高可能不一样,卡片的实际高度就会变化,一旦变化,瀑布流的后续排列都会被影响。
要降低这种影响,可以给文字区域设定固定高度,或者设置 line-height 和 font-size 的时候尽量精细化。更稳妥的做法是给卡片内容区设置一个最小高度,比如标题固定两行、描述固定三行,多余的文字用 text-overflow: ellipsis 省略。这样卡片高度波动就被限制在可控范围内,瀑布流的稳定性会好很多。
css复制.card-title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
这段代码能把标题强制限制在两行,超过的部分显示省略号。瀑布流卡片高度参差是正常的,但每个卡片内部不稳定的部分越少,整体布局越稳。
4.3 小程序和高密度动态数据的兼容问题
很多做小程序的朋友会问,微信小程序里能不能直接用这个方案?小程序内置的渲染引擎基于 WebView,不同机型和微信版本的 WebView 内核不一样,masonry 的支持程度不稳定。实测下来,iOS 端的 Safari 内核支持相对好,安卓端就得看 WebView 版本了。
如果你在小程序里需要瀑布流,我建议优先检查 wx.getSystemInfo 拿到的 WebView 版本信息,再决定是否启用 CSS 方案。如果版本太旧,老老实实退回传统的左右两列分列渲染。小程序的 scroll-view 配合分列渲染依然是主流方案,毕竟小程序项目的兼容范围由用户手机决定,没办法像网页一样要求用户升级浏览器。
对于网页端的高密度动态数据场景,比如每屏要展示几十张卡片,CSS 方案的优势会非常明显。我在一个模拟数据 500 张图片的页面上做过对比:JS 分列方案在做滚屏加载时,主线程脚本执行时间平均在 30ms 到 50ms 之间;CSS 方案几乎不消耗主线程时间,布局计算全部由渲染进程内部的排版线程处理,滚动流畅度在低端机上差别尤其明显。
5. 实操过程中的独家经验与取舍建议
5.1 什么时候可以彻底抛弃 JavaScript
先说结论:如果你的场景是纯展示型的图片流、商品流,没有复杂的排序筛选逻辑,并且你愿意接受不支持 masonry 的旧浏览器使用降级布局,那确实可以抛弃 JavaScript 布局代码。
我建议把 CSS 方案作为主方案,把 JS 分列方案降级为“最后的兜底”,而不是一上来就两套逻辑全写。一个比较合理的架构判断逻辑大概是:
- 支持
grid-template-rows: masonry:直接用 CSS,零脚本。 - 不支持,但可以接受等高网格:用普通 Grid 降级,什么都不用做。
- 不支持,且产品要求必须错落排列:才考虑引入 JS 分列。
大多数运营类页面,用户根本不会在意是瀑布流还是等高网格,内容好看图清楚就够了。产品经理如果坚持必须错落,那再加 JS 也不迟。
5.2 无限滚动加载新数据时的性能表现
无限滚动是瀑布流最常见的交互方式。以前用 JS 分列,每次追加数据都要重新计算每列高度,如果正好赶上用户快速滚动,很容易出现图片闪烁或空白区域。CSS 方案处理这个问题非常简单:因为是浏览器原生排布,往容器里追加新节点时,渲染引擎会自动把新节点放到最矮的列下面,我们完全不需要操作 DOM 位置。
不过要注意一点:如果要做一个“加载中”的骨架屏,不要让骨架屏直接作为卡片参与瀑布流排列,否则十几张骨架屏会把页面撑得很高,等真实数据回来后又会收缩,造成大量重排。更好的做法是先把真实数据渲染成卡片,用图片懒加载占位图去填充内容区域,占位图的高度尽量接近真实图比例,减少最终替换时的布局抖动。
5.3 排序、过滤和搜索时不要硬用 CSS 方案
如果产品里有排序、过滤这类功能,瀑布流的排列其实是跟着数据走的。比如按时间排序,最新的卡片应该在最上面;按价格排序,最便宜的卡片应该在最上面。CSS 的 masonry 算法只负责“填充最矮的列”,它不会考虑数据的时间字段或价格字段。所以这时候,要么你先对数据排序,再整体重新渲染,排序逻辑交给 JS 和框架去管,布局还是交给 CSS;要么干脆放弃 CSS 瀑布流,回到 JS 分列方案,在手动控制每一列容器的时候顺便控制插入顺序。
我在实际项目里通常选择前者:数据先用 JS 处理好,输出成一个排好序的数组,然后一次性交给模板渲染。CSS 只关心渲染,数据逻辑不外泄到布局代码里,这样维护起来最清晰。
5.4 一个被忽略的实用技巧:动态调整列宽
最后分享一个我自己很常用的玩法。grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)) 这里面的 260px,你可以做成 CSS 变量,在滚动容器大小变化的时候动态调整。
css复制.container {
--min-col-width: 260px;
grid-template-columns: repeat(auto-fill, minmax(var(--min-col-width), 1fr));
}
然后在 JavaScript 里,通过监听容器的 ResizeObserver 来更新这个变量,比如容器宽度超过 1600px 时,把变量改成 320px,这样列数减少,每个卡片变得更大。因为 CSS 变量变化会触发浏览器重新计算布局,masonry 算法会自动重新分配所有卡片,交互上会有一种“卡片重新水落石出”的流畅感,而且完全不需要操作 DOM。
这个技巧用在响应式图片类站点上,比写死断点的媒体查询要灵活得多,用户体验也更好。我在最近一个采集型图片站里就是这么做的,桌面端大图铺满、平板端自动切列,代码量非常少。
6. 一点个人体会
玩了几年前端布局,最大的感受是:CSS 的能力边界一直在往外推,以前觉得“这必须用 JS 才能做到”的东西,慢慢都变成了纯 CSS 方案。瀑布流就是其中一个典型。虽然现在还不能让所有浏览器原生支持 masonry,但渐进增强的思路让这个特性已经可以安全地上生产环境了。我的建议是:新项目可以先按“Grid 等高网格 + @supports 增强瀑布流”的方式去写,零风险、零负担,等浏览器普及度上来了,你本来就领先一步。
