Олег Карнауховweb developerОбсудить проект

Блог

Канвас, который выдавил страницу за экран: как замер раздувал вёрстку

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

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

Что было видно

Замер показал странное: контейнер первого экрана 327 пикселей, а сам блок с роликом — 345 и смещён вправо. Вложенный элемент шире родителя, при том что у него задана ширина 100%. Такого не бывает, если ширина 100% берётся от родителя.

Значит, ширину задавало что-то другое.

Причина: канвас сам себе размер

Код подготовки канваса измерял контейнер и записывал результат в два места: в атрибуты width и height — это буфер пикселей, и в инлайновый стиль — это размер на странице.

Второе и есть ошибка. Канвас с инлайновой шириной в пикселях становится блоком фиксированного размера, а значит его min-content равен этой ширине. Родительская колонка сетки была автоматической, то есть подстраивалась под содержимое, — и она обязана была стать не меньше канваса. Дальше цикл: колонка шире, следующий замер даёт большую ширину, стиль обновляется, колонка снова шире.

app/HeroFilm.tsxts
const resize = () => {
  const rect = box.getBoundingClientRect();
  const ratio = Math.min(devicePixelRatio || 1, 2);
  width = rect.width;
  height = rect.height;

  node.width = Math.round(width * ratio);
  node.height = Math.round(height * ratio);

  // Было — и это самораскручивающийся цикл:
  // node.style.width = `${width}px`;
  // node.style.height = `${height}px`;

  context.setTransform(ratio, 0, 0, ratio, 0, 0);
};
Здесь задаётся только буфер пикселей. Размер на странице — дело CSS, и он не должен возвращаться из измерения.

Починка в двух местах

Первое: убрать запись стиля. Размер канваса на странице задаёт CSS — сто процентов от контейнера.

Второе, важнее: сделать колонку сетки такой, чтобы она вообще не могла раздуться содержимым. minmax(0, 1fr) вместо автоматической. Тогда любой слишком широкий ребёнок будет прокручиваться внутри себя, а не растягивать страницу.

app/globals.csscss
.hero-film {
  display: grid;
  /* Строки: шаги, слова, картинка, абзац. */
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  /* Одна колонка — та, что есть, а не та, которую хочет содержимое. */
  grid-template-columns: minmax(0, 1fr);
}

.hero-film-stage canvas {
  width: 100%;
  max-width: 100%;
  height: 100%;
  display: block;
}
minmax(0, 1fr) — самая полезная запись в CSS-сетке. Ноль в минимуме отменяет неявный минимум по содержимому.

CLS измеряет визуальную стабильность. Хорошим считается значение до 0,1 — по реальным посетителям, на 75-м процентиле.

Core Web Vitals: пороговые значения LCP, INP и CLS

Как проверять такое у себя

  • Сравнить scrollWidth документа с innerWidth — есть ли горизонтальная прокруткауже так
  • Найти элемент, который шире родителя, и посмотреть, откуда у него ширинауже так
  • Проверить, нет ли инлайновых стилей, записанных из JavaScriptуже так
  • Заменить автоматические колонки сетки на minmax(0, 1fr)уже так
  • Повторить замер на 375 пикселях, а не только на десктопеуже так