javascript

Baseline: март 2026 или самый насыщенный выпуск

  • воскресенье, 12 апреля 2026 г. в 00:00:32
https://habr.com/ru/articles/1018200/

В этом выпуске 12 фич, которые стали доступы повесеместно с хорошей поддержкой. От трёх фич я в полном воссторге.

Обзор на браузерные API, которые стали Widely available в марте 2026. Раз в месяц я буду вам напоминать, что вы уже можете использовать в проде.

Каждый месяц выходят новые CSS-свойства, HTML-атрибуты, JavaScript-методы и WebAPI, но применять в проде мы их конечно же не будем.

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

Как мы понимаем, что уже можно использовать в проде?

У каждой компании, да что уж там компании, у каждой команды в компании своя методика принятия решения о внедрении той или иной фичи в проекте.

Общий же сценарий выглядит так:

- Посмотрели в пользовательские метрики. Поняли какими браузерами и их версиями в основном пользуются пользователи проекта;

- Заглянули в caniuse и поняли, какие фичи уже поддерживаются большинством браузеров;

- Приняли решение о внедрении той или иной фичи в проект.

Какие-то команды позволяют себе указывать правило "последние три версии браузеров". У других специфика проекта, что проект работает исключительно на iPad с Safari. Сами понимаете, все мы разные и требования разные, и у каждого свой подход.

Baseline - позволяет немного упростить процесс принятия решения о внедрении той или иной фичи в проект. Если фича Widely available значит фича уже как минимум есть во всех основных браузерах как минимум стабильно используются последние 2.5 года.

Какие фичи в вебе стали Widely available в марте 2026?

  • Clear-Site-Data

  • contain-intrinsic-size

  • @counter-style

  • Device orientation events

  • hyphenate-character

  • hyphens

  • image-set()

  • <link rel="modulepreload">

  • Overflow media queries

  • Storage manager

  • Update frequency media query

  • subgrid

1. Clear-Site-Data

Я считаю, что эта функция настолько крутая, насколько она недооценена, но доказать это не могу =)

HTTP-заголовок Clear-Site-Data даёт серверу способ попросить браузер самостоятельно сбросить локальные данные: кэш, куки, хранилища страницы, а при необходимости другие типы. Это сигнал клиенту: очистить то, что браузер держит у себя для этого сайта.

Когда это можно применять: выход из аккаунта, смена пользователя на одном устройстве, сценарии, когда после сессии нельзя оставлять данные в IndexedDB / localStorage, и восстановление пользователей после «битого» кэша или застрявшего service worker — без ручной чистки через DevTools.

Синтаксис

Каждая директива это строка в двойных кавычках. Без кавычек значение считается невалидным. Несколько директив перечисляют через запятую.

Clear-Site-Data: "cache"
Clear-Site-Data: "cache", "cookies"
Clear-Site-Data: "*"

Основные директивы:

  • "cache" — сбросить данные HTTP-кэша. В разных браузерах это может затронуть и смежное (например, bfcache, предзагрузки, шейдеры WebGL и т.п.).

  • "cookies" — удалить все куки для зарегистрированного домена, включая поддомены. Вместе с ними сбрасываются и учётные данные HTTP-аутентификации.

  • "storage" — очистить DOM storage: в том числе localStorage, sessionStorage, IndexedDB, регистрации service worker (аналог unregister), устаревшие Web SQL / FileSystem / данные плагинов.

  • "executionContexts" (экспериментально) — сигнал перезагрузить контексты просмотра для origin (в духе Location.reload()).

  • "clientHints" (экспериментально) — сброс сохранённых Client Hints (Accept-CH); в браузерах, где это поддерживается, подсказки могут очищаться и при "cache", "cookies" или "*", так что отдельная директива нужна, когда вы хотите сбросить только их.

  • "prefetchCache" / "prerenderCache" (экспериментально) — очистка speculation rules: prefetch и prerender. Для обоих видов спекуляций обычно указывают обе директивы, если нужно вычистить всё.

  • "*" — очистить все типы данных, которые заголовок покрывает сейчас и будет покрывать в будущих версиях спецификации.

Примеры

Полный набор для типичного «мы точно вышли из сессии»:

HTTP/1.1 200 OK
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts", "prefetchCache", "prerenderCache"
Content-Type: text/html

Только куки по зарегистрированному домену и поддоменам:

Clear-Site-Data: "cookies"

Практика:

Обычный POST /logout снимает серверную сессию, но в браузере нередко остаются IndexedDB, закэшированные ответы API, старый service worker и ключи в localStorage от предыдущего аккаунта. Clear-Site-Data закрывает эти проблемы полностью: не нужно перечислять в JavaScript каждое хранилище и гадать, не забыли ли что-то ещё.

Если часть аудитории застряла на старом бандле или сломанном кэше, на отдельной recovery-странице можно отдавать, например, "cache" и "storage", чтобы быстро вернуть клиент в консистентное состояние, особенно если подозреваете конфликт версий у service worker.

2. contain-intrinsic-size

contain-intrinsic-size — это сокращение для пары contain-intrinsic-width и contain-intrinsic-height. Оно задаёт размер, который браузер подставляет в раскладку, когда на элементе действует size containment (contain: size, content-visibility: auto / hidden и т.п.): движок вправе считать блок «как будто» фиксированного размера без полного пересчёта детей — ради меньшего числа reflow и более быстрого первого кадра.

Без явного внутреннего размера контейнер с удержанием размеров может вести себя так, словно внутри пусто — полоса прокрутки и позиция скролла начинают «плавать», когда невидимый блок внезапно получает реальную высоту. contain-intrinsic-size как раз отвечает на вопрос: какую площадь зарезервировать, пока содержимое не посчитано или намеренно пропущено.

Синтаксис

/* нет подставляемого размера по осям */
contain-intrinsic-size: none;

/* одно число — и ширина, и высота */
contain-intrinsic-size: 1000px;

/* ширина | высота */
contain-intrinsic-size: 300px 200px;

/* auto + запасной размер (часто для content-visibility: auto) */
contain-intrinsic-size: auto 320px;

/* отдельно для ширины и высоты */
contain-intrinsic-size: auto 400px auto 250px;

/* auto none — без запомнённого размера ведём себя как none, а не как 0px */
contain-intrinsic-size: auto none;

Значения:

  • none — в данном измерении нет подставляемого внутреннего размера.

  • <length> — явная ширина и/или высота для расчётов layout при containment.

  • auto <length> или auto none — пока элемент пропускает отрисовку содержимого (например, вне экрана с content-visibility: auto), браузер может использовать запомненный размер после того, как блок уже хотя бы раз отрисовали «по-настоящему»; если памяти нет — берётся <length> или поведение none (для auto none).

  • Одно значение (ключевое слово, длина или пара auto …) распространяется на обе оси. Две длины или две пары auto … — это ширина, затем высота.

Примеры

Длинная лента: placeholder по высоте + возможность «запомнить» фактическую высоту после первого показа.

.feed-item {
  content-visibility: auto;
  contain-intrinsic-size: auto 280px;
}

Карточка с тяжёлой внутренней вёрсткой вне viewport:

.product-card {
  content-visibility: auto;
  contain-intrinsic-size: auto 360px;
}

Практика

Без contain-intrinsic-size блок с content-visibility: auto вне экрана может дать нулевую или слишком малую оценку высоты; при появлении в viewport страница пересчитывается, ползунок и якоря ведут себя непредсказуемо. Разумный fallback (auto 280px и т.д.) стабилизирует скролл; после первой отрисовки движок может опираться на сохранённый размер, не рендеря тяжёлое содержимое снова только ради замера.

3. @counter-style

@counter-style расширяет набор встроенных стилей списков и позволяет описать свой стиль счётчика, которого нет среди предопределённых. Внутри правила задаются дескрипторы: по ним браузер превращает целочисленное значение счётчика в строковое представление маркера.

В CSS уже есть много готовых стилей счётчиков, но @counter-style даёт способ собрать свой.

Синтаксис

Имя стиля — чувствительный к регистру <custom-ident> без кавычек, не равный none и не совпадающий с CSS-wide keywords. Лучше не называть стиль так же, как значения свойств списков/счётчиков, если хотите избежать путаницы.

Зарезервированы имена встроенных стилей для list-style-type: decimal, disc, square, circle, disclosure-open, disclosure-closed — их нельзя использовать как имя своего @counter-style. При этом они допустимы в других местах, где ожидается имя стиля счётчика (например, в system: extends <counter-style-name>).

@counter-style <counter-style-name> {
  /* один или несколько дескрипторов */
}

Минимальный пример:

@counter-style thumbs {
  system: cyclic;
  symbols: "\1F44D";
  suffix: " ";
}

Дескрипторы

  • system — алгоритм преобразования целого значения счётчика в строку. Если задано cyclic, numeric, alphabetic, symbolic или fixed, обычно нужен дескриптор symbols. Для additive нужен additive-symbols.

  • symbols — символы для маркеров (строки, изображения, идентификаторы).

  • additive-symbols — для аддитивных систем (как римские): пары «символ + неотрицательный вес» по убыванию веса.

  • negative — что дописать до и после представления, если значение счётчика отрицательное.

  • prefix / suffix — что добавить перед и после основного представления маркера (суффикс идёт в том числе после части, которую задаёт negative).

  • range — диапазоны значений, где стиль применим; вне диапазона используется цепочка fallback.

  • pad — минимальная длина представления (например, ведущие нули: 01, 02…).

  • speak-as — как озвучивать маркер синтезом речи (screen readers).

  • fallback — имя стиля счётчика на случай, если текущий не может построить представление или значение вне range. Если цепочка обрывается, в конце браузер падает на decimal.

Примеры

Стиль с фиксированным набором символов и применение к списку:

@counter-style circled-alpha {
  system: fixed;
  symbols: Ⓐ Ⓑ Ⓒ Ⓓ Ⓔ Ⓕ Ⓖ Ⓗ Ⓘ Ⓙ Ⓚ Ⓛ Ⓜ Ⓝ Ⓞ Ⓟ Ⓠ Ⓡ Ⓢ Ⓣ Ⓤ Ⓥ Ⓦ Ⓧ Ⓨ Ⓩ;
  suffix: " ";
}

.items {
  list-style: circled-alpha;
}

Длинный упорядоченный список с start за пределами «запаса» символов уйдёт в fallback, если вы его зададите — иначе движок разберётся по правилам выбранного system.

Собственный маркер для list-style:

@counter-style rocket {
  system: cyclic;
  symbols: "\1F680";
  suffix: " ";
}

ul.features {
  list-style: rocket;
}

Свой стиль в counter() для нумерации разделов:

@counter-style chapter {
  system: numeric;
  symbols: "0" "1" "2" "3" "4" "5" "6" "7" "8" "9";
  suffix: ". ";
}

.article {
  counter-reset: section;
}

.article h2::before {
  counter-increment: section;
  content: counter(section, chapter);
}

Практика

В документе W3C Ready-made Counter Styles собрано более сотни готовых фрагментов @counter-style под разные языки и культуры. Удобно подставлять и проверять через Counter styles converter.

Если много текстового контента, сложная типографика или нестандартная нумерация, @counter-style позволяет держать формат маркеров в CSS, а не в шаблонах и генераторах HTML. Это упрощает локализацию и снимает лишнюю логику с бэкенда — при условии, что вы заранее продумали fallback, range и озвучивание через speak-as, где это важно для доступности.

4. Device orientation events

Device orientation events — это семейство DOM-событий, через которое страница узнаёт физическую ориентацию телефона или планшета и его движение (ускорение и скорость вращения). На мобильных устройствах к этому обычно подключаются гироскоп, акселерометр и магнитометр; на десктопе возможности ограничены железом, зато для сценариев «наклони телефон — поверни карту» API как раз заточен под сенсорные устройства.

Что слушать на window

По смыслу это три потока данных:

  • deviceorientation — ориентация относительно земной системы координат, когда браузер получил свежие углы (DeviceOrientationEvent).

  • deviceorientationabsolute — то же, но в варианте абсолютной ориентации (когда агент это поддерживает).

  • devicemotion — ускорение и угловая скорость вращения с заданной частотой (DeviceMotionEvent), то есть «как резко дёрнули устройство».

У части браузеров доступ к датчикам не открывается сразу. В таких средах перед подпиской на события вызывают статические методы DeviceOrientationEvent.requestPermission() и DeviceMotionEvent.requestPermission() — и только после явного действия пользователя (например, нажатия кнопки), как описано в разделе про разрешения в документации.

Синтаксис: ориентация

window.addEventListener("deviceorientation", (event /* DeviceOrientationEvent */) => {
  console.log(event.alpha, event.beta, event.gamma, event.absolute);
});

Основные поля DeviceOrientationEvent:

  • alpha — поворот вокруг оси z (как «компас» в горизонтальной плоскости, в градусах);

  • beta — наклон вокруг оси x (от −180 до 180);

  • gamma — наклон вокруг оси y (от −90 до 90);

  • absolute — признак абсолютной привязки к системе координат (зависит от устройства и браузера).

Синтаксис: движение

window.addEventListener("devicemotion", (event /* DeviceMotionEvent */) => {
  console.log(event.acceleration, event.accelerationIncludingGravity, event.rotationRate, event.interval);
});
  • acceleration — ускорение без учёта гравитации (если доступно);

  • accelerationIncludingGravity — с гравитацией;

  • rotationRate — скорость вращения вокруг осей в градусах в секунду (DeviceMotionEventRotationRate);

  • interval — интервал между измерениями в миллисекундах.

Пример

«Наклонная» карточка по gamma / beta

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

window.addEventListener("deviceorientation", (event) => {
  const x = Math.max(-15, Math.min(15, event.gamma ?? 0));
  const y = Math.max(-15, Math.min(15, event.beta ?? 0));

  card.style.transform = `rotateY(${x}deg) rotateX(${y * -1}deg)`;
});

5. hyphenate-character

Свойство hyphenate-character задаёт строку, которую пользовательский агент показывает в конце строки непосредственно перед переносом по правилам переноса (hyphenation break). На это правило одинаково опираются и автоматические переносы (hyphens: auto), и мягкие (&shy; / U+00AD): отображаемый символ в точке разрыва берётся из текущего значения свойства.

По умолчанию действует auto: браузер подбирает подходящую строку с учётом типографических традиций языка контента. Явно задавать auto имеет смысл в основном тогда, когда нужно переопределить унаследованное значение (свойство наследуется).

Синтаксис

hyphenate-character: auto;
hyphenate-character: "=";
hyphenate-character: "—";

Значения:

  • auto — типовой символ переноса выбирает сам браузер в духе правил для текущего языка; это начальное значение.

  • <string> — произвольная строка, которую нужно показать перед разрывом вместо выбора по умолчанию.

Примеры

Узкая колонка и явный символ вместо дефиса по умолчанию:

<p lang="en">Superc&shy;alifragilisticexpialidocious</p>
p {
  max-width: 80ch;
  hyphens: auto;
  hyphenate-character: "=";
}

Для стиля издания иногда просят тире вместо дефиса:

.editorial {
  hyphens: auto;
  hyphenate-character: "—";
}

Практика

Когда макет или гайдлайн требуют конкретного знака у переноса (равно, длинное тире, другой локальный вариант), раньше оставалось либо смиряться с дефолтом движка, либо избегать переносов. Теперь вариант фиксируется в CSS и автоматически тянется по дереву за счёт наследования.

В сайдбарах, сетке карточек и «газетных» колонках замена знака переноса часто визуально связывает набор с фирменной типографикой: переносы перестают выглядеть «чужими» по отношению к остальной пунктуации интерфейса.

6. hyphens

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

Без продуманного переноса в узких колонках легко словить рваный набор, вынос за границы контейнера или широкие «провалы» при text-align: justify. С hyphenate-character (см. раздел выше) можно ещё и подобрать символ у разрыва строки, если дефис по умолчанию не подходит макету.

Синтаксис

/* Ключевые значения */
hyphens: none;
hyphens: manual;
hyphens: auto;

/* Глобальные ключевые слова */
hyphens: inherit;
hyphens: initial;
hyphens: revert;
hyphens: revert-layer;
hyphens: unset;

Значения:

  • none — внутри слова перенос не выполняется, даже если в тексте есть мягкий перенос (&shy;, U+00AD) или другие подсказки; строка ломается только по пробельным символам (вплоть до переполнения, если контейнер его не обрезает).

  • manual — начальное значение. Перенос возможен только там, где автор явно указал возможность: мягкий перенос U+00AD (&shy; в HTML) или обычный дефис U+2010 (видимый дефис, который ещё и отмечает допустимый разрыв).

  • auto — браузер сам подбирает места переноса по своим правилам для отмеченного языка; при этом явно заданные точки (&shy;, U+2010) имеют приоритет над автоматикой, если они есть.

Важно для auto: чтобы гарантированно подтянулись нужные словари и правила, у контента должен быть корректный lang в HTML. Иначе автоматический перенос может не сработать или работать непредсказуемо.

Примеры

Ручные переносы при manual (или при auto, где явные &shy; переопределяют алгоритм):

<p lang="ru" class="copy-manual">
  Электри&shy;фи&shy;ка&shy;ция в узкой колонке — переносы только по мягким точкам.
</p>
.copy-manual {
  hyphens: manual;
}

Автоматический перенос длинного слова без разметки внутри:

<p lang="en" class="copy-auto">
  Supercalifragilisticexpialidocious
</p>
.copy-auto {
  max-width: 7ch;
  hyphens: auto;
}

Практика

hyphens: auto особенно уместен там, где ширина ограничена:

  • карточки товаров и подписи;

  • ячейки таблиц с длинными терминами;

  • мобильные статьи и документация;

  • «книжные» колонки и режимы чтения.

Нюансы в связке с другой типографикой

  • При word-break: break-all глиф переноса не рисуется, даже если разрыв произошёл «в логической точке» переноса — это отдельный режим разбиения, не классическая типографская расстановка дефисов.

  • Разрыв по элементу <wbr> не добавляет дефис в конец строки — в отличие от &shy; и от автоматического переноса при auto.

7. image-set()

Функция image-set() задаёт несколько вариантов одного и того же изображения (URL, градиент и т.п.) и передаёт выбор подходящего варианта агенту пользователя. В первую очередь это нужно для экранов с высокой плотностью пикселей: браузер может взять 2x-версию там, где это уместно, и не тянуть лишние байты там, где нет.

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

Синтаксис

Общая идея — список кандидатов через запятую:

/* по плотности пикселей */
background-image: image-set(
  url("image-1x.jpg") 1x,
  url("image-2x.jpg") 2x
);

/* строка вместо url() допустима */
background-image: image-set(
  "tile.png" 1x,
  "tile@2x.png" 2x
);

/* по поддерживаемому формату */
background-image: image-set(
  url("photo.avif") type("image/avif"),
  url("photo.jpg") type("image/jpeg")
);

Для каждого кандидата задаётся:

  • Сама картинка — url(...), строка с путём или другой допустимый <image> (в том числе градиент), но не вложенный image-set() — вложение запрещено.

  • Опционально дескриптор разрешения — 1x, 2x, единицы dppx, dpi, dpcm. Если дескрипторы разрешения используются, у каждого кандидата значение должно быть уникальным — два варианта с одним и тем же 1x в одном наборе корректны не будут.

  • Опционально type("mime/type") — подсказка о формате; браузер может выбрать первый поддерживаемый вариант.

Дескриптор type() и разрешение можно комбинировать в одном кандидате.

Примеры

Retina-фон:

.hero {
  background-repeat: no-repeat;
  background-size: cover;
  background-image: image-set(
    url("/images/hero-400.jpg") 1x,
    url("/images/hero-800.jpg") 2x
  );
}

Приоритет современного формата с фолбэком (без дублирования одного и того же дескриптора разрешения — только type()):

.promo {
  background-repeat: no-repeat;
  background-size: cover;
  background-image: image-set(
    url("/images/promo.avif") type("image/avif"),
    url("/images/promo.webp") type("image/webp"),
    url("/images/promo.jpg") type("image/jpeg")
  );
}

Практика

Раньше адаптив под высокую плотность и «умные» форматы тяготел к <img srcset sizes> и картинкам в разметке, а фоновые и декоративные изображения часто оставались одним файлом «на всех». С image-set() тот же подход переносится в CSS: один раз описали набор — дальше выбор делает браузер.

Фон через image-set() — по-прежнему фон: скринридеры не озвучивают содержимое фона. Если от картинки зависит смысл страницы, её нужно дать alt в <img>, а не полагаться только на background-image.

8. <link rel="modulepreload">

`<link rel="modulepreload">` даёт декларативный способ заранее подтянуть модульный скрипт: скачать, распарсить, скомпилировать и положить результат в module map документа, чтобы позже выполнить его без лишней задержки. По смыслу это ближе всего к rel="preload", но для ES-модулей подготовка глубже, чем просто попасть в HTTP-кэш.

Раньше при <script type="module" src="main.js"> цепочка часто выглядела так: сначала грузится entry, уже из него браузер узнаёт статические import и последовательно дотягивает зависимости. С modulepreload вы можете параллельно начать загрузку entry и заранее известных зависимостей — и меньше ждать на этапе «обнаружил импорт → пошёл в сеть».

Синтаксис

Минимальный вариант:

<link rel="modulepreload" href="/app.js">

Обычно рядом остаётся сам модульный скрипт:

<link rel="modulepreload" href="/assets/app.js">
<script type="module" src="/assets/app.js"></script>

Отличие от rel="preload"

  • preload — в первую очередь скачает ресурс и удержит его в кэше; выполнение/компиляция модуля этим не гарантируется так же, как при modulepreload.

  • modulepreload — скачает, распарсит, скомпилирует и положит результат в module map, чтобы при фактическом import или <script type="module"> работа шла от уже подготовленного модуля.

У modulepreload режим запроса всегда cors. Атрибут crossorigin задаёт, уходят ли учётные данные (куки и т.п.): при anonymous или пустом значении по умолчанию credential mode ведёт себя как same-origin для отправки кук; при use-credentialsinclude, и учётные данные могут уйти и на кросс-ориджин (подробнее документации).

Атрибут as

Для rel="modulepreload" атрибут as необязателен и по умолчанию равен "script". Допустимы в том числе "style", "json" и «скриптоподобные» назначения вроде "audioworklet", "paintworklet", "serviceworker", "sharedworker", "worker". Если указать другое назначение, на элементе может сработать событие error.

Зависимости модулей

Браузер может (по своему усмотрению) дополнительно запросить зависимости указанного модуля — это оптимизация на стороне движка, а не жёсткая гарантия для всех браузеров. Чтобы везде попытаться заранее подтянуть цепочку, явно перечисляют каждый нужный URL отдельным <link rel="modulepreload" …>. События load и error на элементе относятся к тому ресурсу, который указали в href; автоматически подтянутые зависимости отдельных событий на главном потоке не порождают (мониторинг — через DevTools, service worker или логи на сервере).

Примеры

Только entry — уже убирает часть водопада до первого исполнения:

<head>
  <meta charset="utf-8" />
  <title>Приложение</title>
  <link rel="modulepreload" href="/assets/main.js" />
  <script type="module" src="/assets/main.js"></script>
</head>

Entry и известные статические зависимости (main.js тянет modules/canvas.js и modules/square.js) — все три запроса стартуют раньше, чем парсер «дойдёт» до импортов внутри main.js:

<head>
  <meta charset="utf-8" />
  <title>Модули</title>
  <link rel="modulepreload" href="main.js" />
  <link rel="modulepreload" href="modules/canvas.js" />
  <link rel="modulepreload" href="modules/square.js" />
  <script type="module" src="main.js"></script>
</head>

Практика

Если у вас React, Vue, Svelte или просто крупный бандл как набор ES-модулей, modulepreload — способ выиграть время без дополнительного JS в шаблоне: перечислили критичный entry и «широкие» статические зависимости, которые и так попадут в первый граф загрузки. Имеет смысл держать в прелоаде короткий список: то, без чего первый экран или главный сценарий не оживут.

Слишком агрессивный modulepreload конкурирует с LCP, шрифтами и тяжёлыми API вроде предзагрузки изображений. Держите список на уровне измеренных узких мест (например, один entry и 2–3 самых толстых чанка до гидратации), а остальное оставьте обычному графу import и кэшу — так вы реже «загоните» пользователя в очередь запросов без выигрыша по UX.

9. Overflow media queries

Медиавозможности overflow-blockи overflow-inline отвечают на вопрос, как устройство вывода обычно обращается с контентом, который не помещается в исходный содержащий блок — соответственно по блочной или по инлайновой оси. Это не проверка «есть ли сейчас полоса прокрутки у моего div»: признак описывает модель переполнения самого отображающего устройства, а не конкретного элемента в DOM.

overflow-block смотрит на блочную ось, overflow-inline — на инлайновую. При привычной горизонтальной письменности это чаще всего вертикаль и горизонталь.

overflow-block

  • none — контент, выходящий за пределы по блочной оси, не отображается.

  • scroll — такой контент можно увидеть, прокрутив к нему.

  • optional-paged — контент по-прежнему доступен через прокрутку, но пользователь или разметка могут инициировать разрыв страницы (в том числе через свойства вроде break-inside), после чего продолжение оказывается на следующей странице.

  • paged — вывод постраничный: то, что не помещается на текущей странице по блочной оси, переносится на следующую.

overflow-inline

Здесь набор короче — только none и scroll с той же логикой, но относительно инлайновой оси (на экранах чаще горизонтальная прокрутка).

Синтаксис

@media (overflow-block: scroll) {
  /* Среда, где по блочной оси принято прокручивать */
}

@media (overflow-inline: scroll) {
  /* Среда с прокруткой переполнения вдоль строки */
}

Подсветка параграфа, когда среда объявлена как scroll по нужной оси:

@media (overflow-block: scroll) {
  p {
    color: red;
  }
}

Практика

Если среда ведёт себя как прокручиваемый экран, разумно опираться на scroll-контейнеры и ограничения по высоте окна:

@media (overflow-block: scroll) {
  .sheet {
    max-height: 100vh;
    overflow: auto;
  }
}

Если же модель вывода постраничная (paged), важнее думать о разрывах страниц и о том, что блоки не рвутся посредине:

@media (overflow-block: paged) {
  .sheet {
    break-inside: avoid;
  }
}

10. Storage manager

Интерфейс StorageManager — часть Storage API: через него браузер даёт странице оценить занятость и квоту хранилища для текущего origin, запросить «постоянное» хранение (чтобы данные реже вытеснялись при нехватке места) и проверить, выдано ли такое право уже сейчас. Дополнительно тот же объект открывает доступ к корню origin private file system— директории, недоступной произвольным путям в файловой системе пользователя, но удобной для крупных офлайн-данных.

Раньше о том, «сколько ещё можно писать в IndexedDB», приходилось гадать по косвенным признакам или ловить ошибки записи. Теперь estimate() возвращает числа в байтах; persist() и persisted() — явный слой над политикой вытеснения, без которого PWA и тяжёлые офлайн-сценарии часто вели себя непредсказуемо.

Методы

  • estimate()Promise с объектом usage и quota для origin (оценка примерная, браузер может округлять и не учитывать всё подряд).

  • persist()Promise<boolean>: true, если агент может сделать хранилище более устойчивым к очистке (решение остаётся за политикой браузера и, при необходимости, за пользователем).

  • persisted()Promise<boolean>: уже ли выдан режим персистентности.

  • getDirectory()Promise<FileSystemDirectoryHandle> на корень OPFS; имеет смысл, когда нужны файлы в приватной песочнице origin, а не только IndexedDB и Cache Storage.

Синтаксис

Минимальная оценка запаса места:

const estimate = await navigator.storage.estimate();

console.log(estimate.usage); // занято, байт
console.log(estimate.quota); // ориентир квоты, байт

Примеры

Оценка объёма перед офлайн-кэшем

const { usage, quota } = await navigator.storage.estimate();

if (quota && usage / quota > 0.8) {
  console.warn("Почти закончилось место в локальном хранилище");
}

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

const isPersisted = await navigator.storage.persisted();

if (!isPersisted) {
  await navigator.storage.persist();
}

Умные стратегии кэширования: вместо модели «кэшируем всё, а там посмотрим» можно опираться на estimate() и политику persist:

  • не скачивать тяжёлый офлайн-пакет, если usage / quota уже высокий;

  • заранее чистить второстепенные данные при приближении к лимиту;

  • вызывать persist() там, где потеря локальных данных действительно критична (заметки, черновики, карты), а не «на всякий случай» на каждой витрине.

Для приложений вроде редактора или медиатеки getDirectory() + OPFS часто удобнее, чем гнать многомегабайтные блобы в одну только IndexedDB — но это уже отдельная история про File System API.

11. Update frequency media query

Медиавозможность update в @media позволяет проверить, как часто (и можно ли вообще) устройство вывода способно менять внешний вид контента после того, как он уже отрисован. Это не про FPS монитора в лоб, а про класс носителя: печать, e-ink, слабый экран или обычный дисплей.

На десктопе и в смартфоне чаще всего попадёте в fast, зато стоит включить печать, ридеры или экзотические терминалы — и запрос начинает отрабатывать осмысленно. Подробности и таблицу совместимости — в документации по update.

Синтаксис

@media (update: none) { ... }
@media (update: slow) { ... }
@media (update: fast) { ... }

Значения:

  • none — после первоначального вывода раскладку уже нельзя обновить: типичный пример — документ на бумаге (печать), где «экран» не меняется.

  • slow — страница может перестраиваться по обычным правилам CSS, но устройство не успевает отображать изменения так, чтобы они выглядели плавной анимацией; в типовых примерах из документации — электронные книги и сильно ограниченные по скорости устройства.

  • fast — динамические изменения по правилам CSS поддерживаются, носитель не аномально медленный, так что уместны регулярно обновляемые эффекты вроде CSS-анимаций.

Примеры

Анимация включается только там, где носитель считается «быстрым».

@keyframes jiggle {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(25px);
  }
}

@media (update: fast) {
  p {
    animation: 1s jiggle linear alternate infinite;
  }
}

Отключить движение там, где обновление есть, но медленное — чтобы не получить смазывание и лишнее моргание:

@media (update: slow) {
  * {
    animation-duration: 0s !important;
    transition-duration: 0s !important;
  }
}

12. subgrid

Вложенный(sub) grid не задаёт собственный список треков, а берёт те же дорожки, что уже определены у родителя, в пределах области, которую занимает этот элемент.

Когда вы вешаете display: grid на контейнер, grid-элементами становятся только прямые дети. Их потомки идут в обычном потоке — если их не вывести в отдельный grid. Вкладывать сетки друг в друга давно можно, но вложенный grid по умолчанию независим: его колонки и строки не привязаны к родительской сетке. Отсюда классическая боль: карточки в каталоге, формы с подписями — всё это приходилось ломать об дополнительные обёртки, «магические» размеры или скрипты. subgrid даёт возможность внутренние элементы разместить на линиях той же сетки, что и снаружи.

Синтаксис

Вложенный элемент должен быть grid-контейнером; вместо repeat(...) или списка треков указываете subgrid по нужной оси (или по обеим):

.child {
  display: grid;
  grid-template-columns: subgrid; /* колонки — как у родителя, сколько «перекрывает» этот блок */
  /* grid-template-rows: subgrid; — по желанию */
}

Число колонок/строк у subgrid не задаётся вручную: оно равно числу перекрытых родительских треков. Если элемент в родителе тянется с колонки 2 по 7, вложенная сетка по колонкам получит пять дорожек тех же размеров.

Нумерация линий и имена

Номера линий внутри subgrid начинаются с 1 заново — они не «наследуют» глобальные номера родителя. Зато это удобно для переиспользуемых компонентов: разметка внутри блока всегда с grid-column: 2 / 5, а сам блок вы ставите куда угодно на внешней сетке.

Имена линий родителя доступны для позиционирования внутри subgrid. Дополнительно можно объявить свои имена после ключевого слова, в квадратных скобках — они добавляются к родительским:

grid-template-columns: subgrid [a] [b] [c];

Зазоры (gap)

gap, column-gap, row-gap с родителя передаются во вложенную сетку с subgrid, чтобы промежутки совпадали. На subgrid-контейнере вы можете переопределить зазор — например, row-gap: 0, если нужно вернуть часть пространства из промежутка между строками родителя.

Примеры

Карточка в каталоге повторяет колонки внешней сетки:

.catalog {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 24px;
}

.card {
  grid-column: span 4;
  display: grid;
  grid-template-columns: subgrid;
}

Блок полей формы тянется на всю ширину родителя и делит те же две колонки (подпись / поле):

.form {
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: 12px 16px;
}

.field {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
}

Практика

С subgrid типовые сценарии — карточки с общей сеткой заголовков и метаданных, сложные формы, редакционные макеты — можно собирать без дублирования чисел колонок и без синхронизации ширин через JavaScript. По ощущениям это тот же вложенный grid: переключиться обратно на самостоятельные строки (убрав subgrid с grid-template-rows) несложно, если понадобилась неявная сетка по строкам. Учтите и обратную сторону: при minmax и контенте, который влияет на размер, треки могут расти — в том числе с учётом содержимого внутри subgrid и для родительской сетки.

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


В следующем месяце будет всего четыре новые фичи. До встречи в мае.

Скрытый текст

Привет. Я также пишу про CSS-спецификации простым языком. Веду фронтенд дайджест. Ежедневно исследую CSS. Создаю инструменты. Об этом всём я пишу(пока что) в телеграм канале, блоге и других ресурсах. Телеграм является входной точкой. Там без ереси, только код и живые встречи - https://t.me/greatAttractorCode