ResizeObserver 與容器查詢:元素級響應式佈局方案

前端工程(更新於 2026年6月5日)

從視窗響應到元素響應

傳統響應式基於視窗寬度@media),但元件應根據自己的容器尺寸自適應:

對比項 @media 視窗查詢 Container Query ResizeObserver
響應依據 視窗寬度 容器寬度 元素尺寸
作用域 全域 容器內 JS 回呼
CSS 驅動
JS 驅動
元件可移植性
效能 瀏覽器最佳化 瀏覽器最佳化 需注意回呼頻率

ResizeObserver API 詳解

基本用法

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    console.log(`${entry.target.className}: ${width}×${height}`);

    const borderBox = entry.borderBoxSize?.[0];
    console.log(`含邊框: ${borderBox?.inlineSize}×${borderBox?.blockSize}`);
  }
});

observer.observe(document.querySelector('.card'));

監聽選項

observer.observe(element, {
  box: 'content-box'
  // box: 'border-box'
  // box: 'device-pixel-content-box'
});

實戰:自適應卡片佈局

class ResponsiveCard {
  private observer: ResizeObserver;

  constructor(private card: HTMLElement) {
    this.observer = new ResizeObserver((entries) => {
      const { width } = entries[0].contentRect;
      this.applyLayout(width);
    });
    this.observer.observe(card);
  }

  private applyLayout(width: number) {
    if (width < 200) {
      this.card.setAttribute('data-layout', 'compact');
    } else if (width < 400) {
      this.card.setAttribute('data-layout', 'normal');
    } else {
      this.card.setAttribute('data-layout', 'expanded');
    }
  }

  destroy() {
    this.observer.disconnect();
  }
}
.card[data-layout="compact"] {
  flex-direction: column;
  font-size: 12px;
}
.card[data-layout="normal"] {
  flex-direction: column;
  font-size: 14px;
}
.card[data-layout="expanded"] {
  flex-direction: row;
  font-size: 16px;
}

實戰:響應式圖表

class ResponsiveChart {
  private observer: ResizeObserver;
  private chart: Chart | null = null;

  constructor(private container: HTMLElement) {
    this.observer = new ResizeObserver((entries) => {
      const { width, height } = entries[0].contentRect;
      this.resizeChart(width, height);
    });
    this.observer.observe(container);
  }

  private resizeChart(width: number, height: number) {
    if (this.chart) {
      this.chart.resize({ width, height });
    } else {
      this.chart = new Chart(this.container, {
        type: 'line',
        data: this.getChartData(width),
        options: {
          responsive: false,
          maintainAspectRatio: false,
        }
      });
    }
  }

  private getChartData(width: number) {
    const pointCount = width < 300 ? 5 : width < 600 ? 10 : 20;
    return { labels: generateLabels(pointCount), datasets: [...] };
  }

  destroy() {
    this.observer.disconnect();
    this.chart?.destroy();
  }
}

實戰:文字截斷檢測

class TextOverflowDetector {
  private observer: ResizeObserver;

  constructor(
    private element: HTMLElement,
    private onOverflow: (isOverflow: boolean) => void
  ) {
    this.observer = new ResizeObserver(() => {
      const isOverflow = this.checkOverflow();
      this.onOverflow(isOverflow);
    });
    this.observer.observe(element);
  }

  private checkOverflow(): boolean {
    return this.element.scrollWidth > this.element.clientWidth;
  }

  destroy() {
    this.observer.disconnect();
  }
}

const detector = new TextOverflowDetector(
  titleElement,
  (overflow) => {
    tooltipElement.style.display = overflow ? 'inline' : 'none';
  }
);

CSS Container Queries

基本語法

.card-container {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 400px) {
  .card {
    flex-direction: row;
    gap: 16px;
  }
  .card-title {
    font-size: 20px;
  }
}

@container card (min-width: 200px) and (max-width: 399px) {
  .card {
    flex-direction: column;
  }
  .card-title {
    font-size: 16px;
  }
}

@container card (max-width: 199px) {
  .card-title {
    font-size: 14px;
  }
  .card-meta {
    display: none;
  }
}

容器查詢單位

.card-container {
  container-type: inline-size;
}

.card-title {
  font-size: clamp(14px, 5cqi, 24px);
  padding: 2cqi;
}
單位 含義 等價於
cqw 容器寬度的 1% container-query-width
cqh 容器高度的 1% container-query-height
cqi 容器內聯尺寸的 1% container-query-inline
cqb 容器區塊尺寸的 1% container-query-block
cqmin min(cqi, cqb) 較小維度
cqmax max(cqi, cqb) 較大維度

容器查詢 vs 媒體查詢

同一元件在不同場景

<div class="sidebar">
  <product-card />  <!-- 側邊欄:窄容器 → 緊湊佈局 -->
</div>

<div class="main-content">
  <product-card />  <!-- 主內容:寬容器 → 展開佈局 -->
</div>
.product-card-container {
  container-type: inline-size;
}

@container (min-width: 350px) {
  .product-card { flex-direction: row; }
}

@container (max-width: 349px) {
  .product-card { flex-direction: column; }
}

媒體查詢無法做到:同一元件在不同位置自動適配。

對比總結

場景 @media @container
全域佈局(導覽/頁尾) ✅ 合適 ❌ 不需要
元件自適應 ❌ 需手動斷點 ✅ 自動適配
元件可複用性 差(依賴視窗) 好(依賴容器)
巢狀元件 ❌ 無法感知父容器 ✅ 逐層適配
效能 視窗變化觸發 容器變化觸發

ResizeObserver + Container Query 配合

class SmartResponsive {
  private observer: ResizeObserver;

  constructor(private container: HTMLElement) {
    this.observer = new ResizeObserver((entries) => {
      const { width } = entries[0].contentRect;

      if (width < 300) {
        this.showSummary();
      } else {
        this.showFullData();
      }
    });

    this.observer.observe(container);
  }

  private showSummary() {
    this.container.setAttribute('data-density', 'sparse');
  }

  private showFullData() {
    this.container.setAttribute('data-density', 'dense');
  }

  destroy() {
    this.observer.disconnect();
  }
}

效能最佳化

防抖 ResizeObserver 回呼

function debouncedResizeObserver(
  callback: (entries: ResizeObserverEntry[]) => void,
  delay = 100
): ResizeObserver {
  let timer: number;
  let latestEntries: ResizeObserverEntry[];

  return new ResizeObserver((entries) => {
    latestEntries = entries;
    clearTimeout(timer);
    timer = window.setTimeout(() => callback(latestEntries), delay);
  });
}

批次處理

const observer = new ResizeObserver((entries) => {
  requestAnimationFrame(() => {
    for (const entry of entries) {
      updateLayout(entry.target as HTMLElement, entry.contentRect);
    }
  });
});

瀏覽器相容性

API Chrome Firefox Safari Edge
ResizeObserver 64+ 69+ 13.1+ 79+
Container Queries 105+ 110+ 16+ 105+
Container Units (cqw/cqi) 105+ 110+ 16+ 105+
container-type 105+ 110+ 16+ 105+
borderBoxSize 84+ 69+ 13.1+ 84+

最佳實踐

  1. 優先 Container Query:純佈局適配用 CSS 容器查詢,無需 JS
  2. ResizeObserver 處理邏輯:需要資料/行為變化時使用 JS
  3. 及時 disconnect:元件卸載時斷開觀察,避免洩漏
  4. 避免循環:回呼中修改尺寸可能觸發新回呼,需防抖
  5. container-type 選擇:僅需寬度適配用 inline-size,比 size 效能更好

總結

元素級響應式是現代元件化設計的核心需求。CSS Container Queries 實現純 CSS 的容器適配,ResizeObserver 提供精確的 JS 尺寸感知,兩者配合實現完整的元素級響應式方案。告別「視窗斷點硬編碼」,讓元件真正做到「放哪都適配」。

推薦使用 Flexbox 產生器 設計容器內佈局,漸層產生器 製作響應式背景,陰影產生器 除錯容器內卡片樣式。

本站提供瀏覽器本地工具,免註冊即可試用 →

#ResizeObserver#响应式#容器查询#元素尺寸#布局