Theo dõi kích thước phần tử (element size) là nhu cầu phổ biến trong giao diện web hiện đại: đồ thị tự co giãn, lưới masonry, thanh bên thu gọn, khung nội dung động, trình soạn thảo WYSIWYG, và hàng loạt trường hợp responsive “theo container”. Bài viết này tổng hợp toàn diện các cách phát hiện thay đổi kích thước phần tử DOM: cơ chế “cảm biến cuộn” ResizeSensor thế hệ cũ và giải pháp hiện đại ResizeObserver, kèm hướng dẫn tối ưu hiệu năng, xử lý lỗi, và tích hợp framework.
Khi nào cần theo dõi kích thước phần tử
- Thành phần dữ liệu trực quan: biểu đồ, bản đồ, canvas cần vẽ lại khi container đổi kích thước.
- Layout phụ thuộc container: lưới masonry, card responsive, khối văn bản có cột động.
- UI phức tạp: sidebar co giãn, splitter, panel kéo thả, docking layout.
- Khả năng mở rộng: thành phần nhúng (embed), iframe, widget bên thứ ba.
Hai hướng tiếp cận chính
- ResizeSensor (di sản, đa trình duyệt cũ)
- Ý tưởng: chèn vào phần tử cần theo dõi hai hộp “expand” và “shrink” có scrollbar ảo. Khi element đổi kích thước, các hộp con thay đổi scroll, kích hoạt sự kiện scroll → phát hiện thay đổi.
- Ưu điểm: tương thích các trình duyệt cũ, hoạt động kể cả khi window không resize.
- Nhược điểm: thêm DOM ẩn, nhạy cảm với CSS, có chi phí layout/scroll, cần dọn dẹp kỹ để tránh rò rỉ bộ nhớ.
- ResizeObserver (hiện đại, chuẩn web)
- Ý tưởng: API gốc của trình duyệt lắng nghe thay đổi kích thước của phần tử.
- Ưu điểm: gọn nhẹ, chính xác, hiệu năng tốt, không cần hack DOM.
- Nhược điểm: trình duyệt rất cũ không hỗ trợ; cần fallback nếu phải hỗ trợ các bản cũ.
Mổ xẻ cơ chế ResizeSensor
Cơ chế cảm biến cuộn thường:
- Chèn một wrapper ẩn vào phần tử cần theo dõi, đặt position phù hợp (thường relative nếu chưa có).
- Tạo hai vùng cuộn: “expand” và “shrink” (một phình to, một co lại).
- Điều chỉnh kích thước con “expand” lớn hơn “expand” vài px để tạo thay đổi scroll.
- Lắng nghe sự kiện scroll trên cả hai vùng; so sánh offsetWidth/offsetHeight hiện tại với lần trước để nhận biết thay đổi; sau đó reset trạng thái.
- Gọi callback khi phát hiện thay đổi kích thước.
Điểm cần lưu ý:
- Nếu element ban đầu là display: none, cảm biến không thể đo kích thước cho đến khi hiển thị trở lại.
- Đặt aria-hidden, pointer-events: none, visibility phù hợp cho các phần tử ẩn để không ảnh hưởng khả năng truy cập.
- Phải gỡ bỏ cảm biến khi unmount/destroy để tránh rò rỉ sự kiện.
Dùng ResizeObserver: cách làm khuyến nghị
- Ví dụ cơ bản:
const el = document.querySelector('#container');
const ro = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
// Cập nhật layout, re-render, scale canvas, v.v.
console.log(‘Kích thước mới:’, width, height);
}
});
ro.observe(el);
// Khi không cần nữa:
ro.disconnect();
- Tối ưu hiệu năng:
- Gộp cập nhật bằng requestAnimationFrame hoặc debounce/throttle.
- Tránh đo đạc layout đắt đỏ trong vòng lặp observer.
- Hủy observer khi thành phần unmount hoặc khi không cần theo dõi nữa.
Ví dụ throttle bằng requestAnimationFrame:
```js
let rafId = null;
const ro = new ResizeObserver(([entry]) => {
if (rafId) return;
rafId = requestAnimationFrame(() => {
rafId = null;
const { width, height } = entry.contentRect;
updateUI(width, height);
});
});
Fallback chiến lược: ResizeObserver + ResizeSensor
Nếu bạn cần hỗ trợ trình duyệt cũ:
- Dùng ResizeObserver khi có sẵn.
- Fallback sang cơ chế cảm biến cuộn khi không có API chuẩn.
Pseudocode:
function observeElementSize(el, onChange) {
if ('ResizeObserver' in window) {
const ro = new ResizeObserver(([entry]) => onChange(entry.contentRect));
ro.observe(el);
return () => ro.disconnect();
}
// Fallback cảm biến cuộn
const sensor = createScrollSensor(el, rect => onChange(rect));
return () => sensor.destroy();
}
Tích hợp trong framework
- React hook:
import { useEffect, useRef, useState } from 'react';
export function useElementSize() {
const ref = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const node = ref.current;
if (!node) return;
let rafId = null;
const ro = new ResizeObserver(([entry]) => {
if (rafId) return;
rafId = requestAnimationFrame(() => {
rafId = null;
const { width, height } = entry.contentRect;
setSize({ width: Math.round(width), height: Math.round(height) });
});
});
ro.observe(node);
return () => {
if (rafId) cancelAnimationFrame(rafId);
ro.disconnect();
};
}, []);
return [ref, size];
}
- Vue directive (ý tưởng):
- Tạo directive v-resize để attach ResizeObserver vào el trong mounted.
- Gọi binding.value(size) khi thay đổi.
- Ngắt kết nối trong unmounted.
- Svelte action (ý tưởng):
- export function resize(node, callback) { const ro = new ResizeObserver(...); ro.observe(node); return { destroy: () => ro.disconnect() }; }
## Bài toán thực tế và mẫu sử dụng
- Biểu đồ/canvas responsive: vẽ lại canvas theo kích thước container, thay vì nghe window resize.
- Layout masonry: tính lại số cột/chiều cao sau mỗi lần container đổi kích thước.
- Sidebar co giãn: lưu trạng thái và cập nhật breakpoint theo width thực tế của sidebar.
- Component nhúng: quan sát wrapper để tự động điều chỉnh kích thước nội dung nhúng.
## Lỗi thường gặp và cách khắc phục
- display: none: ResizeObserver không báo khi phần tử vô hình; chỉ kích hoạt khi phần tử trở lại thuộc flow. Giải pháp: quan sát phần tử cha hiển thị hoặc lắng nghe state thay đổi hiển thị.
- Vòng lặp layout: Cập nhật style ngay trong callback có thể kích thích vòng lặp đo–ghi. Hãy batch cập nhật bằng rAF hoặc đặt cờ để tránh cập nhật liên tiếp.
- Shadow DOM: Quan sát phần tử bên trong ShadowRoot bình thường, nhưng lưu ý phạm vi style và lifecycle.
- Scrollbar thay đổi: Thêm/bớt scrollbar có thể ảnh hưởng contentRect. Kiểm tra boxSizing, overflow.
- Transform: scale hoặc transform không thay đổi contentRect theo pixel logic như mong đợi; cần tính đến transform khi đo đạc.
## Tối ưu hiệu năng và độ ổn định
- Debounce thay đổi: chỉ cập nhật khi chênh lệch vượt ngưỡng tối thiểu (ví dụ ≥1px).
- Hạn chế đo đạc nhiều phần tử đồng thời; hợp nhất đo đạc vào một pass rAF.
- Tránh thao tác DOM không cần thiết trong callback.
- Dọn dẹp đầy đủ: disconnect/destroy observer hoặc sensor khi component bị tháo.
- Với cảm biến cuộn: đặt aria-hidden="true", pointer-events: none, visibility: hidden để không ảnh hưởng UX và truy cập.
## So sánh nhanh: ResizeObserver vs ResizeSensor
- Chính xác và đơn giản: ResizeObserver vượt trội.
- Tương thích cũ: ResizeSensor có lợi thế trên trình duyệt rất cũ.
- Bảo trì và hiệu năng: ResizeObserver ít phức tạp, ít phụ thuộc CSS/DOM ẩn, thường nhanh và ổn định hơn.
## Liên hệ với container queries (CSS)
- Nếu mục tiêu là thay đổi style theo kích thước container, hãy cân nhắc container queries (CSS) để tách logic style khỏi JS.
- Khi cần chạy logic JS (vẽ lại chart, tính toán phức tạp), ResizeObserver vẫn là lựa chọn số một.
## Quy trình khuyến nghị để triển khai thực tế
1) Ưu tiên ResizeObserver cho hầu hết dự án.
2) Thêm fallback cảm biến cuộn nếu có yêu cầu nghiêm ngặt về trình duyệt cũ.
3) Bọc logic quan sát vào hook/helper chung để dễ tái sử dụng và dọn dẹp.
4) Batch cập nhật UI, tránh vòng lặp reflow.
5) Viết test E2E bằng trình duyệt thật; JSDOM không tính layout nên khó kiểm chứng hành vi kích thước.
## Kết luận
Để phát hiện thay đổi kích thước phần tử DOM một cách đáng tin cậy và hiệu quả, hãy dùng ResizeObserver làm giải pháp chuẩn và chỉ dự phòng bằng cơ chế ResizeSensor khi cần hỗ trợ trình duyệt cũ. Kết hợp với batching qua requestAnimationFrame, dọn dẹp kết nối đúng lúc, và tách bạch nhiệm vụ giữa CSS (container queries) và JS, bạn sẽ có hệ thống UI responsive, mượt mà và bền vững.