Головна / Статті

Хто має контролювати поведінку інтерфейсу? Вибір між HTML, CSS та JavaScript

Концепція для визначення того, коли вбудовані функції HTML та CSS, такі як dialog, popover, :has() та container queries, слід використовувати замість JavaScript, а коли — ні.

Видавець: REACTAPP.TOP15 хв читання3416 слів

Джерела: 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 Positioning, яка дозволяє декларативно позиціонувати елемент відносно іншого елемента. На момент написання підтримка цієї функції в браузерах ще була нерівномірною, тому перевірте актуальні дані про сумісність та мати запасний варіант, якщо вирішите її використовувати.

Найважливіше — це не те, чи 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, які замінюють поширені бібліотеки UI розглядає кожну з них окремо.

Коли використання нативних засобів є неправильним вибором

Перш ніж захоплюватися, варто пам’ятати: здатність створювати щось за допомогою 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 також має свої витрати.
  • Перш ніж додавати ще одну змінну стану, хук, сигнал, директиву, слухача чи залежність, спитайтеся, чи може це вже зробити сама платформа, та використовуйте найнижчий рівень, який чисто відповідає вимогам.