Кто должен контролировать поведение интерфейса? Выбор между HTML, CSS и JavaScript
Критерии для определения того, когда встроенные функции HTML и CSS, такие как диалоговые окна, поп-апы, :has() и запросы к контейнерам, должны заменять JavaScript, а когда — нет.
Источники: Not Everything Needs JavaScript Anymore
Разработчики фронтенда инстинктивно обращаются к JavaScript. Нужно открыть меню — добавляется состояние. Необходимо обработать клик — привязывается слушатель событий. Компонент должен адаптироваться под свою ширину — измеряется её размер; подсказка должна находиться рядом с кнопкой — считаются координаты; требуется эффект прокрутки — слушаются события прокрутки и обновляются стили. Современные фреймворки упрощают всё это: у React есть состояние и эффекты, у Angular — сигналы, у Vue — refs и реактивность, а Svelte позволяет делать обновления крайне лаконично. В этом нет ничего плохого, и JavaScript во многом способствовал тому, что браузер превратился в платформу для приложений, а не только в просмотрщик документов. Однако HTML и CSS постоянно приобретают новые возможности, которые перекрываются с этой работой. В данной статье объясняется, почему это происходит, рассматриваются встроенные примитивы, покрывающие теперь распространённые интеракции, и приводится практическое правило для определения того, какой слой должен управлять данным элементом.
элемент поведения.
Почему платформа продолжает брать на себя функции приложений
Если JavaScript уже может решить почти любую проблему интерфейса, зачем платформе продолжать развиваться? Не потому, что JavaScript не справляется. Дело в том, что браузер постепенно учится решать проблемы, которые раньше каждое приложение должно было решать самостоятельно. Каждый раз, когда это происходит, у разработчиков появляется ещё один вариант, и становится более интересным вопрос о том, какой вариант принесёт наименьшие долгосрочные затраты.
Каждое изменение конкурирует за следующую кадровую процедуру
Полезно вспомнить, что делает браузер после изменения чего-либо на странице. Упрощённая цепочка отрисовки выглядит следующим образом:
JavaScript
↓
Style calculation
↓
Layout
↓
Paint
↓
Composite
Реальные движки гораздо более сложны. Не каждая обновление проходит через все этапы — некоторые операции выполняются в отдельном потоке, композиция может происходить независимо, а некоторые анимации избегают многократного изменения макета или отрисовки. Тем не менее одна идея из этой упрощенной модели остается актуальной: у браузера есть ограниченное время для создания каждой кадра. Выполнение скриптов, обработка ввода, расчет стилей и отрисовка должны делить это время между собой. Чем больше задач накопляется, тем дольше браузеру требуется время на реакцию. Чтобы узнать, сколько времени занимает каждый тип изменения, ознакомьтесь с нашим обзором процессов рифлоу, репейнта и композиции.
Это легко упустить из виду на разработочной машине с быстрым процессором, большим объемом памяти и стабильным соединением. Запустите ту же приложение на более старом телефоне в режиме экономии заряда с несколькими открытыми приложениями — тогда мелкие задержки становятся заметными: анимация пропускает кадры, прокрутка становится медленной, кнопки реагируют с такой задержкой, что кажется, будто они не работают.
Работа над производительностью заключается не только в ускорении существующего кода. Часто лучшей оптимизацией является отказ от создания того, что браузер вообще не требовал от вас создавать.
Речь идет о ответственности, а не о противостоянии HTML и JavaScript
Было бы легко свести этот разговор к слогану:
HTML + CSS = good
JavaScript = bad
Этот слоган — чушь. JavaScript решает проблемы, для которых HTML и CSS изначально не предназначались. Совместная редактирование зависит от логики приложения, а дашборды в реальном времени — от синхронизации данных. Сложные функции перетаскивания элементов, правила домена, интерактивные графики, машины состояний и многоэтапные рабочие процессы не исчезнут только потому, что появился новый селектор.
Случаи, которые стоит пересмотреть, гораздо проще. Предположим, вам нужно показывать и скрывать дополнительную информацию. Для этого можно использовать скрипт:
button.addEventListener('click', () => {
panel.hidden = !panel.hidden;
});
Это совершенно корректный код. Однако HTML позволяет напрямую создавать элементы для отображения информации. Обратите внимание, что случайный символ h после закрывающего тега в примере — это опечатка, его следует убрать:
<details>
<summary>Advanced settings</summary>
<p>...</p>
</details>h
Настоящая разница не в количестве строк. Она заключается в том, кто контролирует поведение. При использовании скрипта взаимосвязь выглядит следующим образом:
Application owns the behavior
С использованием <details> всё выглядит так:
Browser owns the behavior
Когда браузер определяет поведение элемента, он также определяет все детали, которые иначе пришлось бы запоминать: поддержку клавиатуры, состояние раскрытия элемента для использования с технологиями вспомогательного доступа, а также единообразие поведения на разных страницах. Это различие имеет большое значение, чем кажется на первый взгляд.
Почему HTML не остановился на абзацах и формах
В принципе, HTML мог бы давно прекратить развитие. Заголовки, абзацы, формы, кнопки, ссылки, изображения и универсальные контейнеры были достаточны для создания чего угодно, а всё более сложное оставалось на усмотрение скриптов.
Но подумайте, как выглядит такой мир. Каждое приложение заново создаёт поведение всех элементов отображения информации, диалоговых окон, подвсплывающих элементов и форм, причём каждый раз немного по-разному. Библиотеки, кажется, нормализуют эти шаблоны, фреймворки оборачивают библиотеки, и каждый пользователь скачивает ещё одну реализацию того, что браузер мог бы понимать нативно.
Как только какой-либо шаблон появляется повсюду, встроенный примитив начинает окупаться. С такой точки зрения современный HTML не превращается в фреймворк приложений; он постепенно кодирует взаимодействия, которые практически каждое приложение постоянно перестраивает заново.
Подвсплывающие элементы без состояния приложения
Возьмём небольшую панель настроек. Версия компонента обычно начинается с примитива состояния, такого как сигнал в этом примере или его аналог в другом фреймворке:
const open = signal(false);
Затем начинают накапливаться требования:
button click
↓
change state
↓
show overlay
↓
handle outside click
↓
handle Escape
↓
manage stacking
Иногда продукту действительно требуется всё это. Для более простых случаев HTML теперь предлагает API Popover. Кнопка указывает на элемент с атрибутом popover через popovertarget, а браузер обрабатывает включение и выключение поп-упа:
<button popovertarget="settings">
Settings
</button>
<div id="settings" popover>
...
</div>
При стандартном поведении popover (auto) браузер также заботится о простом закрытии поп-упа при нажатии пользователя вне него или клавиши Escape, а также отображает его на верхнем уровне, поэтому не нужно бороться с параметром z-index. Поведение, которое раньше полностью находилось в коде приложения, теперь имеет нативную основу.
Это не значит, что нельзя вообще создавать пользовательские выпадающие списки. Автодополнение с удаленным поиском, виртуализацией, возможностью выбора нескольких элементов и пользовательской обработкой клавиатуры — это совершенно другая проблема. Суть в том, что простой поп-овер больше не обязан начинать свою работу как часть состояния приложения.
Диалоговые окна, создаваемые на основе реального элемента
Модалки являются хорошим примером, потому что они кажутся простыми до тех пор, пока их не создают должным образом. Первая версия состоит всего из одного флага:
open = true;
А затем возникают настоящие вопросы:
- Что происходит с содержимым, находящимся за модальным окном?
- Куда переходит фокус при его открытии?
- Можно ли закрыть его с помощью клавиатуры?
- Что происходит и куда возвращается фокус при его закрытии?
- Отображается ли оно над всем остальным, на верхнем уровне?
- Каким должен быть фон и какие функции у него быть?
В течение многих лет многие реализации начинались с обычного элемента <div>, и вся структура создавалась вручную. Сегодня HTML предоставляет специальный элемент для этой цели:
<dialog>
...
</dialog>
Стоит знать один практически важный момент: модальное поведение достигается путем открытия элемента с помощью метода showModal(). Этот вызов размещает диалоговое окно на верхнем уровне, делает остальную часть страницы недоступной для взаимодействия, позволяет закрыть окно с помощью клавиши Escape и предоставляет псевдоэлемент ::backdrop для стилизации. Открытие элемента с помощью метода show() или атрибута open вместо этого приводит к созданию не модального диалогового окна.
Нативный элемент не решает всех требований к модальным окнам, и в реальных приложениях всё равно может потребоваться дополнительная логика. Однако отправной точкой становится нечто иное: вам больше не нужно изобретать концепцию диалогового окна, поскольку браузер уже знает, что это такое. Это отражает общую тенденцию современных платформ: нативные примитивы не удаляют прикладную логику; они сокращают объём кода, который необходимо написать самостоятельно.
CSS, реагирующий на состояние DOM
HTML — не единственный слой, который берёт на себя задачи. Теперь CSS может выражать взаимосвязи, для которых раньше требовались скрипты.
Рассмотрим поле формы, стили которого должны меняться, когда введённые данные неверны. Скрипт мог бы проверять данные и синхронизировать класс на родительском элементе. Современный CSS часто позволяет напрямую описать такие взаимосвязи:
.field:has(input:invalid) {
/* invalid state */
}
:has() меняет то, что CSS может знать о документе. Его давно просили как недостающий селектор родителя, но архитектурные последствия важнее самого названия: теперь некоторая синхронизация состояния вовсе не обязательна проходить через JavaScript. DOM уже содержит эту информацию, и CSS может на неё реагировать. Нет классов, которые нужно забыть обновить, и нет слушателей, которые требуют очистки.
Компоненты, реагирующие на свою собственную область
В традиционном дизайне, адаптивном к разным устройствам, использовались медиа-запросы:
@media (max-width: 700px) {
...
}
Это хорошо работает, когда важен видимый экран. Однако компоненты часто интересуются чем-то другим. Представьте ту же карточку, отображаемую в двух местах:
Main content
┌───────────────────────────────┐
│ Card │
└───────────────────────────────┘
Sidebar
┌─────────────┐
│ Card │
└─────────────┘
В обоих случаях видимое пространство одинаково; пространство, доступное карточке, — нет. В прошлом системы переиспользуемых компонентов решали эту проблему с помощью кода для измерения: отслеживали элемент, читали его ширину, добавляли класс и повторяли процедуру при любых изменениях макета.
Запросы контейнеров позволяют CSS отвечать на реальный вопрос — а именно, сколько места фактически имеет данный компонент. Вы обозначаете предка как контейнер и задаёте правила, связанные с его размерами:
.card-container {
container-type: inline-size;
}
@container (width < 400px) {
.card {
/* compact layout */
}
}
Это не просто новая синтаксис для адаптивности; это перераспределение ответственностей. Браузер уже вычислил макет, и CSS может использовать этот результат без необходимости повторного измерения со стороны прикладного кода. В нашем сравнении запросов контейнеров и медиа-запросов рассматривается, в каких случаях каждый из них подходит для использования в системе дизайна.
Позиционирование показывает, как развивается платформа
Добавление подсказки к кнопке кажется простой задачей, пока вы её не попробуете на самом деле. Где именно находится кнопка? Какого размера подсказка? Есть ли место снизу или её следует отобразить сверху? Что происходит при прокрутке, изменении размера области отображения или когда какая-то другая часть макета смещает кнопку? Простая подсказка быстро превращается в геометрическую задачу.
Именно поэтому существуют специализированные библиотеки для плавающего интерфейса, которые хорошо решают действительно сложные проблемы. В CSS теперь появилось позиционирование через Anchor, которое позволяет задавать положение элемента относительно другого элемента декларативным образом. На момент написания степень поддержки браузерами этой функции была неравномерной, поэтому проверьте актуальные данные о совместимости и используйте резервный вариант, если решите её применить.
Самое важное — это не то, заменит ли CSS все библиотеки для позиционирования сразу. Важно то, что платформа признала позиционирование настолько распространённым, что оно заслуживает примитивов низкого уровня. Таков цикл развития платформы: проблемы возникают в приложениях, повторяются шаблоны, появляются библиотеки, и в конечном итоге платформа учится на них.
Анимация и ценность декларативных API
В области анимации вопрос производительности становится более сложным. Распространённое упрощение гласит, что анимации CSS выполняются на GPU, а анимации JavaScript — на CPU. На самом деле всё гораздо сложнее: анимации CSS тоже требуют вычислительных ресурсов, JavaScript может создавать очень плавные анимации, а затраты на обработку различных свойств CSS сильно разнятся. Анимация размеров может вызвать изменение макета, другие свойства требуют перерисовки, в то время как transform и opacity обычно лучше справляются с задачами композитора.
Поэтому полезное различие заключается не в CSS против JavaScript, а в том, сколько вычислительных ресурсов требуется для каждого изменения и насколько свободы вы даёте браузеру для планирования этих операций.
Анимации, запускаемые при прокрутке, иллюстрируют эту смену. Традиционно эффект прокрутки реализовывался с использованием такого цикла:
scroll event
↓
JavaScript calculation
↓
update styles
↓
repeat
Теперь некоторые из этих связей можно объявлять напрямую движку отрисовки. Вместо того чтобы постоянно просить браузер вычислять значение, применять его и делать это снова, вы описываете, что анимация развивается вдоль временной шкалы прокрутки. Это совершенно другая категория API, в которой браузер контролирует большую часть механизма и может его оптимизировать. Как и в случае с позиционированием анкоров, проверьте поддержку в браузерах, которые вы используете.
Удаление JavaScript уменьшает не только объем данных
Сокращение использования JavaScript обычно рассматривается в килобайтах, и это важно, потому что каждый скрипт должен пройти через эту процедуру перед тем, как сможет выполнять какие-либо полезные действия:
downloaded
↓
parsed
↓
compiled
↓
executed
Но жизненный цикл кода приложения продолжается долго после его первого запуска. Поведение, реализованное с помощью JavaScript, как правило, влечет за собой:
state
event listeners
synchronization
cleanup
framework lifecycle
hydration
tests
Теперь представьте, что нативный примитив позволяет удалить эту комбинацию:
state
+
outside-click listener
+
Escape handler
+
DOM synchronization
Удалённый код может весить всего несколько сотен байт, но архитектурные изменения реальны. Ваше приложение больше не поддерживает такое поведение. Становится на одну меньше переменных состояния, которые могут устареть, на одного меньше слушателей, о которых кто-то может забыть убрать, на одну меньше связей DOM, которые должны синхронизироваться со состоянием, и на одну меньше операций восстановления с нуля, часто с худшей доступностью по сравнению с нативной версией. Часто самой большой выгодой от функционала платформы является не скорость, а ответственность, которую вы возвращаете браузеру.
У платформы нет окончательной версии
Одно распространённое заблуждение помогает объяснить, почему разработчики упускают эти изменения: речь идёт об «HTML5», как будто HTML достиг пятой версии и затем практически остановился. Это не так. Никакого HTML6 в разработке нет, и ждать HTML7 тоже нечего. HTML поддерживается как живой стандарт, и спецификация постоянно развивается.
CSS следует похожей модели. Многие разработчики изучали «CSS3», но теперь CSS поставляется не как один большой версионный пакет. Отдельные модули развиваются в собственном темпе, и именно поэтому вы сталкиваетесь с такими названиями, поскольку не существует какого-то общего «CSS6»:
Selectors Level 4
CSS Color Level 5
CSS Cascade Level
В примере отрезана последняя запись; модуль Cascading and Inheritance также имеет собственный номер уровня. Поскольку модули независимы, структура сетки может развиваться без ожидания селекторов, обработка цветов происходит отдельно от шрифтов, а запросы к контейнерам поступают без каких-либо предполагаемых этапов развития CSS4. В масштабах веба постепенное развитие — единственный разумный подход.
JavaScript также продолжает развиваться
То же самое касается и языка. Стандартизированная спецификация — ECMAScript; JavaScript — это название, которое разработчики используют для языка, реализуемого движками в соответствии с этой спецификацией. Именно поэтому прогресс описывается через версии ECMAScript, а не через пронумерованные выпуски «JavaScript», и новые возможности постоянно появляются.
Следовательно, платформа не застывает благодаря этой комбинации:
HTML5
+
CSS3
+
JavaScript
Все три слоя продолжают развиваться. ECMAScript получает более совершенные примитивы, CSS добавляет возможности для формирования макета, стилизации и управления взаимодействием, а HTML обретает семантику и поведение, которые раньше приходилось реализовывать самостоятельно в приложениях. Со временем границы между ними смещаются, поэтому знания о платформе, полученные много лет назад, требуют регулярного обновления.
Знания платформы остаются актуальными дольше, чем выбор фреймворка
Эти функции существуют независимо от любого фреймворка. Контейнерные запросы работают одинаково независимо от того, был ли компонент создан с использованием Angular, React, Vue, Svelte или обычного кода DOM. Функция :has() не зависит от способа создания DOM. Нативный элемент <dialog> по-прежнему остается диалоговым окном в рамках фреймворка, а элемент popover является функцией браузера независимо от того, что его отрисовало.
Это делает знания о платформе чрезвычайно долговечными. Экспертиза в определённых фреймворках переносится на проекты, использующие эти фреймворки; знания о платформе применимы практически к любому веб-проекту. Это не умаляет значимости фреймворков, которые решают серьёзные архитектурные проблемы и связанные с опытом разработчика. Понимание слоёв, лежащих в основе фреймворков, даёт дополнительный вариант действий, когда абстракция не нужна, и, что не менее важно, позволяет понять, какую пользу приносит абстракция. Если вам нужен каталог конкретных замен, наш обзор нативных функций HTML, заменяющих распространённые библиотеки интерфейса, рассматривает каждую из них по отдельности.
Когда использование нативных решений — неверный выбор
Прежде чем увлекаться, важно помнить о предосторожности: возможность создания чего-либо с помощью HTML или CSS не означает, что это обязательно нужно делать. Возьмём в пример выпадающий список, который требует:
- асинхронного поиска;
- сотен виртуализированных опций;
- возможности выбора нескольких элементов;
- сложной навигации с помощью клавиатуры;
- индивидуальных правил фильтрации;
- загрузки опций с сервера;
- синхронизации выборов с состоянием приложения.
Наличие простого выпадающего окна ничем не помогает решить эти проблемы, а попытки исказить дизайн, чтобы избежать JavaScript, только ухудшат архитектуру.
CSS также заслуживает реалистичного подхода. Он выполняет реальную работу: формирование макета и отрисовка — это не бесплатные операции, большой DOM остаётся ресурсоёмким, а плохо подобранные анимации могут всё равно работать плохо.
Следовательно, цель не в том, чтобы использовать HTML и CSS каждый раз, когда это технически возможно. Гораздо более полезным правилом является следующее:
Используйте самый низкий уровень, который эффективно решает конкретное требование.
Иногда этим уровнем является HTML, иногда CSS, иногда JavaScript, а большинство качественных приложений сочетают все три. Быстрый способ применения этого правила — последовательно задавать себе вопросы: выражает ли семантический элемент или атрибут это поведение? Если нет, может ли CSS получить его из данных, уже имеющихся в DOM? Только когда ответы на оба вопроса отрицательные или требование выходит за рамки возможностей нативной версии, следует переносить это поведение в код приложения.
Самые простые случаи требуют самых простых решений
Сложные функции будут по-прежнему требовать JavaScript, и это нормально. Однако вокруг этих функций большинство приложений содержат сотни незначительных взаимодействий: разделы, которые можно расширять, легкие надписи, карточки, адаптирующиеся под свое местоположение, стилизация недействительных полей, простые переходы, отслеживание одного элемента другим. Когда какой-либо встроенный элемент, атрибут или короткое правило CSS покрывает одно из этих взаимодействий, вашему приложению может совсем не понадобиться обрабатывать его самостоятельно.
То, что JavaScript может решить эту проблему, не является чем-то удивительным; он может решить практически любую задачу. Ценно наличие дополнительного варианта.
Обновление когнитивной модели
Многие разработчики изучали веб благодаря четкому разделению обязанностей:
HTML → structure
CSS → presentation
JavaScript → behavior
Это хорошая модель для обучения, но современные браузеры обладают гораздо большими возможностями, чем это показывает. Теперь HTML содержит информацию о поведении элементов, CSS реагирует на сложные взаимосвязи в DOM и лейауте, а сам язык постоянно получает новые функции. Платформа продолжала меняться даже тогда, когда привычки оставались неизменными.
Многие из этих привычек считались неоспоримыми истинами: для отображения надстроек требуется состояние приложения, для позиционирования — скрипты, компоненты могут реагировать только на определенные точки просмотра, анимации прокрутки требуют обработчиков событий. Все это могло быть верным когда-то, но сейчас не всегда так.
Именно поэтому следить за развитием HTML и CSS не значит просто запоминать умные синтаксические конструкции. Новая возможность браузера может избавить от необходимости в каких-либо зависимостях, обработчиках событий, состояниях, объектах ResizeObserver или любом коде, единственная функция которого заключалась в воспроизведении того, что браузер теперь может делать нативно.
Основные выводы
- Браузеры постоянно внедряют функции, которые ранее приходилось повторно реализовывать в приложениях; каждое такое внедрение предоставляет более экономичную альтернативу.
- Время обработки фреймов является ограниченным ресурсом, поэтому избегание ненужных операций часто является наиболее эффективным способом оптимизации.
- Стоит думать с точки зрения ответственности за реализацию: теги
<details>, API Popover и тег<dialog>с методомshowModal()позволяют браузеру самостоятельно управлять поведением, включая аспекты доступности, которые иначе пришлось бы обрабатывать вручную. - Селекторы
:has()и контейнерные запросы позволяют CSS реагировать на состояние DOM и размер компонентов без необходимости синхронизации или использования кода для измерений. - Позиционирование элементов и анимации, основанные на прокрутке, направлены в одном направлении, но перед тем как на них полагаться, необходимо проверить текущую поддержку в браузерах.
- Нативные решения не всегда лучше: сложные элементы интерфейса по-прежнему лучше реализовывать с помощью JavaScript, а работа с CSS также имеет свои затраты.