JSX — це не HTML: справжні компроміси, які стоять за маркуванням компонентів
Зрозумійте, що ви втрачаєте, коли маркап перетворюється на JSX — від інструментів та парсингу до форм, доступності та портативності, а також шаблони, які допомагають повернути більшу частину цього.
Майже кожен посібник з React запевняє новачків, що JSX — це „по суті HTML всередині JavaScript“. Схожість дійсна, але ці запевнення приховують низку проблем, які проявляються пізніше у процесах компіляції, розмірах пакетів, затримках у введенні даних та перевірках доступності. Нижче ви побачите, чим насправді є JSX під своєю зовнішньою формою, які можливості змінюються, коли маркування переходить від парсера браузера до середовища виконання JavaScript, та які конкретні патерни допомагають відновити більшу частину втраченого без відмови від компонентів.
Знайомий вигляд на іншій машині
JSX майже повністю запозичує формат HTML. Елементи починаються з <button>, закінчуються з </button> та вкладаються один у одного так само, як це робилося з маркуванням з 1990-х років. Саме ця знайомість значно полегшила перехід до односторінкових додатків для цілої генерації розробників:
// It looks like HTML...
function UserCard({ name, role }) {
return (
<div className="card">
<h3>{name}</h3>
<p>{role}</p>
</div>
);
}
Однак насправді це дві незалежні технології. HTML — це декларативний мовлення розмітки, яке движки браузерів обробляють безпосередньо за допомогою сильно оптимізованого нативного коду. JSX — це синтаксис, який компілятор переписує у вкладені виклики функцій JavaScript: React.createElement у класичному підході до трансформації або допоміжні функції на кшталт _jsx() у сучасному автоматичному режимі виконання. Тег <div> у компоненті зовсім не є розміткою; це просто список аргументів.
Використання JSX дає багато переваг: дерева компонентів, які керуються даними, автоматична синхронізація між станом та DOM, а також перевірка типів у шаблонах. Однак кожна абстракція має свою ціну. Заміна нативного формату браузера на шар JavaScript означає залежність від інструментів для компіляції, більше використання пам’яті під час роботи та обхід кількох функцій стійкості, які платформа надає безкоштовно. Жодна з цих витрат не є причиною уникати JSX, але про кожну з них варто знати під час розробки додатку.
Податок на інструменти
Втрата робочого процесу подвійного клацання
Перше, що зникає — це простота веб-платформи без жодних залежностей. Звичайна HTML-сторінка не потребує нічого, крім редактора та браузера. Ви можете створити index.html на офлайн-пристрої, двічі клацнути по ньому, і браузер негайно відобразить її за URL у форматі file://.
Жоден двигун JavaScript — чи то V8, JavaScriptCore чи SpiderMonkey — не розуміє <div className="box"> як коду. Перш ніж щось з’явиться на екрані, JSX потребує ланцюга компіляції:
- компілятор, такий як Babel, SWC або esbuild
- інструмент для об’єднання файлів, такий як Vite, Webpack, Rollup або Turbopack
- npm, pnpm або yarn для встановлення залежностей
- Node.js або Bun для виконання цих інструментів
- папка
node_modules, яка зазвичай містить сотні мегабайт налаштувань, парсерів, плагінів та поліфілів
(Існують версії Babel для прямого використання у браузері для швидких експериментів, але їх не використовують у серйозних проектах.) Різниця у шляху від вихідного файлу до екрана виглядає так:
HTML Workflow:
[index.html] ----------> Directly parsed by Browser Engine (Instant)
JSX Workflow:
[Component.jsx]
└─> AST Parsing
└─> Transpilation (_jsx() calls)
└─> Bundling & Minification
└─> Network Download
└─> JS Parse & Compile
└─> Runtime Virtual DOM
└─> DOM Mutation
Тягар технічного обслуговування
Пов’язування маркапу з компілятором створює постійні витрати:
- Занепад інструментального середовища. Проєкт, до якого ніхто не торкався протягом трьох років, часто відмовляється компілюватися, оскільки пакети з верхнього рівня, необхідні версії Node.js чи API-функції збирачів коду еволюціонували.
- Крихкість карти джерела. Дебагування продакшн-коду передбачає перетворення мініфікованого результату назад у початкові компоненти. Якщо карти джерела відсутні або неправильні, сліди виконання показують анонімні виклики під час роботи програми замість вашого коду.
- Затримка у зворотному зв’язку під час компіляції. Сучасні збирачі коду, написані на Rust чи Go, працюють надзвичайно швидко, проте у дуже великих кодових базах постійна перекомпіляція все одно створює затримку, якої просто немає під час редагування сирого маркапу.
Обмеження синтаксису, успадковані від JavaScript
Оскільки JSX парсується як JavaScript, він успадковує його зарезервовані слова та суворішу граматику, втрачаючи при цьому більш лояльний підхід HTML.
Зарезервовані слова стають іншими іменами властивостей
Атрибут HTML — це рядок, який приєднується до вузла DOM. Властивість JSX — це ключ у об’єкті, який передається функції. Оскільки class та for є зарезервованими словами в JavaScript, JSX використовує інші назви. Стандартна форма HTML:
<!-- Native, standards-compliant HTML -->
<label for="username">Username</label>
<div class="profile-card" tabindex="0"></div>
у JSX перетворюється на наступну. Також зверніть увагу, що tabIndex отримує числове виразне значення, а не рядок:
// JSX equivalents forced by JavaScript engine constraints
<label htmlFor="username">Username</label>
<div className="profile-card" tabIndex={0}></div>
Чутливість до регістру та об’єкти стилів
Імена атрибутів HTML нечутливі до регістру, тоді як пропси JSX чутливі до регістру та здебільшого мають формат camelCased: onClick, strokeWidth, autoComplete, tabIndex. Одне уточнення щодо поширеної думки: атрибути aria-* та data-* є винятком та зберігають свої імена з дефісами у JSX, тож aria-label пишеться точно так само, як у HTML.
Вбудовані стилі змінюються більш фундаментально. У HTML стиль — це звичайний рядок, який браузер парсить:
<!-- HTML: Zero runtime allocation -->
<div style="background-color: red; margin-top: 10px;"></div>
У JSX це об’єкт у форматі літералу JavaScript, і літерал, написаний усередині результату відображення, створюється заново щоразу, коли компонент відображається:
// JSX: Allocates a new JavaScript object on every single render pass
<div style={{ backgroundColor: 'red', marginTop: '10px' }} />
Для більшості компонентів це є мінімальним фактором. У компонентах, які дуже часто оновлюються, таких як великі таблиці даних або шари на канвасі, керовані вказівниками, тисячі короткочасних об’єктів стилів створюють навантаження на механізм збирання сміття, що може проявлятися у затримках. Уникнути цього можна, виводячи статичні об’єкти стилів за межі компонента або використовуючи імена класів.
Суворі правила закриття
HTML5 навмисно допускає елементи типу void без закриваючих слешів: <input>, <img>, <br> та <hr> є дійсними у своїй початковій формі. JSX, навпаки, дотримується правил XML. Якщо забути про самозакриваючий слеш чи закриваючу тег, компілятор зупиниться з помилкою синтаксису, тож процес будування завершиться невдачею, а не з будь-якими проблемами.
Витрати під час виконання: нативний парсинг проти віртуального DOM
Коли браузер отримує HTML, він токенізує байти по мірі їх надходження та поступово створює вузли DOM, використовуючи нативний код, який розвивався протягом десятиліть, та за допомогою сканера передзавантаження, який виявляє ресурси заздалегідь. Коли додаток відображається повністю за допомогою JSX, цей нативний шлях у більшості випадків обходиться заради виконання JavaScript:
Standard HTML Processing:
Network Bytes ──> Tokenizer ──> DOM Tree ──> Render Tree ──> Paint
JSX / Virtual DOM Processing:
Network Bytes ──> JS Parse/Compile ──> JS Execution ──> Component Tree
──> VDOM Allocation ──> VDOM Diffing (Reconciliation)
──> DOM Patching ──> Render Tree ──> Paint
Пам’ять та збір сміття
Для обчислення оновлень React зберігає опис інтерфейсу користувача в пам’яті JavaScript, який зазвичай називається віртуальним DOM. Цей процес працює приблизно так:
- Під час першого відображення створюється дерево об’єктів JavaScript, яке описує кожен елемент, його властивості та дочірні елементи.
- Коли змінюється стан, відповідні компоненти знову запускаються та створюють новий опис своєї частини дерева.
Зауважте, що React перерисовує лише піддерево під компонентом, стан якого змінився, а не всю програму, проте принцип залишається тим самим: браузер вже зберігає справжній DOM у своїй внутрішній пам’яті, а купа JavaScript також містить паралельне представлення. Це додаткове виділення пам’яті підвищує її використання та частоту збирання сміття, що особливо помітно на пристроях з низькими характеристиками.
Витрати на гідратацію
Рендеринг з боку сервера у фреймворках на кшталт Next.js чи Remix надсилає справжній HTML, тож перше відображення сторінки відбувається швидко. Однак перш ніж сторінка зможе відреагувати на вхідні дані, браузер мусить завантажити JavaScript для компонентів, які на ній знаходяться, виконати його, перебудувати внутрішню структуру React та додати обробники подій до існуючого DOM. Цей крок гідратації займає основну смугу виконання, і на важких сторінках це проявляється у високому показнику загального часу блокування (TBT) та низькому рейтингу взаємодії з моментом першого відображення (INP).
Стрімування та стійкість до помилок
HTML був розроблений на основі двох принципів, які часто ігноруються односторінковими додатками, орієнтованими на JSX: поступове стрімування та стійкість до помилок.
Коли зникає стрімування
Браузер починає відображати документ ще до його повного завантаження. Припустимо, сервер наразі надіслав <head> та 50 КБ з 200 КБ сторінки; браузер вже може запитати таблиці стилів та шрифти, а також намалювати заголовок та навігаційну панель, поки решта даних ще передається.
Додаток на JSX, який відображається виключно на клієнтській стороні, працює інакше:
- браузер отримує майже порожню структуру, яка містить лише
<div id="root"></div> - він завантажує пакет JavaScript
- він аналізує та виконує цей пакет
- компоненти запускаються, створюють DOM та нарешті відображають контент
При повільному з’єднанні 3G або слабкому телефоні користувач бачить порожню сторінку протягом усього цього процесу. Це все, з чим спочатку мусить працювати браузер:
<!-- What the browser sees initially in a standard JSX SPA -->
<!DOCTYPE html>
<html>
<head>
<title>App</title>
</head>
<body>
<div id="root"></div>
<script src="/static/bundle.8f9b2c.js"></script>
</body>
</html>
Коли помилки більше не прощаються
Парсер HTML відомий своєю толерантністю. Подайте йому пошкоджений маркап, як-от цей:
<div>
<p>Unclosed paragraph
<div>Nested incorrectly</b>
</div>
і він не зламається. Алгоритм парсингу закриває та знову вкладає елементи згідно з чітко визначеними правилами відновлення та все одно відображає контент.
React значно менш толерантний під час виконання. Якщо процес відображення зазнає помилки, наприклад тому, що вираз на кшталт {user.profile.name} намагається отримати властивість з значенням undefined, React демонтує всю структуру, якщо жоден межовий елемент для обробки помилок її не спіймає, залишаючи порожній екран. React не постачає готовий компонент <ErrorBoundary>; ви самі пишете його як класовий компонент (або використовуєте невелику бібліотеку) та розміщуєте його навколо ризикованих ділянок.
Форми та події
Форми HTML з самого початку існування вебу нативно обробляли введення даних, їх верифікацію та надсилання. Типові шаблони JSX часто замінюють ці примітивні механізми на їх реалізації за допомогою JavaScript.
Контрольовані та нативні елементи введення
Звичайний елемент <input> зберігає власний стан. При введенні даних внутрішня буферна пам’ять браузера оновлюється миттєво, без участі скриптів. У той же час типовий підхід у React робить елемент введення контрольованим, тож стан React стає джерелом істини:
// Every keystroke triggers a state change, a re-render, and a VDOM diff
function SearchInput() {
const [value, setValue] = useState("");
return (
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
Тепер кожен натиск клавіші проходить через систему подій, змінює стан, знову виконує функцію компонента, узгоджує дані та потім повертає значення назад у DOM. Це підходить для невеликого компонента. Однак коли основний потік зайнятий обробкою даних чи складною анімацією, або коли вхідний елемент знаходиться всередині великої деревоподібної структури компонентів, яка перерисовується разом з ним, користувачі можуть помітити затримку у відображенні символів після їх набору.
Синтетичні події
React оточує нативні події браузера власною системою синтетичних подій, спочатку для усунення розбіжностей між старішими браузерами. Однак ця абстракція створює певні приховані проблеми:
- поширення подій у дереві React може відрізнятися від поширення через нативні слухачі DOM, що ускладнює роботу коду, який використовує обидва підходи
Доступність та семантичний зсув
JSX може створювати абсолютно доступний, семантичний HTML. Однак патерни, які він заохочує, з часом схильні підривати семантику.
„Суп“ з div у обгортках компонентів
Компонент має повертати єдиний кореневий вузол. Фрагменти (<Fragment> або <>) дозволяють вирішити цю проблему без додаткового DOM, але багато кодових баз все ще обгортають дочірні елементи в контейнери <div> зі звички чи для формування макету. Структура, яку ви хотіли створити, виглядає так:
<!-- What you intended to build -->
<main>
<article>
<h1>Article Title</h1>
<p>Content goes here...</p>
</article>
</main>
Коли компоненти-обгортки мають кілька рівнів, вони часто відображають щось близьке до цього:
<!-- What JSX component wrapping often generates in the actual DOM -->
<div class="AppWrapper">
<div class="LayoutContainer">
<main>
<div class="ArticleWrapper">
<article>
<div class="HeadingGroup">
<h1>Article Title</h1>
</div>
<div class="ParagraphContainer">
<p>Content goes here...</p>
</div>
</article>
</div>
</main>
</div>
</div>
Додаткові шари ускладнюють структуру DOM, ускладнюють розташування елементів за допомогою CSS та створюють зайвий шум між ключовими елементами. Загальні <div>-елементи без позначень ролей здебільшого ігноруються технологіями допомоги користувачеві, але складні ланцюги обгорток все одно ускладнюють розуміння структури маркапу та сприяють помилкам, наприклад, коли обгортка випадково порушує структуру списку чи заголовка.
Функціонал клавіатури, який є безкоштовно, поки він ще працює
Вбудовані інтерактивні елементи, такі як <button>, <a>, <select> та <details>, мають функціонал, який легко вважати наявним за замовчуванням:
- за замовчуванням вони можуть бути активовані за допомогою клавіші Tab
- клавіші Enter та Space автоматично їх активують
Оскільки JSX уможливлює дуже просте приєднання обробника кліку до будь-якого елемента, як у <div onClick={handleClick}>, команди регулярно створюють власні контролери з несемантичних елементів, забуваючи про обробники клавіатури, tabIndex та ролі ARIA, які ці нативні елементи автоматично надають. Наш огляд прихованих проблем компонентів React розглядає ще більше таких пасток.
Стандарти, взаємодійність та залежність
HTML — це відкритий стандарт, який підтримується організацією WHATWG, причому W3C історично також брав участь у його розробці. Сторінки, написані у 1997 році, досі коректно відображаються у сучасних браузерах. JSX, навпаки, не є веб-стандартом; у нього є неформальна специфікація, і його підтримують кілька бібліотек, зокрема React, Preact та Solid, проте кожне його використання залежить від компілятора та середовища виконання, до якого спрямовані результати компіляції. Це менш жорстка форма прив’язки, ніж у пропрієтарних форматах, але прив’язка все одно існує.
Власні елементи та власна модель компонентів платформи
Браузери вже мають модель компонентів: Custom Elements та Shadow DOM. Звичайний HTML використовує їх безпосередньо:
<user-avatar src="avatar.jpg" size="large"></user-avatar>
React історично погано справлявся з власними елементами з двох причин:
- він передавав усі параметри невідомим маленькими літерами елементам у вигляді рядкових атрибутів, тому об’єкти та масиви не могли бути передані як властивості елемента
new CustomEvent('user-select'), не мапуються на властивість на кшталт onUserSelect, що змушує використовувати компоненти-обгортки або ручні слухачі через refsReact 19 значно покращив ситуацію, дозволяючи встановлювати властивості у спеціальних елементах, коли це визначено самим елементом, та підтримуючи обробники спеціальних подій, тому перевірте версію React, яку ви використовуєте, перш ніж припускати, що ці обмеження все ще діють.
Переносимість маркапу
Система дизайну, написана на стандартних HTML та CSS, може використовуватися будь-де: у WordPress, Django, Ruby on Rails, шаблонах Go, Vue, Angular, Svelte чи простих статичних сторінках. Система дизайну, написана у вигляді компонентів JSX, прив’язана до екосистеми JavaScript. Щоб використовувати її з не-JavaScript-бекендом, потрібна служба рендерингу Node.js або окрема реалізація кожного компонента.
HTML та JSX поруч один з одним
Підсумовуючи переваги та недоліки:
- Виконання: HTML парсується нативно браузером; JSX компілюється у виклики функцій JavaScript, які виконуються під час роботи програми.
- Інструментарій: Для HTML потрібні редактор та браузер; для JSX потрібен компілятор, інструмент збирання коду, менеджер пакетів та середовище для компіляції.
- Синтаксис: HTML нечутливий до регістру та більш толерантний; JSX чутливий до регістру, використовує перейменовані атрибути та вимагає закриття елементів у стилі XML.
- Відображення: HTML передає дані та відображає їх поступово; JSX, який відображається на клієнтській стороні, чекає на завантаження та виконання зібраного коду.
- Помилки: HTML може відновитися після пошкодженого форматування; неконтрольована помилка під час відображення призводить до розбирання дерева React.
- Форми: нативні елементи вводу зберігають свій власний стан; елементи вводу з керуванням переробляються після кожного натискання клавіші.
Відновлення втраченого
Усе це не закликає до відмови від розробки на основі компонентів. Це закликає усвідомлено вирішувати, де абстракція справді варта своїх зусиль. Екосистема рухається саме в цьому напрямку, створюючи патерни, які відновлюють швидкість та стійкість нативного HTML, зберігаючи при цьому декларативну модель створення контенту.
Виберіть рендеринг з урахуванням сервера
- React Server Components рендеруються на сервері та не надсилають JavaScript-код компонентів на клієнт для тих частин, які не є інтерактивними.
- Astro використовує архітектуру островів: сторінки за замовчуванням є статичним HTML, а інтерактивні віджети ініціалізуються лише окремо.
- Qwik замінює процес ініціалізації на можливість продовження роботи, серіалізуючи стан додатку в HTML так, щоб код виконувався лише тоді, коли користувач фактично взаємодіє з ним.
Дозвольте браузеру керувати станом форми
Замість того, щоб відтворювати кожен натискання клавіш у стан форми, нехай вбудований елемент <form> зберігає значення та читає їх один раз під час надсилання за допомогою FormData:
// Clean, native, performant HTML-first form submission
function LoginForm() {
function handleSubmit(event) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const email = data.get("email");
// Send payload...
}
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" required />
<button type="submit">Sign In</button>
</form>
);
}
Введення оновлюється з природною швидкістю, атрибут required забезпечує вбудовану перевірку, а компонент відображається лише один раз, а не з кожним натисканням клавіш. Останні версії React ґрунтуються на тій самій ідеї з формовими діями, які безпосередньо приймають FormData.
Зберігайте семантику строгою
Розглядайте JSX як засіб для створення семантичного HTML, а не як дозвіл на накопичення контейнерів:
- замінюйте обгорткові елементи
<div>на фрагменти (<></>) там, де вони не використовуються для стилізації - використовуйте вбудовані інтерактивні елементи, такі як
<button>,<dialog>,<details>та<summary>, замість самостійно створених компонентів - додайте
eslint-plugin-jsx-a11yдо процесу безперервної інтеграції, щоб відсутність міток, ролей та обробників клавіатури призводила до невдачі компіляції
Підсумок
JSX змінив розробку фронт-енду, показавши, що інтерфейси найкраще описувати як прогнозовані функції від даних, та вирішив реальні проблеми з підтримкою синхронності великих, динамічних інтерфейсів зі станом. Це все ще абстракція JavaScript, а не нова версія HTML. Вибір JSX означає жертвування нативним стрімінгом, можливістю створення контенту без складання проектів, стійкістю до помилок та довгостроковою стабільністю стандартів заради композиції елементів та реактивної ергономіки. Часто це є корисною заміною. Инженерна майстерність полягає у точному розумінні того, що було жертвувано, та у використанні серверної обробки, нативних форм та семантичних елементів там, де можна безкоштовно повернути ці можливості.
Пов’язана література
- REST проти GraphQL: Справжні компроміси, які стоять за кожною архітектурою — Пояснює конкретні проблеми, які вирішують REST та GraphQL, їхню внутрішню схему роботи та приховані компроміси, які потрібно врахувати перед вибором одного з них для вашого API.
- Десять прихованих проблем компонентів React, які уповільнюють сучасні додатки — Дізнайтеся про десять поширених помилок у компонентах React — від проблем із семантичним HTML до відсутності механізму мемоїзації — та про способи їх вирішення, щоб додатки залишалися швидкими, доступними та без помилок у 2026 році.