Как отследить изменение размера элемента — и почему window.resize тут молчит

У тебя дашборд: слева боковая панель, справа график. График рисуется под ширину контейнера. Ты подписался на window.addEventListener("resize", ...), проверил — растягиваешь окно, график подстраивается. Отлично.
Потом кто-то сворачивает боковую панель. Контейнер графика стал на 240 пикселей шире. А график остался узким, с пустой полосой справа.
Смотри, в чём дело: окно браузера-то не менялось. Событие resize есть только у окна. Элемент может вырасти от свёрнутой панели, от подгруженного текста, от смены шрифта — и никакого события не будет. Для этого в браузере есть отдельный наблюдатель — ResizeObserver.
Что делает ResizeObserver
ResizeObserver следит за размером конкретного элемента и вызывает твою функцию, когда этот размер изменился. Неважно, по какой причине: окно, соседний блок, контент внутри.
Он работает во всех современных браузерах. Библиотеки не нужны.
Пошагово: следим за размером контейнера
-
Возьми элемент, за которым будешь следить. Не окно, а именно контейнер, от которого зависит твой график:
const box = document.querySelector(".chart-box"); -
Создай наблюдателя с функцией-обработчиком. Браузер передаст в неё список изменившихся элементов:
const observer = new ResizeObserver((entries) => { for (const entry of entries) { const { inlineSize: width, blockSize: height } = entry.contentBoxSize[0]; chart.resize(width, height); } });inlineSize— это ширина,blockSize— высота. Почему такие имена и почему массив — разберём ниже. -
Начни наблюдение:
observer.observe(box);Важная деталь: обработчик вызовется сразу, один раз, с текущим размером. Отдельно «нарисовать первый раз» не нужно.
-
Выбери, какой размер мерить. По умолчанию —
content-box, то есть без отступов и рамок. Если тебе нужен размер вместе сpaddingиborder, попроси его явно:observer.observe(box, { box: "border-box" });А в обработчике читай
entry.borderBoxSize[0]. Если путаешься, где кончается контент и начинается рамка, освежи блочную модель. -
Не забудь отписаться. Когда элемент уходит со страницы — в React это возврат из
useEffect— останови наблюдение:observer.disconnect();Без этого наблюдатель живёт дальше и держит ссылку на элемент в памяти.
В React всё это укладывается в один эффект:
useEffect(() => {
const observer = new ResizeObserver(([entry]) => {
setWidth(entry.contentBoxSize[0].inlineSize);
});
observer.observe(ref.current);
return () => observer.disconnect();
}, []);
В старых примерах ты встретишь entry.contentRect.width. Он тоже работает, но это наследие первой версии API. contentBoxSize новее и умеет отдавать и border-box, поэтому в новом коде бери его.
Шаг, на котором ломаются все: ошибка в консоли
Однажды ты увидишь в консоли:
ResizeObserver loop completed with undelivered notifications.
Это не баг браузера. Это ты в обработчике изменил размер того же элемента, за которым следишь. Он изменился — снова вызов — снова меняешь. Браузер обрывает этот круг и откладывает остаток уведомлений на следующий кадр.
Чаще всего виновато так:
new ResizeObserver(([entry]) => {
box.style.height = entry.contentRect.width / 2 + "px"; // меняем то, что наблюдаем
});
Как починить:
- Меняй другой элемент. Наблюдай за обёрткой, а размер задавай вложенному блоку.
- Отложи изменение на кадр: оберни код в
requestAnimationFrame(() => { ... }). Цикл разрывается. - Подумай, нужен ли тут JS вообще. Пропорцию 2:1 CSS делает сам:
aspect-ratio: 2 / 1.
Обычно предупреждение безвредное — раскладка в итоге встаёт правильно. Но если оно сыплется постоянно, это знак, что обработчик гоняет сам себя.
Если наблюдаешь сразу за многими элементами — например, за каждой карточкой в ленте, — не создавай по наблюдателю на каждую. Один ResizeObserver спокойно следит за сотней элементов: вызывай observe для каждого, а в обработчике различай их по entry.target.
Что получится
Твой график теперь подстраивается под свой контейнер, а не под окно. Свернули панель, развернули чат сбоку, догрузили легенду — он перерисовывается сам. И код при этом короче, чем с window.resize: не нужно вручную искать, кто и когда поменял ширину.
Если задача чисто визуальная — например, шрифт или колонки должны меняться от ширины — сначала попробуй CSS. Для текста хорошо работает адаптивный размер шрифта через clamp, для раскладок — grid. ResizeObserver нужен, когда размер должен знать именно JavaScript: графики, канвас, виртуальные списки.
Почему contentBoxSize — массив?
Для элементов, которые разбиваются на части, — например, текста в колонках. В обычной вёрстке там всегда один элемент, поэтому смело бери [0].
Чем это лучше window.resize с проверкой ширины?
resize срабатывает только при изменении окна и не знает про отдельные блоки. ResizeObserver реагирует на любую причину изменения элемента и не вызывается, если размер на самом деле не поменялся.
Нужно ли добавлять debounce?
Обычно нет: браузер сам вызывает обработчик не чаще одного раза за кадр. Debounce имеет смысл, только если перерисовка очень тяжёлая — например, запрос к серверу.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.


