2024年3月,我在重构公司后台管理系统时遇到一个典型问题:页面左侧是300px宽的侧边栏,右侧是自适应内容区。内容区里有一个卡片组件,需要根据容器宽度在单列/双列/三列之间切换。
用媒体查询写死断点?不行。因为右侧内容区宽度会随着侧边栏折叠(折叠后变600px)和窗口缩放动态变化。我试过ResizeObserver + JS计算,结果页面卡顿、代码混乱,最后被测试提了3个bug。
直到我用了CSS Container Queries(容器查询),问题才彻底解决。这篇文章就是我的实战记录。
媒体查询基于视口宽度,无法感知父容器尺寸。当组件被放在不同宽度的容器中时,必须为每个场景写一套样式。
/* 媒体查询:只能根据视口宽度 */
@media (max-width: 768px) {
.card { width: 100%; }
}
@media (min-width: 769px) and (max-width: 1200px) {
.card { width: 50%; }
}
/* 问题:如果父容器只有400px,但视口是1200px,卡片还是50%宽度,布局崩了 */
我最初用ResizeObserver监听容器尺寸变化,然后通过JS切换class。代码量翻倍,而且频繁触发重排。
// ResizeObserver方案:代码复杂,性能差
const container = document.querySelector('.card-container');
const observer = new ResizeObserver(entries => {
for (let entry of entries) {
const width = entry.contentRect.width;
if (width < 400) {
container.classList.add('single');
container.classList.remove('double', 'triple');
} else if (width < 700) {
container.classList.add('double');
container.classList.remove('single', 'triple');
} else {
container.classList.add('triple');
container.classList.remove('single', 'double');
}
}
});
observer.observe(container);
// 问题:每次resize都触发DOM操作,100ms内触发20次,页面掉帧
| 方案 | 代码量 | 性能(100次resize) | 可维护性 | 浏览器兼容 |
|---|---|---|---|---|
| 媒体查询 | 30行CSS | 0.2ms(原生) | 差(依赖视口) | 所有浏览器 |
| ResizeObserver | 50行JS + 20行CSS | 12ms(含重排) | 差(JS逻辑耦合) | Chrome 64+ |
| Container Queries | 25行CSS | 0.3ms(原生) | 好(纯CSS) | Chrome 105+, Safari 16+, Firefox 110+ |
数据来源:在Chrome 120下用Performance面板测试,模拟100次resize事件(每次间隔10ms)。Container Queries的样式计算耗时与媒体查询几乎一致,而ResizeObserver方案因为触发了JS重排,耗时是前者的40倍。
<div class="page-layout">
<aside class="sidebar">侧边栏(可折叠)</aside>
<main class="content">
<div class="card-container">
<div class="card">卡片1</div>
<div class="card">卡片2</div>
<div class="card">卡片3</div>
<div class="card">卡片4</div>
</div>
</main>
</div>
/* 定义容器上下文 */
.card-container {
container-type: inline-size;
container-name: card-grid;
display: flex;
flex-wrap: wrap;
gap: 16px;
padding: 16px;
}
/* 卡片基础样式 */
.card {
flex: 1 1 100%;
min-width: 200px;
background: #f5f5f5;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 容器查询:根据容器宽度切换布局 */
@container card-grid (max-width: 400px) {
.card {
flex: 1 1 100%;
}
}
@container card-grid (min-width: 401px) and (max-width: 700px) {
.card {
flex: 1 1 calc(50% - 8px);
}
}
@container card-grid (min-width: 701px) {
.card {
flex: 1 1 calc(33.33% - 11px);
}
}
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; padding: 20px; }
.demo-container {
container-type: inline-size;
container-name: demo;
border: 2px dashed #ccc;
padding: 10px;
resize: horizontal;
overflow: auto;
min-width: 200px;
max-width: 1000px;
}
.card-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.card-item {
flex: 1 1 100%;
min-width: 150px;
background: #e3f2fd;
border: 1px solid #90caf9;
border-radius: 6px;
padding: 16px;
text-align: center;
}
@container demo (min-width: 400px) {
.card-item { flex: 1 1 calc(50% - 6px); }
}
@container demo (min-width: 700px) {
.card-item { flex: 1 1 calc(33.33% - 8px); }
}
.info { margin-bottom: 16px; color: #666; }
</style>
</head>
<body>
<h2>拖拽右下角调整容器宽度</h2>
<p class="info">容器宽度 < 400px:单列 | 400-700px:双列 | > 700px:三列</p>
<div class="demo-container">
<div class="card-grid">
<div class="card-item">卡片 A</div>
<div class="card-item">卡片 B</div>
<div class="card-item">卡片 C</div>
<div class="card-item">卡片 D</div>
<div class="card-item">卡片 E</div>
</div>
</div>
</body>
</html>
另一个实战场景:表格列数需要根据容器宽度动态隐藏/显示。用Container Queries配合CSS Grid实现。
.table-container {
container-type: inline-size;
container-name: table-grid;
}
.data-table {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
/* 容器宽度小于500px时,隐藏“地址”列 */
@container table-grid (max-width: 500px) {
.col-address {
display: none;
}
}
/* 容器宽度小于300px时,只显示“姓名”和“电话” */
@container table-grid (max-width: 300px) {
.col-email,
.col-address {
display: none;
}
}
我在生产环境(Chrome 120,MacBook Pro M1)做了压测:
结论:Container Queries在大量组件场景下,性能优势明显,且代码更简洁。
Container Queries必须显式设置 container-type: inline-size 或 size,否则查询不生效。我花了2小时排查,最后发现父元素没加这个属性。
/* 错误:没有container-type,查询无效 */
.parent { container-name: my-container; }
@container my-container (max-width: 500px) { ... }
/* 正确 */
.parent { container-type: inline-size; container-name: my-container; }
当容器同时使用flex布局和container-type时,flex子项的尺寸计算可能异常。解决方案:把container-type放在外层wrapper上。
<!-- 错误:flex容器直接作为查询容器 -->
<div style="display: flex; container-type: inline-size;">
<div class="child">...</div>
</div>
<!-- 正确:加一层wrapper -->
<div style="display: flex;">
<div style="container-type: inline-size;">
<div class="child">...</div>
</div>
</div>
Safari 16.4之前不支持container-name,导致查询不生效。我的解决方案:使用@supports做降级。
/* 降级方案:不支持Container Queries时用媒体查询 */
@supports (container-type: inline-size) {
.card-container { container-type: inline-size; }
@container (max-width: 400px) { ... }
}
@supports not (container-type: inline-size) {
@media (max-width: 768px) { ... }
}
CSS Container Queries是响应式设计的未来。它解决了媒体查询无法感知容器尺寸的根本问题,性能优于JS方案。建议在Chrome 105+、Safari 16.4+、Firefox 110+的项目中直接使用,低版本浏览器用@supports做降级。
我的建议:从卡片、表格这类独立组件开始用,逐步推广到整个页面布局。别一次性重构所有代码,容易踩坑。
专注技术分享与实战