CSS content-visibility и Containment: ленивый рендеринг, который ускоряет страницу в 7 раз

Две строчки CSS ускоряют рендеринг в 7 раз. Разбираем CSS content-visibility и containment: как работает ленивый рендеринг, реальные кейсы с цифрами, пошаговое внедрение и типичные ошибки.

Почему рендеринг — узкое место, о котором все забывают

Вы оптимизировали изображения, настроили CDN, включили Brotli-сжатие и вырезали лишний JavaScript. Lighthouse показывает зелёные цифры, а страница всё равно подтормаживает при скролле и получает плохой INP. Знакомая ситуация?

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

Вот конкретный пример. На типичной странице e-commerce каталога с 50–100 карточками товаров браузер тратит сотни миллисекунд на рендеринг элементов, которые ещё даже не попали в viewport. Каждая карточка — это изображения, стили, расчёт позиции. Умножьте на сотню, и получите 300–800 мс рендеринга при первой загрузке, блокировку основного потока и проваленный порог INP в 200 мс.

Именно эту проблему решают CSS Containment и свойство content-visibility. По сути, это механизм ленивого рендеринга, встроенный прямо в браузер — буквально две-три строчки CSS, которые могут ускорить начальную отрисовку страницы в 7 раз. И в 2026 году это наконец работает во всех основных браузерах, без оговорок.

Что такое CSS Containment и зачем он нужен

CSS Containment — это спецификация, которая позволяет изолировать поддерево DOM от остальной страницы. Идея простая: если браузер знает, что конкретный блок не влияет на остальные элементы, он может оптимизировать рендеринг — пропустить пересчёт стилей и Layout для этого блока, когда меняется что-то снаружи.

Управляется это свойством contain, у которого есть несколько значений:

  • layout — изолирует расчёт Layout. Изменения внутри элемента не вызовут перерасчёт позиций снаружи, и наоборот.
  • paint — ограничивает отрисовку границами элемента. Содержимое не может визуально «вылезти» за пределы контейнера.
  • style — изолирует CSS-счётчики и другие стилевые эффекты. Не путайте с Shadow DOM — стили тут не скоупятся.
  • size — размер элемента вычисляется без учёта дочерних элементов, как будто он пуст.
  • content — сокращение для layout paint style. Самый безопасный вариант для широкого применения.

Базовый пример:

/* Изолируем секции страницы */
.page-section {
  contain: content; /* layout + paint + style */
}

/* Для элементов с фиксированным размером можно добавить size */
.product-card {
  contain: strict; /* layout + paint + style + size */
  width: 300px;
  height: 400px;
}

Значение contain: content — самый безопасный выбор. Оно говорит браузеру: «содержимое этого блока не влияет на внешний Layout и Paint». Браузер может пропустить пересчёт этого поддерева при изменениях в других частях страницы. А вот strict добавляет size containment и даёт максимальную оптимизацию, но требует явно заданных размеров — иначе элемент просто схлопнется в ноль (и это неприятный сюрприз, если вы к нему не готовы).

Когда contain работает, а когда — нет

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

Но есть нюанс. contain не пропускает начальный рендеринг. Браузер всё равно вычисляет стили и Layout для всех элементов при первой загрузке — он просто делает это эффективнее при последующих обновлениях. Для настоящего ленивого рендеринга нужен следующий уровень — content-visibility.

content-visibility: auto — ленивый рендеринг на уровне браузера

Честно говоря, content-visibility — это именно то свойство, ради которого стоит читать эту статью. Оно идёт значительно дальше простого containment и позволяет браузеру полностью пропустить рендеринг элемента — включая Layout, Paint, вычисление стилей и даже hit-testing — до тех пор, пока элемент не приблизится к видимой области экрана.

У свойства три значения:

  • visible — значение по умолчанию, никаких оптимизаций.
  • hidden — контент скрыт и не рендерится, но его состояние кешируется. При повторном показе рендеринг происходит мгновенно.
  • auto — главная звезда. Контент за пределами viewport не рендерится, но при приближении к видимой области браузер автоматически запускает рендеринг just-in-time. При этом контент остаётся доступным для поиска по странице (Ctrl+F), навигации по Tab и скринридеров.

Минимальный рабочий пример

Допустим, у вас блог с длинным списком постов:

<!-- HTML: список постов блога -->
<main>
  <article class="post">
    <h2>Заголовок первого поста</h2>
    <p>Контент...</p>
  </article>
  <article class="post">
    <h2>Заголовок второго поста</h2>
    <p>Контент...</p>
  </article>
  <!-- ...ещё 50 постов -->
</main>
/* CSS: две строчки — и рендеринг ускорен в разы */
.post {
  content-visibility: auto;
  contain-intrinsic-size: auto 500px;
}

Всё. Две строчки CSS. Серьёзно.

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

Как это работает внутри

Когда браузер встречает элемент с content-visibility: auto, происходит следующее:

  1. Элемент автоматически получает layout, style и paint containment.
  2. Если элемент за пределами viewport — добавляется size containment, и браузер полностью пропускает рендеринг содержимого.
  3. Элемент занимает пространство, заданное через contain-intrinsic-size — это предотвращает скачки Layout.
  4. Когда элемент приближается к viewport — size containment снимается, контент рендерится, и элемент принимает свой реальный размер.
  5. Когда элемент уходит обратно за viewport — процесс повторяется, но браузер запоминает реальный размер (если использовать contain-intrinsic-size: auto <length>).

Критически важный момент: в отличие от display: none или visibility: hidden, контент с content-visibility: auto остаётся в DOM и accessibility tree. Пользователь может найти текст через Ctrl+F, перейти к элементу по Tab, а скринридер прочитает содержимое. Браузер просто не тратит ресурсы на визуальный рендеринг — и это красиво.

contain-intrinsic-size: борьба с прыгающим скроллбаром

Итак, вы добавили content-visibility: auto, запустили страницу... и скроллбар начал жить своей жизнью. Дёргается, прыгает, меняет длину на ходу. Это самая частая проблема при внедрении.

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

Решение — свойство contain-intrinsic-size, которое задаёт «предполагаемый» размер элемента в нерендеренном состоянии. В 2026 году лучшая практика — обязательно использовать ключевое слово auto:

/* ❌ Старый подход: фиксированный placeholder */
.section {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px; /* всегда 500px, даже если реальная высота другая */
}

/* ✅ Лучший подход 2026: auto + fallback */
.section {
  content-visibility: auto;
  contain-intrinsic-size: auto 500px; /* запоминает реальный размер, 500px — fallback */
}

/* ✅ Для grid/multi-column layout */
.grid-item {
  content-visibility: auto;
  contain-intrinsic-size: auto none; /* предотвращает overflow в grid */
}

Как работает auto <length>:

  1. При первом рендеринге элемент использует fallback-значение (500px в нашем примере).
  2. Когда элемент попадает в viewport и рендерится, браузер запоминает его реальный размер.
  3. Когда элемент уходит обратно за viewport, вместо fallback используется запомненный размер.
  4. Результат — скроллбар остаётся стабильным. Никаких дёрганий.

Небольшой совет: выбирайте fallback-значение максимально близко к средней высоте ваших элементов. Если посты блога обычно занимают 400–600px, ставьте auto 500px. Чем ближе fallback к реальности — тем стабильнее первоначальная прокрутка, пока браузер ещё не запомнил реальные размеры.

Реальные кейсы: цифры и результаты

Теория — это хорошо. Но давайте посмотрим, что дают эти оптимизации на реальных проектах.

Демо web.dev: ускорение рендеринга в 7 раз

В демонстрации команды Chrome на web.dev применение content-visibility: auto к блокам контента на длинной странице снизило время рендеринга с 232 мс до 30 мс — ускорение в 7,7 раз. И это на странице с относительно простым контентом. На реальных сайтах со сложной вёрсткой эффект бывает ещё заметнее.

Speed Kit: A/B тест на e-commerce (2024–2025)

Компания Speed Kit провела серию A/B тестов на живых e-commerce сайтах. На категорийных страницах крупного спортивного ритейлера с 36 SSR-рендеренными карточками товаров (каждая с глубиной DOM до 130 элементов) content-visibility: auto дал улучшение INP на 245 мс на 95-м перцентиле в мобильном Samsung Browser. По сути, одно CSS-свойство превратило «красный» INP в «зелёный».

Facebook: 250 мс экономии при навигации

Инженеры Facebook использовали content-visibility: hidden для кеширования неактивных вкладок в SPA. Результат — до 250 мс экономии при возврате к ранее посещённым представлениям. Браузер не перерисовывал всё заново, а просто «включал» уже отрендеренный контент.

Wantedly: ускорение Layout в 3–6 раз

На страницах с тысячами элементов в списках разработчики Wantedly получили ускорение Layout в 3–6 раз. Но тут они наступили на грабли: на адаптивных страницах высота элементов менялась в зависимости от устройства, и фиксированный contain-intrinsic-size приводил к постоянным пересчётам Layout. Переход на auto <length> решил проблему.

Практическое руководство: как внедрить content-visibility в проект

Шаг 1: определите кандидатов для оптимизации

Откройте Chrome DevTools → Performance, запишите трейс загрузки страницы и посмотрите на секцию Rendering. Ищите длительные задачи Layout и Paint. Лучшие кандидаты для content-visibility: auto:

  • Секции страницы ниже первого экрана (below the fold)
  • Списки карточек товаров, постов, комментариев
  • Футер и боковые панели
  • Табы и аккордеоны со скрытым контентом
  • Длинные таблицы данных

Важно: не применяйте к элементам в первом viewport — это навредит LCP, потому что браузер задержит рендеринг критического контента. Это ровно противоположный эффект от желаемого.

Шаг 2: примените CSS

/* Базовый паттерн для секций страницы */
.below-fold-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 600px;
}

/* Для карточек товаров в каталоге */
.product-card {
  content-visibility: auto;
  contain-intrinsic-size: auto 420px;
}

/* Для постов в ленте */
.feed-post {
  content-visibility: auto;
  contain-intrinsic-size: auto 350px;
}

/* Для футера */
footer {
  content-visibility: auto;
  contain-intrinsic-size: auto 300px;
}

Шаг 3: проверьте в DevTools

После применения content-visibility откройте Chrome DevTools → Performance и запишите ещё один трейс. Сравните время Rendering до и после — вы должны увидеть ощутимое сокращение времени Layout и Paint при начальной загрузке.

Ещё загляните во вкладку Elements — элементы с пропущенным рендерингом будут помечены специальным индикатором.

Шаг 4: проверьте edge cases

/* Если используете якорные ссылки — проверьте плавную прокрутку */
html {
  scroll-behavior: smooth;
}

/* content-visibility: auto может сломать smooth scrolling к якорям внутри
   нерендеренных секций. Решение — исключите целевые секции */
.section-with-anchor-target {
  content-visibility: visible; /* не применяем ленивый рендеринг */
}

content-visibility: hidden — секретное оружие для SPA

Значение hidden часто упускают из виду, а зря — для одностраничных приложений оно невероятно полезно. В отличие от display: none, content-visibility: hidden сохраняет состояние рендеринга в кеше. При повторном показе элемент появляется мгновенно, без перерисовки с нуля.

/* SPA: переключение вкладок с кешированием */
.tab-panel {
  content-visibility: hidden; /* скрыт, но кеширован */
}

.tab-panel.active {
  content-visibility: visible; /* моментальное появление */
}

/* Анимация показа (поддержка с 2025+) */
.tab-panel {
  content-visibility: hidden;
  transition: content-visibility 300ms allow-discrete;
}

.tab-panel.active {
  content-visibility: visible;
}

Практический сценарий: у вас дашборд с 5 вкладками, каждая набита сложными графиками и таблицами. Вместо того чтобы уничтожать и пересоздавать DOM при переключении (привет, React-ререндеры), вы просто переключаете content-visibility между hidden и visible. Переход — мгновенный.

Поддержка браузерами в 2026

С сентября 2025 года content-visibility получило статус Baseline Newly Available — поддержка во всех трёх основных движках:

  • Chrome/Edge — полная поддержка с версии 85 (с 2020 года, кстати)
  • Firefox — полная поддержка с версии 125 (2024)
  • Safari — поддержка с версии 18 (2024)

Есть одна оговорка: в Safari (по состоянию на 18.3.1) встроенный поиск по странице (Cmd+F) не всегда находит текст внутри элементов с content-visibility: auto, если они за пределами viewport. Проблема известная, Apple работает над исправлением. В Chrome и Firefox поиск работает корректно.

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

Типичные ошибки и подводные камни

Ошибка 1: применение к элементам в первом viewport

Если вы примените content-visibility: auto к hero-секции или LCP-элементу, браузер может задержать его рендеринг. Это ухудшит LCP. Правило простое: применяйте только к контенту ниже первого экрана.

Ошибка 2: забыли contain-intrinsic-size

Без contain-intrinsic-size нерендеренные элементы имеют нулевую высоту. Это ломает скроллбар, вызывает CLS и создаёт совершенно ужасный UX при прокрутке. Всегда задавайте contain-intrinsic-size: auto <length>.

Ошибка 3: DOM API в скрытых элементах

Если JavaScript вызывает getBoundingClientRect(), offsetHeight или другие Layout API на элементе с пропущенным рендерингом — браузер будет вынужден отрендерить его синхронно. Весь выигрыш в производительности улетает. Проверьте свой JS-код на наличие таких вызовов — это одна из тех ошибок, которые сложно отловить без профилирования.

Ошибка 4: якорные ссылки и smooth scrolling

content-visibility: auto может сломать плавную прокрутку к якорям, если целевой элемент находится внутри нерендеренного блока. Браузер просто не знает точную позицию якоря, пока элемент не отрендерен. Решение — не применяйте content-visibility: auto к блокам с часто используемыми anchor-целями.

Ошибка 5: overflow и CSS-трансформации

Элементы с content-visibility: auto получают paint containment, что ведёт себя как overflow: hidden. Если дочерний элемент масштабируется через transform: scale() больше 1.0, он будет обрезан. Учитывайте это при работе с анимациями и hover-эффектами — я лично потратил на эту ошибку пару часов, прежде чем понял, в чём дело.

Комбинирование с другими техниками оптимизации

Максимальный эффект получается при комбинировании content-visibility с другими оптимизациями. Вот паттерн, который хорошо себя показывает:

/* Комплексная оптимизация длинной страницы */

/* 1. Hero-секция: максимальный приоритет */
.hero img {
  fetchpriority: high;
  loading: eager;
}

/* 2. Секции ниже fold: ленивый рендеринг */
.content-section:nth-child(n+2) {
  content-visibility: auto;
  contain-intrinsic-size: auto 500px;
}

/* 3. Изображения ниже fold: ленивая загрузка */
.content-section:nth-child(n+2) img {
  loading: lazy;
  decoding: async;
}

/* 4. Тяжёлые виджеты: комбинация contain и content-visibility */
.heavy-widget {
  content-visibility: auto;
  contain-intrinsic-size: auto 400px;
  contain: layout style; /* дополнительная изоляция для обновлений */
}

Эта комбинация работает синергетически: content-visibility пропускает рендеринг скрытых секций, loading="lazy" откладывает загрузку изображений внутри них, а fetchpriority="high" обеспечивает приоритет LCP-элемента. В итоге — быстрый LCP, низкий INP и минимальное потребление ресурсов.

Измерение эффекта: как доказать результат

Внедряя content-visibility, обязательно замеряйте результат до и после. Без цифр вы не узнаете, насколько это помогло конкретно вашему проекту. Вот практический чеклист:

  1. Chrome DevTools → Performance: запишите трейс и сравните суммарное время Rendering (Layout + Paint) до и после.
  2. Web Vitals Extension: проверьте INP при типичных взаимодействиях — клик, скролл, ввод текста.
  3. Lighthouse: сравните общий Performance Score и конкретные метрики.
  4. CrUX (Chrome UX Report): через 28 дней проверьте полевые данные для INP и CLS.
  5. CLS: убедитесь, что contain-intrinsic-size не вносит дополнительный Layout Shift.

Полезный трюк: в Chrome DevTools → Rendering включите опцию «Paint flashing». Она подсвечивает зелёным области, которые перерисовываются. С content-visibility: auto вы увидите, что элементы за viewport не мигают зелёным — они действительно не рендерятся. Выглядит весьма убедительно, особенно когда нужно показать результат тимлиду или менеджеру.

Часто задаваемые вопросы

Влияет ли content-visibility на SEO и индексацию?

Нет. Контент с content-visibility: auto остаётся в DOM и полностью доступен поисковым роботам. В отличие от display: none, этот контент не скрыт — он просто не отрисован визуально. Googlebot видит весь контент независимо от CSS-рендеринга. Более того, улучшение Core Web Vitals (INP, CLS) за счёт этого свойства может положительно повлиять на ранжирование.

Можно ли использовать content-visibility с фреймворками (React, Vue, Angular)?

Да, без проблем. content-visibility — чистое CSS-свойство, которое работает независимо от фреймворка. Добавьте его в стили компонентов-контейнеров: списков, секций, карточек. Единственное — проверьте, что ваш JS-код не вызывает Layout API (offsetHeight, getBoundingClientRect()) на нерендеренных элементах, иначе браузер принудительно запустит рендеринг и выигрыш будет потерян.

Чем content-visibility: hidden отличается от display: none?

Главное отличие — content-visibility: hidden кеширует состояние рендеринга. При повторном показе элемент появляется мгновенно, без полного перерендеринга. С display: none элемент полностью удаляется из рендер-дерева, и при повторном показе браузер строит Layout и Paint заново. Для SPA с переключением вкладок разница очень заметна.

Безопасно ли использовать content-visibility в продакшене?

Вполне. С сентября 2025 года свойство имеет статус Baseline Newly Available и поддерживается в Chrome, Firefox и Safari. Даже если какой-то экзотический браузер его не поддержит — свойство просто игнорируется, страница работает нормально. Это полностью безопасное прогрессивное улучшение, которое можно катить в прод без страха.

Насколько content-visibility улучшает INP?

Зависит от структуры страницы. На страницах с большим количеством контента ниже fold (каталоги, ленты, длинные статьи) реальные улучшения INP составляют 100–250 мс — этого часто достаточно, чтобы перейти из «красной» зоны (>200 мс) в «зелёную». Наибольший эффект — на мобильных устройствах, где процессор слабее и каждая миллисекунда рендеринга на счету.

Об авторе Editorial Team

Our team of expert writers and editors.