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 чувствительны к регистру и обычно пишутся в формате camelCase: 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 намеренно допускает элементы без закрывающих слешей: <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 демонтирует всю структуру, если никакой элемент ErrorBoundary не поймал ошибку, в результате чего появляется пустой экран. React не поставляет готовый компонент <ErrorBoundary>; его нужно написать самостоятельно в виде классового компонента (или использовать небольшую библиотеку) и разместить его специально вокруг рискованных участков кода.
Формы и события
Формы HTML с самых ранних времен веба нативно обрабатывали ввод данных, их проверку и отправку. Типичные паттерны JSX часто заменяют эти примитивы на реализации на JavaScript.
Контролируемый ввод versus нативный ввод
Обычный элемент <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. Однако любое использование JSX зависит от компилятора и среды выполнения, на которую направлено результат компиляции. Это более мягкая форма «закрепления» по сравнению с проприетарными форматами, но закрепление тем не менее существует.
Пользовательские элементы и собственная модель компонентов платформы
Браузеры уже включают в себя модель компонентов: Custom Elements и Shadow DOM. Обычный HTML использует их напрямую:
<user-avatar src="avatar.jpg" size="large"></user-avatar>
React из-за двух причин исторически плохо справлялся с пользовательскими элементами:
- он передавал все атрибуты в неизвестные элементы с маленькой буквы в виде строковых атрибутов, поэтому объекты и массивы не могли передаваться в качестве свойств элементов
new CustomEvent('user-select'), не соответствуют свойствам вроде onUserSelect, из-за чего приходится использовать обёрточные компоненты или вручную настраивать обработчики через refsВ React 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 году.