主題: Web platform
ResizeObserver 入門:元素尺寸、盒模型與更新邊界
元素變窄不一定是視窗變窄。用可跑的 JavaScript 範例理解 ResizeObserver,算清楚 content box 與 border box,分辨 inline/block 軸,並處理隱藏、清理與尺寸回饋循環。
動態迷因(展開/收合)
側欄一展開,圖表就被擠窄了。瀏覽器視窗沒動,圖表卻需要重畫。
只監聽 window.resize 會漏掉這個需求。ResizeObserver 讓程式觀察元素的版面尺寸,尺寸變化時再收到通知。拖曳面板、Grid 重新分配空間、內容換行,都可能改變元素尺寸,不必先改變 viewport。MDN:ResizeObserver
我會先問這個元件需要哪個尺寸,拿來做什麼。圖表需要容器的可繪製空間;純粹切換卡片排列,CSS container query 可能就夠了。排版與繪圖的責任仍在使用尺寸的程式。
先看一個能跑的面板
把 HTML 放進空白頁面,再將 JavaScript 放在它後面。桌面上可以拖曳面板右下角;不方便拖曳的裝置,也可以在開發者工具改變它的寬度。
<div
id="resize-panel"
style="box-sizing: border-box; width: 300px; min-width: 160px;
max-width: 100%; padding: 12px; border: 2px solid;
resize: horizontal; overflow: auto;"
>
<p>Resize this panel. Its text can wrap.</p>
</div>
<p id="size-output" role="status">Waiting for a size notification.</p>
const panel = document.querySelector("#resize-panel");
const output = document.querySelector("#size-output");
let resizeObserver;
if ("ResizeObserver" in window) {
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target !== panel) continue;
const size = entry.borderBoxSize?.[0];
if (!size) {
output.textContent = "Border box data is unavailable.";
continue;
}
output.textContent =
`Border box: inline ${size.inlineSize.toFixed(1)}px, ` +
`block ${size.blockSize.toFixed(1)}px.`;
}
});
resizeObserver.observe(panel, { box: "border-box" });
} else {
output.textContent = "Live size reporting is unavailable.";
}
function stopWatchingPanel() {
resizeObserver?.disconnect();
}
這段假設 HTML 已存在,兩次查詢都取得元素。如果程式在元件建立前執行,查詢可能得到 null;要等元素存在再觀察,不能把 selector 字串交給 observe() 等它自行搜尋。
尺寸文字放在面板外面,減少顯示結果反過來改變受測面板的機會。它只呈現資料,沒有加入圖表套件或輪詢。應用程式卸載這個示範時,呼叫 stopWatchingPanel() 清理。
entries 是本次通知的目標清單,一個 observer 可以觀察多個元素。某次沒有某個目標,不代表它已被刪除;用 entry.target 對應自己的元件。MDN:ResizeObserverEntry
width 指哪一圈,決定要加還是減
一般元素的 CSS 盒模型可以從裡往外看:
內容 content → 內距 padding → 邊框 border → 外距 margin
content box 是內容區。border box 包含內容、padding 與 border,不包含 margin。ResizeObserver 預設以 content box 的變化作為觀察條件;需要外側邊框盒時,指定 { box: "border-box" }。MDN:observe() 的 box
CSS 的 box-sizing 也有這兩個名稱,但它決定 CSS width/height 怎麼算。observer 的 box 決定觀察哪個盒子,呼叫 observe(panel, { box: "border-box" }) 不會改變元素的 CSS box-sizing。
只看「width 是 180px」還不夠。以下假設普通盒子沒有捲軸,也沒有其他尺寸限制:
| CSS 設定 | 已知 width 指哪裡? | 要算的尺寸 |
|---|---|---|
border-box; width: 180px,左右 padding 各 8px、border 各 1px |
整個邊框盒是 180px | 內容寬是 180 − 16 − 2 = 162px |
content-box; width: 150px,左右 padding 各 12px、border 各 3px |
內容區是 150px | 邊框盒寬是 150 + 24 + 6 = 180px |
先指出已知數字是哪一層,再決定加或減。這比背「width 要加 padding」可靠,因為 border-box 已經把它算進去了。MDN:box-sizing
這也影響通知。固定 content box 後增加 padding,border box 可以變大,content box 卻不變。若產品關心外側尺寸,就不能期待預設觀察替你通知所有 padding 變化。
讀資料前,先選盒子,再選軸
| 欄位 | 哪個盒子? | 哪種軸? |
|---|---|---|
entry.contentBoxSize[0] |
內容盒 | inlineSize/blockSize 邏輯軸 |
entry.borderBoxSize[0] |
邊框盒 | inlineSize/blockSize 邏輯軸 |
entry.contentRect |
普通 HTML 元素的內容區 | width/height 實體軸 |
這裡以一般單一片段的 HTML 元素示範。現行尺寸欄位是陣列,所以先取 [0],再讀尺寸物件。這個陣列與 callback 的 entries 不同:前者描述一個目標的尺寸片段,後者是這次通知的目標清單。本文不展開多欄分片的處理。MDN:contentBoxSize
inline 是文字在一行內前進的軸,block 是一行接著一行排列的軸。它們會隨 writing-mode 改變:
| 書寫模式 | inlineSize 對應 | blockSize 對應 |
|---|---|---|
horizontal-tb |
實體寬度 | 實體高度 |
vertical-rl/vertical-lr |
實體高度 | 實體寬度 |
direction: rtl 只把水平文字的前進方向改成由右向左,不會讓水平書寫變直書。因此,水平模式中內容寬 240px、高 70px,改成 RTL 後仍是 inline 240、block 70,尺寸也不會變成負值。MDN:不同文字方向
如果需求是「border box 的 block 軸長度」,就讀:
const blockLength = entry.borderBoxSize[0].blockSize;
contentRect.height 同時換了盒子與軸。它是內容區的實體高度;沒有 padding、border 且採水平模式時,數字可能剛好相同,但不能當作通用替代。MDN:borderBoxSize、contentRect
display: none 與 transform,版面意義不同
transform: scale(0.5) 縮小呈現結果,不會把原本 200px 的內容盒改成 100px。CSSWG 規範明確指出,CSS transform 不觸發 resize observation。不要把 observer 當作視覺縮放或位置追蹤器。
原本已渲染、尺寸非零的元素改成 display: none,則不再產生原來的版面盒子,會形成可觀察的尺寸變化。這不代表 observer 自動 disconnect()。元件隱藏與停止觀察是兩個操作。CSSWG:通知條件
visibility: hidden 與 opacity: 0 一般會保留原本版面空間。不要把「看不見」一律解讀為尺寸歸零;visibility 的特殊 collapse 情況另有規則。MDN:visibility、opacity
圖表如果先放在隱藏分頁裡,量到零尺寸時應跳過依尺寸計算的繪圖,等容器有可用尺寸再畫。這是應用程式的策略,API 不會替你決定零寬度的圖表該長怎樣。
收到通知後,改尺寸的程式能停下來嗎?
ResizeObserver 的通知會參與瀏覽器的渲染流程。callback 中改版面,可能又造成新的尺寸變化。如果每次都將受觀察元素加寬:
entry.target.style.width = `${entry.contentRect.width + 10}px`;
在 content-box 且沒有其他寬度限制的假設下,數字會一路增長。瀏覽器會限制單次渲染中的循環,可能回報 ResizeObserver loop completed with undelivered notifications.,但不會修好會持續更新的規則。MDN:Observation errors
動態迷因(展開/收合)
可以加上停止條件,例如「內容寬不足 320px 才補到 320px」:
if (entry.contentRect.width < 320) {
entry.target.style.width = "320px";
}
這裡仍假設 content-box,且沒有其他規則阻止寬度達到 320px。達到目標後不再改寫,才有穩定點;它不保證整個過程只通知一次。如果另一段程式又把寬度縮回去,仍要一起檢查。
圖表常見的安排是觀察尺寸由外部版面決定的容器,再更新內部 canvas。但若 canvas 的大小反過來決定容器大小,換成觀察父元素也未必安全。要追的是尺寸依賴,包含跨元素、跨 observer 的更新。
requestAnimationFrame() 可以幫忙合併昂貴繪圖、保留最新尺寸,卻不證明更新會收斂。把「每次加 10px」搬到下一個影格,仍可能持續變大。callback 也在主執行緒執行,昂貴工作不會因 API 非同步就免費。web.dev:使用與效能
初始通知、內容更新與清理
一般已渲染、非零尺寸的元素開始被觀察時,會收到初始通知。不過它不是「所有圖片與字型都載入完成」的訊號。要匯出最終圖表,應另外定義資源與資料的就緒條件。CSSWG:開始觀察
固定寬高的聊天面板增加訊息,可能只讓捲動內容變長,盒子尺寸仍不變。要處理新訊息,直接接在應用程式新增訊息的流程;需要觀察 DOM 變動時,再考慮 MutationObserver。ResizeObserver 不會替每一筆內容更新發事件。
清理時也要符合擁有範圍:
unobserve(target)停止一個目標,適合共用 observer 中某個元件卸載。disconnect()停止這個 observer 的所有目標。物件仍可再用observe()開始新的觀察。
這些方法不刪除 DOM,也不取消 callback 已排入的繪圖或網路請求。若自己安排 rAF 或 fetch,需要另外清理那份工作。MDN:unobserve()、disconnect()
哪些需求可以先交給 CSS?
卡片要依所在面板寬度改成一欄或兩欄,若只是樣式變化,可以先用 CSS container query。為合適祖先設定 container-type: inline-size,再用 @container 設定子元素樣式,不用另寫 observer 來維護 class。MDN:Container queries
需要把尺寸傳給 JavaScript 圖表、繪圖或既有排版函式時,ResizeObserver 才接上程式的需求。尺寸、內容變動、交會狀態各有不同工具,不必用一個 observer 包辦。
相容性要檢查到實際使用的欄位與選項。第一個範例沒有 border box 資料時,會顯示功能不可用,保留普通面板。若產品要求相同尺寸功能,替代測量也必須保留盒子與軸的語意;不能默默把 contentRect.width 改名為 border width。舊版實作的資料形狀與細項支援,仍需在目標瀏覽器驗證。
我學到什麼
- 我會先標出 CSS width 指哪一圈,再計算內容與邊框尺寸。觀察 border box,不會替我改 CSS box-sizing。
- 我會分別選盒子與軸。RTL 不等於直書,blockSize 也不永遠等於 height。
- 我會區分版面盒子與畫面效果。隱藏分頁與 transform 縮放,需要不同的尺寸處理。
- 我會檢查尺寸更新能否停下來,再考慮合併繪圖;停止 observer 與取消其他工作分開清理。
練習時先拖曳上面的面板,再改 padding、writing-mode、display 與 transform。每次只改一個條件,說得出通知與數字為什麼不同,再接到真正的圖表。
外部參考資料與延伸學習
- MDN ResizeObserver:API 入口、相容性與觀察循環。
- MDN CSS 盒模型:用圖解分清 content、padding、border 與 margin。
- web.dev ResizeObserver:元素尺寸通知、繪圖需求與效能取捨。
- Web Dev Simplified 原作者入門文章:2022 年程式入門,舊的相容性與 CSS 能力描述以現行文件為準。
- Learn Resize Observer In 5 Minutes:同作者的公開入門影片,適合搭配空白頁面操作。