Material UI Lab Masonry 组件完全指南:瀑布流布局的配置、实现原理与 SSR 支持
Masonry 是 MUI Lab 中用于构建“瀑布流”布局的组件:它把宽度一致、高度不一的内容块按行顺序排布,每个新元素都会进入当前最矮的列,从而最大化利用空间。本篇基于 MUI 官方文档与 @mui/lab 源码,完整覆盖 Masonry 的基本用法、columns / spacing / sequential / default* 等全部配置项,并深入源码剖析其基于 CSS Flexbox 负边距与 order 重排的布局引擎、ResizeObserver / MutationObserver 的响应机制,以及服务端渲染(SSR)模式的实现细节。
一、Masonry 是什么:布局规则与适用场景
官方文档 masonry.md 对 Masonry 的定义是:
Masonry lays out contents of varying dimensions as blocks of the same width and different height with configurable gaps.(Masonry 把尺寸各异的组织内容排列为宽度相同、高度不同、间隙可配置的块。)
具体布局规则有三条:
- 等宽变高:Masonry 维护一组宽度一致、高度不同的内容块,子元素可以是任意 React 元素,包括
<div />和<img />; - 按行排序:内容按行(row)顺序进入布局。当某一行已被指定的列数填满后,下一个元素开始新行;
- 最短列优先:新元素被添加到当前最矮的列中,以此优化空间利用。
组件实现位于 Masonry.js,类型声明位于 Masonry.d.ts,测试位于 Masonry.test.js。它与另一个组件的分工值得注意:文档中明确提示,Masonry 是按行(row)排序子元素的;如果你希望图片按列(column)排序,应使用 ImageList 的 masonry 布局变体。
二、基本用法:最小可用示例
BasicMasonry.tsx 展示了 Masonry 的最小形态:
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Masonry from '@mui/lab/Masonry';
import { styled } from '@mui/material/styles';
const heights = [150, 30, 90, 70, 110, 150, 130, 80, 50, 90, 100, 150, 30, 50, 80];
const Item = styled(Paper)(({ theme }) => ({
backgroundColor: '#fff',
...theme.typography.body2,
padding: theme.spacing(0.5),
textAlign: 'center',
color: (theme.vars || theme).palette.text.secondary,
}));
export default function BasicMasonry() {
return (
<Box sx={{ width: 500, minHeight: 393 }}>
<Masonry columns={4} spacing={2}>
{heights.map((height, index) => (
<Item key={index} sx={{ height }}>
{index + 1}
</Item>
))}
</Masonry>
</Box>
);
}
几个要点:
Masonry是容器,可接收任意数量的子元素,children是必填属性(见 Masonry.d.ts 中children: NonNullable<React.ReactNode>的声明,空 children 会触发 propTypes 警告,测试 Masonry.test.js 中有专门用例验证);- 每个子项的宽度由 Masonry 统一控制(按
columns均分容器宽度),高度由子项内容决定; - 整个布局包裹在固定宽度 500px 的
Box中,便于演示列宽分配。
三、图片瀑布流(Image Masonry)
ImageMasonry.tsx 演示了 Masonry 在图片墙场景的应用:每个子项是一个包含 Label(带编号的 Paper)和 <img /> 的 div,三列排布,图片通过 srcSet 提供 2x 高清版本并开启 loading="lazy" 懒加载:
<Masonry columns={3} spacing={2}>
{itemData.map((item, index) => (
<div key={index}>
<Label>{index + 1}</Label>
<img
srcSet={`${item.img}?w=162&auto=format&dpr=2 2x`}
src={`${item.img}?w=162&auto=format`}
alt={item.title}
loading="lazy"
style={{
borderBottomLeftRadius: 4,
borderBottomRightRadius: 4,
display: 'block',
width: '100%',
}}
/>
</div>
))}
</Masonry>
图片场景有一个源码层面的细节:在 Masonry.js 的 handleResize 中,布局引擎会检查每个子项内的嵌套 <IMG> 节点,如果发现某个图片的 clientHeight === 0(图片尚未加载完成、没有实际渲染高度),整个重排会被标记为 skip,等待图片加载后再由 ResizeObserver 触发重新计算。也就是说,图片异步加载完成会自动触发一次瀑布流重排,无需手动干预。
四、变高项(Items with Variable Height)
MasonryWithVariableHeightItems.tsx 用可展开的 Accordion(最小高度各异)作为子项,说明 Masonry 对“动态高度”的支持:
<Masonry columns={3} spacing={2}>
{heights.map((height, index) => (
<Paper key={index}>
<StyledAccordion sx={{ minHeight: height }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography component="span">Accordion {index + 1}</Typography>
</AccordionSummary>
<AccordionDetails>Contents</AccordionDetails>
</StyledAccordion>
</Paper>
))}
</Masonry>
文档特别强调:为了满足“新元素永远进入最短列”的规则,项目之间可能在不同列之间移动。从源码结构看,这正是由 Masonry.test.js 中 “should re-compute the height of masonry when dimensions of any child change” 用例所验证的行为:当某个子项的高度从 20px 变为 10px 后,容器总高度会随之重算——因为任何子项尺寸变化都会通过 ResizeObserver 触发一次完整重排(见后文第五节原理分析)。
五、配置列数:columns
固定列数
FixedColumns.tsx 演示 columns={4}:容器宽度被等分为 4 份,每份再减去 spacing 得到子项实际宽度。
响应式列数
columns 接受 MUI 标准的响应式值。ResponsiveColumns.tsx 中:
<Masonry columns={{ xs: 3, sm: 4 }} spacing={2}>
即小屏 3 列、sm 断点及以上 4 列。从类型声明看(Masonry.d.ts),columns 的类型是 ResponsiveStyleValue<number | string>,因此除了对象形式 { xs, sm, md, lg, xl },还支持数组形式 [3, 'sm', 4]。
源码印证:在 Masonry.js 的 getStyle 函数中,columns 先经 unstable_resolveBreakpointValues 解析为各断点取值,再由 handleBreakpoints 为每个断点生成媒体查询样式,核心公式是子项宽度:
width = (100 / columnValue).toFixed(2) + '%';
// 子项实际宽度:calc(<width>% - <spacing>)
测试文件 Masonry.test.js 的 “should generate correct responsive styles regardless of breakpoints order” 用例还验证了一个实用细节:响应式对象的键序不影响结果,传入 { sm: 5, md: 7, xs: 3 } 与按断点升序排列生成完全相同的媒体查询样式。
六、配置间距:spacing
固定间距
FixedSpacing.tsx 使用 spacing={3}。这里有一个关键约定(文档原文):
传入
spacing的值会被乘以主题(theme)的spacing字段。
即 spacing={3} 在默认主题下(spacing: (factor) => 8 * factor px)等效于 24px 的间隙。这一行为在源码中由 createUnarySpacing(theme) 生成的 transformer 实现:对数字值(以及可解析为数字的字符串)执行 getValue(transformer, number),其余字符串(如 '4px')则原样使用。
响应式间距
<Masonry columns={3} spacing={{ xs: 1, sm: 2, md: 3 }}>
同样受 ResponsiveStyleValue 约束,可传对象或数组。
间距的实现机制值得单独说明。Masonry 并不使用 CSS gap,而是采用“容器负边距 + 子项四向半边距”的经典做法,getStyle 中生成:
{
margin: `calc(0px - (${spacing} / 2))`, // 容器:抵消外溢的边距
'& > *': { margin: `calc(${spacing} / 2)` } // 每个子项:四周 spacing/2
}
这样无论水平还是垂直方向,任意两个相邻子项之间的净距离都恰好是 spacing。同时容器高度会在布局引擎测得 maxColumnHeight(最矮列之外最高的那一列总高)后设置为 maxColumnHeight + spacing,测试用例 “should apply correct default styles” 精确断言了这三个值(容器负边距 -spacing/2、子项边距 spacing/2、子项宽度 width/columns - spacing)。
七、顺序模式:sequential
Sequential.tsx 演示:
<Masonry
columns={4}
spacing={2}
defaultHeight={450}
defaultColumns={4}
defaultSpacing={1}
sequential
>
开启 sequential 后,元素按从左到右的顺序依次填入各列(第 1、2、3、4 个元素分别在第 1~4 列,第 5 个元素回到第 1 列),而不是进入当前最短列。适合需要保持阅读顺序严格的场景。
源码印证:Masonry.js 的 handleResize 中两种策略是并列分支:
if (sequential) {
columnHeights[nextOrder - 1] += childHeight;
child.style.order = nextOrder;
nextOrder += 1;
if (nextOrder > currentNumberOfColumns) {
nextOrder = 1;
}
} else {
const currentMinColumnIndex = columnHeights.indexOf(Math.min(...columnHeights));
columnHeights[currentMinColumnIndex] += childHeight;
child.style.order = currentMinColumnIndex + 1;
}
测试用例 “should place children in sequential order”(浏览器环境)验证了 2 列 3 个子项时计算样式 order 依次为 1, 2, 1。
八、服务端渲染(SSR):defaultHeight / defaultColumns / defaultSpacing
Masonry 的动态布局依赖浏览器 API(读取子项实际高度),在服务端执行时这些值不可用。为此组件提供三个仅用于 SSR 的 prop(见 SSRMasonry.tsx):
<Masonry
columns={4}
spacing={2}
defaultHeight={450}
defaultColumns={4}
defaultSpacing={1}
>
文档中的注意事项原文:
defaultHeight应当足够大以容纳所有行。另外需要注意,在服务端渲染的情况下,元素不会被添加到最短列。
进入 SSR 分支的条件(Masonry.js L189-L196):
const isSSR =
!maxColumnHeight &&
defaultHeight &&
defaultColumns !== undefined &&
defaultSpacing !== undefined;
即首次服务端渲染(尚未测得 maxColumnHeight)且三个 default* prop 全部提供时启用。SSR 分支生成的纯 CSS 样式为(L52-L77):
- 容器高度固定为
defaultHeight(px); - 容器与子项使用
defaultSpacing换算出的固定负/正半边距; - 每个子项宽度为
calc(100/defaultColumns% - defaultSpacing); - 用
nth-of-type(defaultColumns n + i)选择器为子项依次赋予order: 1..defaultColumns,实现“按行左到右”的确定性列分配——这正是文档所说“SSR 下不进入最短列”的原因:纯 CSS 无法知道每项高度,只能做静态轮转。
浏览器端水合(hydrate)完成后,handleResize 会立即运行,用真实测量值接管布局,SSR 静态样式随之被动态 order 与容器高度覆盖。测试用例 “should support server-side rendering” 对 isSSR: true 时的完整样式对象(含 nth-of-type(4n+1) → order: 1 等规则)做了精确断言。
九、布局引擎原理:从源码看 Masonry 如何工作
结合 Masonry.js 全文,Masonry 的运行时机制可以归纳为四部分:
1. 静态骨架:Flexbox 容器
getStyle 生成的基础容器样式是:
{
width: '100%',
display: 'flex',
flexFlow: 'column wrap', // 纵向主轴 + 允许换行
alignContent: 'flex-start',
boxSizing: 'border-box',
}
注意主轴方向是 column:每个“行”是 Flex 容器的一条主轴,换行产生下一行。子项通过 order 属性决定落入哪一列,从而把二维瀑布流映射为“纵向 Flex + order 重排”的一维问题。
2. 防合并哨兵:line breaks
源码 L353-L358 揭示了一个精妙设计——组件末尾渲染一组透明哨兵元素:
// A line break is added to the end of each column to prevent columns from merging.
const lineBreaks = new Array(numberOfLineBreaks).fill('').map((_, index) => (
<span key={index} data-class="line-break"
style={{ flexBasis: '100%', width: 0, margin: 0, padding: 0, order: index + 1 }} />
));
每个哨兵 flexBasis: '100%',按 order: 1..N 占据各列末尾。它的作用是强制每列独占一条主轴轨道,防止相邻列在无内容的情况下发生视觉合并;哨兵自身宽高均为 0,不占据可见空间。numberOfLineBreaks 初始为 0(或 SSR 时为 defaultColumns - 1),在首次重排后更新为“实际列数 - 1”。
3. 动态重排:ResizeObserver + MutationObserver
useEnhancedEffect(L298-L349)建立了完整的响应管线:
- 对容器内每个子项
resizeObserver.observe(childNode),子项尺寸变化即触发重排; - 用
MutationObserver监听childList,动态新增的子节点自动纳入观察、移除的自动解绑,并在每次变更回调中直接调用handleResize()——因此列表项的增删不需要任何手动刷新; - 重排入口做了 16ms(约 60fps)的防抖;
- 若运行环境缺少
ResizeObserver或MutationObserver(如纯 Node 环境),直接退出不观测——这解释了为什么 SSR 场景必须显式提供default*prop。
4. 高度测量与列分配:handleResize
handleResize(L211-L296)的核心步骤:
- 取容器
clientWidth与第一个可见子项的宽度及左右margin,反推出实际列数:Math.round(parentWidth / (firstChildWidth + marginLeft + marginRight))——因此响应式列数变化(断点切换)无需额外监听,宽度一变自然重算; - 遍历子项,读取计算样式得到每项高度(含上下 margin);高度为 0 或内含未加载图片(
IMG.clientHeight === 0)的子项触发skip,本次重排放弃,等待下一次观察事件; - 按
sequential或最短列策略为每个子项写入child.style.order; - 通过
ReactDOM.flushSync同步更新两个 state:maxColumnHeight(驱动容器高度)与numberOfLineBreaks(驱动哨兵数量)。flushSync保证高度与 order 在同一次同步提交中生效,避免布局抖动。
十、Props 速查表
综合 Masonry.d.ts 与实现代码中的默认值(L174-L185):
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
children |
NonNullable<ReactNode> |
必填 | 内容,可包含任意元素 |
columns |
ResponsiveStyleValue<number | string> |
4 |
列数,支持对象/数组响应式值 |
spacing |
ResponsiveStyleValue<number | string> |
1 |
子项间距,为主题 spacing 的倍数(数字时) |
sequential |
boolean |
false |
true 时按左到右顺序填入而非最短列 |
defaultColumns |
number |
— | 仅 SSR:静态列数 |
defaultHeight |
number(px) |
— | 仅 SSR:容器固定高度,应足够容纳所有行 |
defaultSpacing |
number |
— | 仅 SSR:spacing 同义,主题倍数 |
component |
ElementType |
'div' |
根节点使用的 HTML 元素或组件 |
classes |
Partial<MasonryClasses> |
— | 类名覆盖,目前仅 root 插槽(见 masonryClasses.ts) |
sx |
SxProps<Theme> |
— | 追加样式 |
组件注册名为 MuiMasonry(styled 声明 L163-L166),因此也支持通过主题的 components: { MuiMasonry: { ... } } 做全局样式定制;useThemeProps 的接入(L169-L172)保证了这一能力。
十一、实践建议与边界
基于文档与源码行为,总结如下适用前提与限制:
- 布局计算发生在浏览器端。没有
ResizeObserver/MutationObserver的环境不会触发任何列分配,SSR 场景务必成对提供defaultColumns/defaultHeight/defaultSpacing; defaultHeight偏大会留白、偏小会裁切:文档建议“足够大以渲染所有行”,且 SSR 阶段列分配是静态轮转(nth-of-type规则,测试中有精确断言),水合后才会切换为最短列策略,首屏可能出现短暂的列位调整;- 未加载的图片不参与本轮重排:Masonry 会跳过
clientHeight === 0的图片子项等待其加载,图片墙场景建议配合loading="lazy"并预留稳定尺寸,减少重排次数; spacing是倍数而非像素:传入数字时经主题spacing函数换算,若需像素级控制可传入可解析字符串或'0px'这类字面值;- 与 ImageList 的选型:按行排序、元素需动态重排选
Masonry;按列排序的图片墙用ImageList的 masonry 变体(官方文档在 masonry.md 中给出了该指引)。
参考文件
- 官方文档:docs/data/material/components/masonry/masonry.md
- 组件实现:packages/mui-lab/src/Masonry/Masonry.js
- 类型声明:packages/mui-lab/src/Masonry/Masonry.d.ts
- 类名工具:packages/mui-lab/src/Masonry/masonryClasses.ts
- 测试用例:packages/mui-lab/src/Masonry/Masonry.test.js
- 示例代码:BasicMasonry.tsx、ImageMasonry.tsx、MasonryWithVariableHeightItems.tsx、FixedColumns.tsx、ResponsiveColumns.tsx、FixedSpacing.tsx、ResponsiveSpacing.tsx、Sequential.tsx、SSRMasonry.tsx
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00