Как автоматически изменять размеры элемента canvas так, чтобы он всегда соответствовал размерам страницы (окна браузера)?

У меня есть canvas, растянутый на всю страницу, и его нужно подстраивать при изменении размеров окна. При установке CSS width/height: 100% содержимое размывается. Я пробовал в обработчике window.resize менять реальный размер элемента <canvas>, корректировать контекст и вызывать ререндер (объекты для отрисовки хранятся в JSON и рисуются по координатам) — тогда отображение правильное, но иногда приложение зависает при ресайзе (например, при изменении влево — всё нормально, при дальнейшем изменении вверх — зависает и не восстанавливается; похожие проблемы при переходе в полноэкранный режим). Использование ResizeObserver дало тот же или худший результат. Как правильно динамически изменять размеры canvas под экран, чтобы избежать размытия и зависаний?
  • 1 июля 2026 г. 7:00
Ответы на вопрос 1
Коротко — две главные вещи, которые нужно делать правильно при динамическом масштабировании canvas:

1) управлять реальным буфером пикселей элемента (атрибуты canvas.width/height) в соответствии с CSS‑размерами *и* devicePixelRatio, а не полагаться только на CSS width/height;  
2) не пересоздавать/перерисовывать всё синхронно на каждый событие resize — использовать requestAnimationFrame / дебаунс / offscreen‑рендеринг, ограничивать разрешение и учитывать максимумы GPU, чтобы не перегрузить CPU/GPU.

Ниже — подробности и готовый шаблон.

Почему размывается
- Если вы ставите в CSS width/height:100% и не меняете canvas.width/canvas.height, то браузер масштабирует растровый буфер на экранные пиксели → размытость на HiDPI.
- Правильно: CSS size (в CSS‑пикселях) должен совпадать с отображаемым размером, а реальный буфер должен быть CSS_size * devicePixelRatio.

Правильный алгоритм (пошагово)
1. Получаете видимый размер в CSS‑пикселях (например, getBoundingClientRect() или clientWidth/clientHeight).  
2. Умножаете на devicePixelRatio (dpr = window.devicePixelRatio || 1) и округляете.  
3. Проверяете — если canvas.width/height уже равны этим значениям, ничего не делаете.  
4. Иначе устанавливаете canvas.width = w*dpr, canvas.height = h*dpr и canvas.style.width = w+'px', canvas.style.height = h+'px'.  
5. Сбрасываете/устанавливаете transform контекста: ctx.setTransform(dpr,0,0,dpr,0,0) (или ctx.scale(dpr,dpr) после ctx.setTransform(1,0,0,1,0,0)) — тогда ваши координаты остаются в CSS‑пикселях.  
6. Вызываете ререндер (желательно в requestAnimationFrame).

Пример функции (упрощённо)
```js
function resizeCanvasToDisplaySize(canvas, drawFn) {
  const dpr = Math.max(1, window.devicePixelRatio || 1);
  const rect = canvas.getBoundingClientRect();
  const cssW = Math.round(rect.width);
  const cssH = Math.round(rect.height);
  // предохраняемся от нуля/слишком больших значений
  const maxTexture = 16384; // подберите под целевую платформу
  const w = Math.min(maxTexture, Math.max(1, Math.floor(cssW * dpr)));
  const h = Math.min(maxTexture, Math.max(1, Math.floor(cssH * dpr)));

  if (canvas.width !== w || canvas.height !== h) {
    canvas.width = w;
    canvas.height = h;
    canvas.style.width = cssW + 'px';
    canvas.style.height = cssH + 'px';
    const ctx = canvas.getContext('2d');
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // теперь координаты в CSS-пикселях
    if (typeof drawFn === 'function') drawFn();
  }
}
```

Как интегрировать с событиями ресайза без зависаний
- Не делайте тяжёлый ререндер в обработчике window.onresize напрямую. Сформируйте изменение размера и рендер на следующем requestAnimationFrame.
- Используйте дебаунс или throttle. Лучше: при первом событии запускайте requestAnimationFrame, а все последующие события до следующего RAF игнорируйте.
- Можно использовать ResizeObserver на контейнере, но всё равно нужно дебаунс/RAF, потому что RO может выдавать много событий за секунду.

Пример обработки с RAF
```js
let rafId = null;
const canvas = document.querySelector('canvas');

function scheduleResizeAndDraw() {
  if (rafId) return;
  rafId = requestAnimationFrame(() => {
    rafId = null;
    resizeCanvasToDisplaySize(canvas, drawAll);
  });
}

window.addEventListener('resize', scheduleResizeAndDraw);
new ResizeObserver(scheduleResizeAndDraw).observe(document.documentElement); // или контейнер
```

Оптимизации для плавности и предотвращения подвисаний
- Ограничьте dpr или итоговый размер, если canvas становится слишком большим (см. maxTexture). На больших мониторах и с высоким DPR буфер может превысить возможности GPU и тормозить. Можно масштабировать downsample: используя dprEffective = Math.min(dpr, 2) или динамически снижать во время ресайза.
- Если рендер очень тяжёлый — используйте OffscreenCanvas + WebWorker: в воркере делаете тяжёлую отрисовку в OffscreenCanvas, возвращаете ImageBitmap и отрисовываете на основном canvas. Это снимает нагрузку с UI‑потока.
- Во время интерактивного изменения размера (перетаскивание границы) можно рендерить временно в более низком разрешении (например 0.5 * dpr), а по окончании ресайза — перерисовать в полном качестве.
- Не считывайте layout-параметры (getBoundingClientRect, clientWidth) часто и не смешивайте чтение и запись DOM в одном цикле — это вызывает layout thrash. Читайте размеры один раз, затем изменяйте canvas и рендерьте.
- Если приложение “зависает” на некоторых направлениях изменения окна, вероятно вы либо создаёте рекурсивные/многочасовые перерисовки, либо создаёте очень большой буфер (выполняется OOM или GPU блокировка). Проверьте исходные размеры и console.logs, ставьте ограничение maxTexture.

Когда стоит применять OffscreenCanvas
- Если у вас очень сложная векторная/пиксельная сцена (много объектов) и перенос рендеринга в фон уменьшит лаг при ресайзе — используйте OffscreenCanvas в воркере. Тогда на основной странице вы просто drawImage(imageBitmap) — это быстро.

Контрольный список для отладки зависаний
- Логируйте вычисленные cssW/cssH и итоговые canvas.width/height при ресайзе. Если они внезапно становятся гигантскими — именно это вызывает проблему.  
- Проверьте devicePixelRatio на устройствах (иногда меняется при перемещении окна на другой монитор).  
- Проверьте максимальный размер текстуры GPU (16384, 32768 и т.д.) и лимитируйте.  
- Убедитесь, что рендер происходит через RAF, а не синхронно внутри обработчика resize/RO.  
- При подозрении на утечки/блокировку профилируйте main thread в DevTools.

Итого
- Делайте canvas.width/height = cssSize * devicePixelRatio, а CSS style.width/height = cssSize px;  
- Устанавливайте контекстный transform (setTransform) под dpr;  
- Дебаунсьте/используйте requestAnimationFrame при ресайзе; при тяжёлой отрисовке — offscreen/worker или рендер в пониженном разрешении во время ресайза;  
- Ограничьте максимальные размеры, чтобы не превысить GPU/память.

Если хотите, пришлите ваш текущий код обработчика ресайза и функцию рендера — посмотрю конкретно и укажу, где вероятно затыки и как их исправить.
Похожие вопросы