css columns 不能用于真实瀑布流加载,因其按文档流切片导致新图插入时重排错乱、无法获取列高、懒加载失效;js 瀑布流需维护列高数组并动态追加到最短列,同时处理图片预加载、intersectionobserver 触发、响应式列数变更等细节。

直接用 column-count + column-gap 做瀑布流,能自动加载更多,但顺序错乱、无法监听图片高度、不支持懒加载 —— 这种方式只适合纯静态内容,动态场景下必须用 JS 控制列高。
为什么 CSS columns 不能用于真实瀑布流加载
CSS 的多栏布局(column-count)是按文档流顺序切片渲染的,图片 DOM 顺序 = 显示顺序。一旦你滚动触发新图插入末尾,浏览器会重新分栏,导致已加载图片位置跳变、高度塌陷、甚至重复渲染。更关键的是:column-count 完全不暴露每列实际高度,IntersectionObserver 无法精准判断“哪列最短”,也就没法把新图插到正确位置。
常见错误现象:
- 新图一加进来,整页重排,用户正在看的图片突然上移或消失
- 图片加载完成前留白严重,加载后又挤压其他列,触发多次回流
- 懒加载区域判断失准,明明在视口内,图片却没触发加载
JS 瀑布流核心:维护列高数组 + 动态追加
本质就两步:记录每列当前总高度,每次插入新图时找最小值列,append 到它内部。关键不是“怎么放”,而是“怎么持续维护高度”。容易被忽略的点:
- 列容器必须是独立
<div> 元素(如 <code><div class="col" id="col-0"></div>),不能靠position: absolute模拟 —— 否则offsetHeight取不到真实值 - 图片原始尺寸未知时,不能直接用
img.height,得先用new Image()预加载,拿到naturalWidth/naturalHeight,再按目标列宽等比缩放算出渲染高度 - 滚动加载新图时,
minHeight数组不能重置;要在已有列高基础上继续找最小列,否则新图全堆第一列
示例逻辑片段:
function appendToShortestCol(imgData) {
const cols = document.querySelectorAll('.col');
const heights = Array.from(cols).map(col => col.offsetHeight);
const minIndex = heights.indexOf(Math.min(...heights));
const targetCol = cols[minIndex];
<p>const img = document.createElement('img');
img.src = imgData.url;
img.onload = () => {
// 此处应根据 naturalWidth/naturalHeight 重设 height,避免回流
const ratio = img.naturalHeight / img.naturalWidth;
img.style.height = <code>${targetCol.clientWidth * ratio}px</code>;
};
targetCol.appendChild(img);
}</p>
懒加载与滚动监听的配合要点
IntersectionObserver 是首选,但必须配合适当的 rootMargin 和阈值,否则在快速滚动时会漏掉触发。更重要的是:观察目标不能是图片本身(因为还没插入 DOM),而应是“占位节点”或“加载提示元素”。
- 每次插入新图前,先在最后一列末尾加一个
<div class="loader-trigger"></div>,观察它是否进入视口 - 触发后,立即移除该触发器,再请求下一批数据,避免重复加载
- 节流不是必须的 ——
IntersectionObserver本身已做性能优化;但手动监听scroll事件时,必须用requestIdleCallback或时间戳节流 - 图片加载失败时,要 fallback 到默认占位图,并把对应列高减去预估值,否则后续图片全部错位
响应式列数变化时的列高重置陷阱
列数不是固定值。窗口缩放或横竖屏切换时,列数可能从 4 → 2 → 3 变化。此时不能简单清空 minHeight 数组,也不能强行保留旧数组长度 —— 正确做法是:
- 监听
resize,用getComputedStyle获取当前生效的列数(比如通过document.body.clientWidth / 250 | 0) - 若新列数 ≠ 旧列数,销毁旧列容器,重建对应数量的新
<div class="col">,并把原内容按顺序分发过去(不是按高度,是按 DOM 顺序)<li>分发后,再逐列调用 <code>offsetHeight初始化新列高数组 —— 这步不能省,否则首次追加就会错列
真正难的从来不是“怎么实现第一屏”,而是“怎么让后续每一次加载都保持列高可信、位置稳定、视觉连贯”。列高数组一旦失真,整个瀑布流就不可逆地崩坏。











