Гайды

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

Иллюстрация: карточка на столе тянется в ширину, а рядом линейка отмечает её новый край

У тебя дашборд: слева боковая панель, справа график. График рисуется под ширину контейнера. Ты подписался на window.addEventListener("resize", ...), проверил — растягиваешь окно, график подстраивается. Отлично.

Потом кто-то сворачивает боковую панель. Контейнер графика стал на 240 пикселей шире. А график остался узким, с пустой полосой справа.

Смотри, в чём дело: окно браузера-то не менялось. Событие resize есть только у окна. Элемент может вырасти от свёрнутой панели, от подгруженного текста, от смены шрифта — и никакого события не будет. Для этого в браузере есть отдельный наблюдатель — ResizeObserver.

Что делает ResizeObserver

ResizeObserver следит за размером конкретного элемента и вызывает твою функцию, когда этот размер изменился. Неважно, по какой причине: окно, соседний блок, контент внутри.

Он работает во всех современных браузерах. Библиотеки не нужны.

Пошагово: следим за размером контейнера

  1. Возьми элемент, за которым будешь следить. Не окно, а именно контейнер, от которого зависит твой график:

    const box = document.querySelector(".chart-box");
    
  2. Создай наблюдателя с функцией-обработчиком. Браузер передаст в неё список изменившихся элементов:

    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        const { inlineSize: width, blockSize: height } = entry.contentBoxSize[0];
        chart.resize(width, height);
      }
    });
    

    inlineSize — это ширина, blockSize — высота. Почему такие имена и почему массив — разберём ниже.

  3. Начни наблюдение:

    observer.observe(box);
    

    Важная деталь: обработчик вызовется сразу, один раз, с текущим размером. Отдельно «нарисовать первый раз» не нужно.

  4. Выбери, какой размер мерить. По умолчанию — content-box, то есть без отступов и рамок. Если тебе нужен размер вместе с padding и border, попроси его явно:

    observer.observe(box, { box: "border-box" });
    

    А в обработчике читай entry.borderBoxSize[0]. Если путаешься, где кончается контент и начинается рамка, освежи блочную модель.

  5. Не забудь отписаться. Когда элемент уходит со страницы — в 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 имеет смысл, только если перерисовка очень тяжёлая — например, запрос к серверу.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →