W3docs

JavaScript Resize Observer API

Изучите Resize Observer API в JavaScript для отслеживания изменений размеров элементов — для адаптивных компонентов, canvas и автоматически растущих полей.

ResizeObserver API позволяет реагировать на изменение размера конкретного элемента, независимо от причины этого изменения. Он относится к тому же семейству, что и MutationObserver (наблюдающий за деревом DOM) и IntersectionObserver (наблюдающий за видимостью): это эффективные наблюдатели на основе обратных вызовов, заменяющие громоздкие циклы опроса.

Почему бы не использовать событие resize?

Событие resize окна срабатывает только при изменении размера окна браузера. Однако элемент на странице может изменить размер по множеству других причин:

  • Изменяется правило CSS (срабатывает медиазапрос, переключается класс).
  • Изменяется содержимое элемента (загружается текст, появляется изображение, в список добавляются строки).
  • Flexbox или grid переформатируется из-за того, что соседний элемент вырос или уменьшился.
  • Родительский контейнер изменяется перетаскиваемым разделителем, боковой панелью или сплиттером.

Ни одна из этих причин не изменяет окно браузера, поэтому window.addEventListener('resize', ...) никогда не сработает. Можно использовать опрос через setInterval и сравнивать getBoundingClientRect() каждые несколько миллисекунд, но это расходует CPU и всё равно запаздывает за реальным изменением.

ResizeObserver решает эту проблему: он следит за одним или несколькими элементами и вызывает обратный вызов только тогда, когда их размер действительно изменился.

Информация

ResizeObserver по умолчанию сообщает о размере содержимого элемента, а не о его позиции. Если вам нужно знать, когда элемент перемещается или прокручивается в область видимости, используйте IntersectionObserver. Для измерений на уровне viewport смотрите размеры окна и прокрутка.

Базовое использование

Паттерн повторяет другие наблюдатели: создайте наблюдатель с обратным вызовом, затем укажите ему, за какими элементами observe.

const box = document.querySelector('#box');

const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    console.log(`${entry.target.id} is now ${width} x ${height}`);
  }
});

ro.observe(box);

Обратный вызов получает array записей — по одной на каждый наблюдаемый элемент, изменившийся в данном пакете, — поэтому один наблюдатель может следить за множеством элементов одновременно.

Что содержится в записи

Каждая запись описывает один элемент и его новый размер. Есть два способа прочитать этот размер.

Простой способ — entry.contentRect, DOMRectReadOnly со свойствами width, height, top и left (смещения относительно блока отступов элемента):

const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    console.log(entry.target);          // the element
    console.log(entry.contentRect.width);  // content-box width in px
    console.log(entry.contentRect.height); // content-box height in px
  }
});

Более точный способ использует свойства размеров блока, которые являются массивами объектов { inlineSize, blockSize } (массивами, потому что элемент может быть разбит на несколько колонок):

  • entry.contentBoxSize — блок содержимого, без учёта отступов и границы.
  • entry.borderBoxSize — блок границы, включая отступы и границу.
  • entry.devicePixelContentBoxSize — блок содержимого, измеренный в физических пикселях устройства — идеально для чёткого рендеринга canvas на экранах с высоким DPI.
const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    // inlineSize ~ width, blockSize ~ height (for a horizontal writing mode)
    const { inlineSize, blockSize } = entry.borderBoxSize[0];
    console.log(`border box: ${inlineSize} x ${blockSize}`);
  }
});

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

ro.observe(box, { box: 'border-box' });
Примечание

inlineSize и blockSize учитывают режим записи. В режиме слева направо и сверху вниз (по умолчанию) inlineSize соответствует ширине, а blockSize — высоте, но в вертикальном режиме записи они меняются местами. Предпочитайте их жёстко заданным width/height, если ваш интерфейс должен поддерживать несколько направлений записи.

Методы

ResizeObserver предоставляет три метода:

  • observe(element, options) — начать наблюдение за элементом. Вызывайте один раз на элемент. Необязательный параметр options.box может принимать значения 'content-box' (по умолчанию), 'border-box' или 'device-pixel-content-box'.
  • unobserve(element) — остановить наблюдение за конкретным элементом.
  • disconnect() — остановить наблюдение за всеми элементами одновременно.
ro.observe(el);        // start
ro.unobserve(el);      // stop watching this one
ro.disconnect();       // stop watching everything

Вариант использования 1 — Адаптивные компоненты («Container Queries в JS»)

Медиазапрос реагирует на viewport. Но переиспользуемая карточка может быть широкой в основной колонке и узкой в боковой панели при одной и той же ширине viewport. С помощью ResizeObserver можно адаптировать компонент к его собственной ширине:

const card = document.querySelector('.card');

const ro = new ResizeObserver(([entry]) => {
  const width = entry.contentRect.width;
  // Toggle a layout class based on the element's own width.
  card.classList.toggle('card--compact', width < 400);
});

ro.observe(card);
.card { display: flex; gap: 1rem; }
.card--compact { flex-direction: column; }

Теперь карточка располагается вертикально всякий раз, когда она сама уже 400px — независимо от размера окна. Это удобно в дашбордах, разделённых панелях и встраиваемых виджетах.

Совет

Современный CSS позволяет делать это нативно с помощью container queries (@container). Если нужно лишь изменить стили в зависимости от размера контейнера, предпочтите CSS container queries — они декларативны и работают вне основного потока. Используйте ResizeObserver, когда в ответ на изменение размера нужно выполнить JavaScript (пересчитать значения, перерисовать canvas, перерендерить диаграмму).

Вариант использования 2 — Изменение размера canvas или диаграммы

У <canvas> есть два размера: отображаемый CSS-размер и размер буфера рисования (canvas.width/canvas.height). Если они не совпадают, битмап растягивается и выглядит размытым. ResizeObserver синхронизирует буфер с отображаемым размером, чтобы рисунки оставались чёткими:

const canvas = document.querySelector('#chart');
const ctx = canvas.getContext('2d');

const ro = new ResizeObserver(([entry]) => {
  // Use device-pixel size for sharp rendering on high-DPI screens.
  const size = entry.devicePixelContentBoxSize?.[0];
  const width = size ? size.inlineSize : entry.contentRect.width;
  const height = size ? size.blockSize : entry.contentRect.height;

  canvas.width = width;
  canvas.height = height;
  redraw(ctx, width, height); // your drawing / charting code
});

ro.observe(canvas, { box: 'device-pixel-content-box' });

Та же идея применима к библиотекам построения диаграмм: наблюдайте за контейнером диаграммы и вызывайте метод resize() библиотеки при его изменении — вместо того чтобы подписываться только на window.resize.

Вариант использования 3 — Автоматически растущий Textarea

Поскольку обратный вызов срабатывает при каждом изменении измеренного размера элемента, зависимые элементы можно синхронизировать. Классический пример — отображение высоты textarea в соседнем элементе или реакция на рост, вызванный содержимым:

const textarea = document.querySelector('#message');
const counter = document.querySelector('#height-readout');

const ro = new ResizeObserver(([entry]) => {
  const h = Math.round(entry.contentRect.height);
  counter.textContent = `${h}px tall`;
});

ro.observe(textarea);

Каждый раз, когда пользователь перетаскивает ручку изменения размера textarea — или ваш код изменяет его высоту по мере ввода — счётчик обновляется автоматически, без события resize и без опроса.

Предупреждение «ResizeObserver loop»

Вы можете столкнуться с таким сообщением в консоли:

ResizeObserver loop completed with undelivered notifications.

(В некоторых браузерах формулировка звучит как «ResizeObserver loop limit exceeded».) Это происходит, когда ваш обратный вызов изменяет размер наблюдаемого элемента, что снова запускает наблюдатель, который снова изменяет размер — возникает петля обратной связи.

// Anti-pattern: this can cause the loop warning.
const ro = new ResizeObserver(([entry]) => {
  // Resizing the observed element from inside its own callback. Bad.
  entry.target.style.height = entry.contentRect.width + 'px';
});
Внимание

Не изменяйте синхронно размер наблюдаемого элемента внутри его собственного обратного вызова. Если изменение размера под влиянием размера неизбежно, разорвите цикл: записывайте значение только тогда, когда оно действительно изменилось (защитная проверка), или откладывайте запись с помощью requestAnimationFrame. Обычно предупреждение безвредно и самоустраняется, но истинный бесконечный цикл снизит производительность.

Очистка

Наблюдатель хранит ссылку на каждый элемент, за которым следит, что может препятствовать его сборке мусором. Всегда прекращайте наблюдение, когда оно больше не нужно — например, при размонтировании компонента или уничтожении представления:

function mountWidget(el) {
  const ro = new ResizeObserver(handleResize);
  ro.observe(el);

  // Return a cleanup function.
  return () => ro.disconnect();
}

Забыть об этом — распространённый источник утечек памяти в одностраничных приложениях. Подробнее об оптимизации работы с размерами и макетом смотрите в оптимизации производительности DOM.

ResizeObserver поддерживается во всех современных браузерах, поэтому вы можете использовать его без полифилла в любом актуальном браузере.

Проверьте свои знания

Практика
Почему предпочтительнее использовать ResizeObserver вместо события 'resize' окна для компонента?
Почему предпочтительнее использовать ResizeObserver вместо события 'resize' окна для компонента?
Практика
Что содержит каждая запись ResizeObserver?
Что содержит каждая запись ResizeObserver?
Практика
Что обычно вызывает предупреждение 'ResizeObserver loop'?
Что обычно вызывает предупреждение 'ResizeObserver loop'?
Was this page helpful?