JSX to nie HTML: rzeczywiste kompromisy stojące za markupiem komponentów
Zrozum, co tracisz, gdy markup staje się JSX – od narzędzi i parsowania po formularze, dostępność i przenośność – oraz wzorce, które pozwalają odzyskać większość z tego.
Prawie każdy tutorial dotyczący React zapewnia początkujących, że JSX to „w zasadzie HTML wewnątrz JavaScriptu”. Podobieństwo jest rzeczywiste, ale to zapewnienie ukrywa szereg kosztów, które objawiają się później w procesach budowania aplikacji, rozmiarze plików, opóźnieniach przy wprowadzaniu danych oraz audytach dostępności. Poniżej zobaczysz, czym naprawdę jest JSX pod jego pozorną formą, które funkcje zmieniają się, gdy składnia przechodzi z parsera przeglądarki do środowiska wykonawczego JavaScriptu, oraz jakie konkretne wzorce mogą przywrócić większość utraconych funkcji bez rezygnacji z komponentów.
Znany wygląd na innej platformie
JSX prawie w całości zapożycza strukturę HTML. Elementy zaczynają się od <button>, kończą na </button> i są układane w sposób identyczny jak składnia od lat 90. XX wieku. To znajomość znacznie ułatwiła przechodzenie na aplikacje jednostronicowe całej generacji programistów:
// It looks like HTML...
function UserCard({ name, role }) {
return (
<div className="card">
<h3>{name}</h3>
<p>{role}</p>
</div>
);
}
Jednak w rzeczywistości są to dwie niepowiązane ze sobą technologie. HTML to język znaczników deklaratywny, który silniki przeglądarek parsują bezpośrednio za pomocą mocno zoptymalizowanego kodu natywnego. JSX to składnia, którą kompilator przepisuje na nawarstwione wywołania funkcji JavaScript: React.createElement w klasycznej transformacji lub pomocniki takie jak _jsx() we współczesnym automatycznym środowisku wykonawczym. <div> w komponencie wcale nie jest znacznikiem; to lista argumentów.
Zmiana podejścia daje wiele korzyści: drzewa komponentów sterowane danymi, automatyczna synchronizacja między stanem a DOM oraz weryfikacja typów w szablonach. Każda abstrakcja ma jednak swoją cenę. Zastąpienie natywnego formatu przeglądarki warstwą JavaScript oznacza konieczność korzystania z narzędzi do budowania aplikacji, większe zużycie pamięci w czasie działania oraz pomijanie kilku funkcji odpornych na awarie, które platforma oferuje za darmo. Żadna z tych kosztów nie jest powodem, by unikać JSX, ale każda z nich warto znać podczas projektowania aplikacji.
Podatek od narzędzi
Utrata trybu pracy przy podwójnym kliknięciu
Pierwszą rzeczą, która znika, jest prostota platformy internetowej oparta na braku zależności. Zwykła strona HTML nie wymaga niczego poza edytorem i przeglądarką. Można stworzyć plik index.html na urządzeniu offline, kliknąć w niego dwukrotnie, a przeglądarka natychmiast wyświetli go z adresu URL typu file://.
Żaden silnik JavaScriptu, czy to V8, JavaScriptCore czy SpiderMonkey, nie rozumie <div className="box"> jako kodu. Zanim cokolwiek pojawi się na ekranie, JSX wymaga łańcucha kompilacji:
- kompilator taki jak Babel, SWC lub esbuild
- bundler taki jak Vite, Webpack, Rollup lub Turbopack
- npm, pnpm lub yarn do instalacji zależności
- Node.js lub Bun do uruchamiania tych narzędzi
- foldera
node_modules, który zazwyczaj zawiera setki megabajtów presetów, parserów, pluginów i polyfilli
(Istnieją wersje Babel działające bezpośrednio w przeglądarce do szybkich eksperymentów, ale nie nadają się do użycia w produkcie.) Różnica w ścieżce od pliku źródłowego do pikseli wygląda następująco:
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
Obciążenie konserwacją
Powiązanie markupu z kompilatorem generuje ciągłe koszty:
- Zepsucie łańcucha narzędzi. Projekt, którym nikt nie zajmował się przez trzy lata, często odmawia kompilacji, ponieważ pakiety używane w projekcie, wymagane wersje Node.js lub interfejsy narzędzi do pakowania uległy zmianie.
- Kruchość map źródłowych. Debugowanie w środowisku produkcyjnym polega na odwzorowaniu skompresowanego wyniku z powrotem do oryginalnych elementów kodu. Gdy mapy źródłowe są brakujące lub błędne, ślady wywołań pokazują anonimowe funkcje wykonane w czasie działania, a nie twój kod.
- Z opóźnieniem powrót informacji o procesie kompilacji. Nowoczesne narzędzia do pakowania napisane w Rust lub Go są niezwykle szybkie, jednak w bardzo dużych bazach kodu ciągła ponowna kompilacja nadal powoduje opóźnienie, którego po prostu nie ma przy edycji surowego kodu.
Ograniczenia składniowe odziedziczone od JavaScript
Ponieważ JSX jest analizowany jako JavaScript, odziedzicza jego słowa rezerwowane oraz bardziej ścisłą gramatykę, tracąc przy tym elastyczność charakterystyczną dla HTML.
Słowa rezerwowane stają się przypisami o nowych nazwach
Atrybut HTML to ciąg znaków przypisany do węzła DOM. Przypis JSX to klucz w obiekcie przekazywanym do funkcji. Ponieważ class i for są słowami rezerwowanymi w JavaScript, JSX używa innych nazw. Standardowy formularz HTML:
<!-- Native, standards-compliant HTML -->
<label for="username">Username</label>
<div class="profile-card" tabindex="0"></div>
staje się następujący w JSX. Zwróć również uwagę, że tabIndex otrzymuje wyrażenie liczbowe zamiast ciągu znaków:
// JSX equivalents forced by JavaScript engine constraints
<label htmlFor="username">Username</label>
<div className="profile-card" tabIndex={0}></div>
Różnica między wielkimi a małymi literami i obiekty stylu
Nazwy atrybutów HTML są niewrażliwe na wielkość liter, natomiast właściwości JSX są wrażliwe i zazwyczaj mają format camelCased: onClick, strokeWidth, autoComplete, tabIndex. Jedna korekta powszechnego twierdzenia: atrybuty aria-* i data-* stanowią wyjątek i zachowują swoje nazwy z podkreśleniami w JSX, więc aria-label jest zapisywany dokładnie tak samo jak w HTML.
Stylizacje wstawione bezpośrednio zmieniają się w bardziej fundamentalny sposób. W HTML styl to zwykła ciąg znaków, który przeglądarka analizuje:
<!-- HTML: Zero runtime allocation -->
<div style="background-color: red; margin-top: 10px;"></div>
W JSX jest to literał obiektu w JavaScript, a literał umieszczony w wyniku renderowania jest tworzony na nowo za każdym razem, gdy komponent się renderuje:
// JSX: Allocates a new JavaScript object on every single render pass
<div style={{ backgroundColor: 'red', marginTop: '10px' }} />
Dla większości komponentów jest to zaniedbywalne. W komponentach, które są renderowane bardzo często, takich jak duże tabele danych czy nakładki na obrazku sterowane wskaźnikiem, tysiące krótkotrwałych obiektów stylu powodują presję na proces zbierania śmieci, co może objawiać się przerwami w działaniu. Uniknięcie tego osiąga się poprzez przeniesienie statycznych obiektów stylu poza komponent lub używanie nazw klas.
Straoge zasady zamknięcia
HTML5 celowo toleruje elementy typu void bez znaków zamknięcia: <input>, <img>, <br> oraz <hr> są wszystkie poprawne w takiej formie. JSX natomiast stosuje zasady XML. Jeśli zapomnimy o znaku samozamknięcia lub o tagu zamknięciowym, kompilator zatrzymuje się z błędem składniowym, przez co proces budowania aplikacji zawodzi, zamiast działać dalej w sposób uporządkowany.
Koszt w czasie wykonywania: natywna analiza kontra wirtualny DOM
Gdy przeglądarka otrzymuje HTML, tokenizuje przychodzące bajty i stopniowo buduje węzły DOM, wykorzystując kod natywny doskonalony przez dziesięciolecia oraz skaner wstępnego ładowania, który wcześnie wykrywa potrzebne zasoby. Gdy aplikacja renderuje się w całości za pomocą JSX, ten natywny sposób jest w dużej mierze omijany na rzecz wykonywania kodu 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
Pamięć i zbieranie śmieci
Aby obliczać aktualizacje, React przechowuje opis interfejsu użytkownika w pamięci JavaScript, zwykle nazywany wirtualnym DOM. Proces ten przebiega mniej więcej w następujący sposób:
- Pierwsze renderowanie tworzy drzewo obiektów JavaScript opisujących każdy element, jego właściwości oraz potomne elementy.
- Gdy zmienia się stan, dotknięte komponenty są ponownie uruchamiane i tworzą nowy opis swojej części drzewa.
Należy zauważyć, że React ponownie renderuje poddrzewo poniżej komponentu, którego stan się zmienił, a nie całe aplikacje, ale zasada pozostaje ta sama: przeglądarka przechowuje już rzeczywisty DOM w swojej wewnętrznej pamięci, a stos JavaScript zawiera równoległą reprezentację. To dodatkowe przydzielanie pamięci zwiększa jej zużycie oraz częstotliwość zbierania śmieci, co jest szczególnie widoczne na urządzeniach mobilnych o niskich parametrach.
Koszt hydratacji
Rendering po stronie serwera w frameworkach takich jak Next.js lub Remix wysyła prawdziwy HTML, dzięki czemu pierwsze odświeżenie strony następuje szybko. Zanim jednak strona zareaguje na wprowadzone dane, przeglądarka musi pobrać JavaScript komponentów znajdujących się na niej, je wykonać, odbudować wewnętrzne drzewo Reacta oraz dodać obsługę zdarzeń do istniejącego DOM-u. Ten krok hydratacji zajmuje główny wątek, a na ciężkich stronach objawia się wysokim czasem całkowitego blokowania (Total Blocking Time – TBT) oraz niskim wynikiem Interakcji do pierwszego odświeżenia (Interaction to Next Paint – INP).
Strumieniowanie i tolerancja na błędy
HTML został zaprojektowany w oparciu o dwa zasady, które aplikacje jednostronicowe oparte na JSX często podważają: stopniowe strumieniowanie oraz tolerancja na błędy.
Kiedy znika strumieniowanie
Brauzer zaczyna renderować dokument, zanim ten zostanie w pełni pobraany. Załóżmy, że serwer dostarczył już <head> oraz 50 KB z 200 KB strony; wtedy brauzer może już żądać plików z stylami i czcionkami oraz wyświetlać nagłówek i elementy nawigacyjne, podczas gdy reszta nadal jest w transporcie.
Aplikacja JSX renderowana wyłącznie po stronie klienta działa inaczej:
- brauzer otrzymuje niemal pustą strukturę zawierającą jedynie
<div id="root"></div> - pobiera plik z JavaScriptem
- analizuje i wykonuje ten plik
- komponenty uruchamiają się, budują DOM i w końcu wyświetlają treść
Przy wolnym połączeniu 3G lub słabym telefonie użytkownik widzi pustą stronę przez cały ten czas. To wszystko, z czym brauzer musi na początku pracować:
<!-- 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>
Kiedy błędy przestają być tolerowane
Parser HTML jest znany z dużego tolerancji. Podaj mu uszkodzony kod, na przykład taki:
<div>
<p>Unclosed paragraph
<div>Nested incorrectly</b>
</div>
i nie zawiedzie. Algorytm parsowania zamyka i ponownie układa elementy zgodnie z dobrze określonymi regułami naprawczymi i mimo to wyświetla treść.
React jest o wiele mniej wyrozumiały w czasie działania. Jeśli renderowanie zakończy się błędem, na przykład dlatego, że wyrażenie takie jak {user.profile.name} próbuje uzyskać dostęp do właściwości obiektu undefined, React usuwa cały drzewo komponentów, jeśli żaden element ErrorBoundary nie złapie błędu, co skutkuje pustym ekranem. React nie dostarcza gotowego komponentu <ErrorBoundary>; trzeba go napisać jako komponent klasowy (lub użyć małej biblioteki) i umieścić go celowo wokół obszarów o wysokim ryzyku.
Formularze i zdarzenia
Formularze HTML od początków sieci internetowej obsługują wprowadzanie danych, walidację i wysyłanie informacji w sposób wbudowany. Typowe wzorce JSX często zastępują te elementy prostsze implementacjami w JavaScript.
Wejścia kontrolowane versus wbudowane
Zwykły element <input> przechowuje swój własny stan. Pisanie tekstu natychmiast aktualizuje wewnętrzną pamięć przeglądarki, bez udziału żadnego skryptu. Idiomaticzny wzorzec w React sprawia natomiast, że wejście staje się kontrolowane, dzięki czemu stan React staje się źródłem prawdy:
// 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)}
/>
);
}
Każde naciśnięcie klawisza przechodzi teraz przez system zdarzeń, zmienia stan, ponownie uruchamia funkcję komponentu, dostosowuje wartości, a następnie wrzuca ją z powrotem do DOM. To jest w porządku dla małego komponentu. Gdy główny wątek jest zajęty przetwarzaniem danych lub intensywnymi animacjami, albo gdy dane wejściowe znajdują się wewnątrz dużego drzewa komponentów, które są renderowane razem z nimi, użytkownicy mogą zauważyć opóźnienie w pojawianiu się znaków po ich wpisaniu.
Zdarzenia syntetyczne
React otacza rdzenne zdarzenia przeglądarki własnym systemem zdarzeń syntetycznych, początkowo po to, by ukryć różnice pomiędzy starszymi przeglądarkami. Ta abstrakcja jednak powoduje pewne ukryte problemy:
- przepływ zdarzeń w drzewie React może różnić się od przepływu w rdzennych słuchaczach DOM, co utrudnia pisanie kodu wykorzystującego oba podejścia
Dostępność i degradacja semantyki
JSX może tworzyć doskonale dostępny, semantyczny HTML. Jednak wzorce, które promuje, mają tendencję do stopniowego niszczenia semantyki z upływem czasu.
Mieszanina elementów div z otaczających komponenty struktur
Komponent musi zwracać pojedynczy węzeł korzeniowy. Fragmenty (<Fragment> lub <>) rozwiązują ten problem bez dodatkowego DOM, ale wiele baz kodu nadal otacza dzieci elementami <div> z przyzwyczajenia lub ze względów na układ. Struktura, którą chciało się stworzyć, wygląda następująco:
<!-- What you intended to build -->
<main>
<article>
<h1>Article Title</h1>
<p>Content goes here...</p>
</article>
</main>
Gdy komponenty otaczające są warstwowe, często renderują coś bardziej przypominającego to:
<!-- 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>
Dodatkowe warstwy powodują większą objętość DOM-u, komplikują układ w CSS i wprowadzają zakłócenia pomiędzy elementami kluczowymi. Ogólne <div> bez określonych ról są w większości ignorowane przez technologie wspomagające, ale długie łańcuchy komponentów otaczających nadal utrudniają zrozumienie struktury markupu i sprzyjają popełnianiu błędów, na przykład gdy komponent otaczający przypadkowo niszczy strukturę listy lub nagłówka.
Zachowanie klawiatury, które masz za darmo, dopóki go nie stracisz
Wrodzone elementy interaktywne takie jak <button>, <a>, <select> i <details> mają zachowania, które łatwo uważać za oczywiste:
- domyślnie można na nie skupić się w kolejności tabu
- klawisze Enter i Space automatycznie je aktywują
Ponieważ JSX upraszcza do granic możliwości dodawanie obsługi kliknięć do dowolnego elementu, jak w <div onClick={handleClick}>, zespoły regularnie tworzą niestandardowe kontrolki z elementów niesemantycznych, zapominając o obsłudze klawiatury, atrybucie tabIndex oraz rolach ARIA, które te elementy natywne dostarczają automatycznie. Naszy przegląd ukrytych pułapek komponentów React omawia więcej takich problemów.
Standardy, interoperacyjność i uwięzienie
HTML to otwarty standard utrzymywany przez WHATWG, przy czym W3C odgrywało w nim historycznie istotną rolę. Strony napisane w 1997 roku nadal działają we współczesnych przeglądarkach. JSX natomiast nie jest standardem sieciowym – posiada nieformalną specyfikację i jest wspierane przez kilka bibliotek, w tym React, Preact i Solid, ale każde jego użycie wymaga kompilatora oraz środowiska wykonawczego, do którego skierowane są kompilowane pliki. Jest to lżejsza forma „zamknięcia” niż w przypadku formatu własnościowego, ale mimo to stanowi formę zamknięcia.
Elementy niestandardowe i własny model komponentów platformy
Przeglądarki już zawierają model komponentów: Custom Elements w połączeniu z Shadow DOM. Zwykły HTML korzysta z nich bezpośrednio:
<user-avatar src="avatar.jpg" size="large"></user-avatar>
React historycznie słabo radziło sobie z elementami niestandardowymi z dwóch powodów:
- przekazywało wszystkie atrybuty do nieznanych tagów małych liter jako atrybuty tekstowe, więc obiekty i tablice nie mogły być przekazywane jako właściwości elementów
new CustomEvent('user-select'), nie odpowiadały żadnej właściwości typu onUserSelect, co zmuszało do użycia komponentów otaczających lub ręcznych słuchaczy poprzez refyReact 19 w dużej mierze rozwiązał ten problem, umożliwiając ustawianie właściwości w elementach niestandardowych, gdy te je definiują, oraz obsługę niestandardowych obsługiwców wydarzeń, więc sprawdź, jaką wersję React używasz, zanim założysz, że te ograniczenia nadal obowiązują.
Przenośność markupu
System projektowy napisany w standardowym HTML i CSS może być używany wszędzie: w WordPress, Django, Ruby on Rails, szablonach Go, Vue, Angular, Svelte lub prostych stronach statycznych. System projektowy napisany jako komponenty JSX jest związany z ekosystemem JavaScript. Użycie go w backendzie nieopartym na JavaScript wymaga usługi renderowania Node.js lub osobnej implementacji każdego komponentu.
HTML i JSX obok siebie
Podsumowując zalety i wady:
- Wykonywanie: HTML jest natywnie parsowany przez przeglądarkę; JSX kompiluje się do wywołań funkcji JavaScript, które są uruchamiane w czasie rzeczywistym.
- Narzędzia: HTML wymaga edytora i przeglądarki; JSX potrzebuje kompilatora, narzędzia do pakietowania, menedżera pakietów oraz środowiska do budowy aplikacji.
- Syntaksa: HTML jest nieczuły na wielkość liter i wysoce tolerancyjny; JSX jest wrażliwy na wielkość liter, używa przemianowanych atrybutów i wymaga zamknięcia w stylu XML.
- Renderyzacja: HTML przesyła dane i rysuje je stopniowo; JSX renderowany po stronie klienta czeka na pobranie i uruchomienie całego pakietu.
- Błędy: HTML potrafi odradzić się po uszkodzonej strukturze; niezłapany błąd renderowania powoduje usunięcie drzewa React.
- Formularze: elementy wejściowe typu native przechowują swój własny stan; elementy kontrolowane są renderowane na nowo przy każdym naciśnięciu klawisza.
Odzyskiwanie tego, co straciliśmy
Nic z tego nie przemawia za porzuceniem rozwoju opartego na komponentach. Przemawia za świadomym podejściem do tego, gdzie abstrakcja jest warta swojej ceny. Ekosystem zmierza dokładnie w tym kierunku, oferując wzorce, które przywracają szybkość i odporność naturalnego HTML, jednocześnie zachowując deklaratywny model tworzenia.
Wybierz renderowanie typu server-first
- React Server Components renderują się na serwerze i nie wysyłają żadnego JavaScriptu komponentów do klienta w przypadku elementów, które nie są interaktywne.
- Astro wykorzystuje architekturę opartą na wyspach: strony są domyślnie statycznym HTML, a jedynie izolowane interaktywne elementy są wypełniane treścią.
- Qwik zastępuje ten proces wypełnianiem możliwością kontynuacji działania aplikacji, serializując stan aplikacji do HTML tak, aby kod był wykonywany tylko wtedy, gdy użytkownik faktycznie z nim interaguje.
Pozwól przeglądarce zarządzać stanem formularza
Zamiast odzwierciedlać każdy nacisk klawisza w stanie formularza, niech natywny element <form> przechowuje wartości i odczytuje je raz podczas wysłania formularza za pomocą 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>
);
}
Wpis aktualizuje się z prędkością natywną, a atrybut required zapewnia wbudowaną weryfikację; komponent renderuje się tylko raz, a nie przy każdym nacisku klawisza. Nowsze wersje Reacta opierają się na tej samej koncepcji, używając operacji formularza, które przyjmują bezpośrednio FormData.
Zachowaj ścisłą semantykę
Traktuj JSX jako sposób na tworzenie semantycznego HTML, a nie jako uprawnienie do stosowania kontenerów:
- zastąpaj elementy otaczające
<div>fragmentami (<></>) tam, gdzie nie służą one do stylizacji - używaj wbudowanych elementów interaktywnych takich jak
<button>,<dialog>,<details>i<summary>zamiast samodzielnie budowanych komponentów - dodaj
eslint-plugin-jsx-a11ydo procesu ciągłej integracji, aby brak etykiet, ról i obsługowników klawiatury powodował niepowodzenie kompilacji
Podsumowanie
JSX zmieniło rozwój front-endu, pokazując, że interfejsy najlepiej opisywać można jako przewidywalne funkcje danych, i rozwiązało rzeczywiste problemy związane z utrzymywaniem dużej, dynamicznej interfejsu użytkownika w synchronizacji z stanem. Nadal jest to abstrakcja JavaScriptu, a nie nowsza wersja HTML. Wybór tego rozwiązania oznacza rezygnację z natywnego streamowania, możliwości tworzenia bez konieczności kompilacji, tolerancji na błędy oraz długoterminowej stabilności standardów na rzecz kompozycji i ergonomicznego podejścia reaktywnego. Często jest to korzystna zamiana. Umiejętność inżynierska polega na dokładnej znajomości tego, co poświęca się, oraz na korzystaniu z renderowania na serwerze, natywnych form i elementów semantycznych tam, gdzie można uzyskać te funkcje za darmo.
Literatura pokrewna
- REST vs GraphQL: Prawdziwe kompromisy stojące za każdą architekturą — Wyjaśnia konkretne problemy, które rozwiązują REST i GraphQL, ich wewnętrzną mechanikę oraz ukryte kompromisy, które należy wziąć pod uwagę przed wyborem jednego z nich do swojej API.
- Dziesięć ukrytych powodów problemów z komponentami React, które spowalniają nowoczesne aplikacje — Poznaj dziesięć powszechnych błędów w komponentach React – od braków w semantycznym HTML po brak memoizacji – oraz rozwiązania potrzebne, aby aplikacje pozostały szybkie, dostępne i wolne od błędów do roku 2026.