Kurs JavaScript i React · Moduł 14: Zaawansowane stylowanie

Zero-runtime CSS-in-JS - Kosmiczna Kompilacja Stylów

4 min czytania
W tej lekcji8

Pilotze, wyobraź sobie dwa typy silników statku kosmicznego. Pierwszy oblicza trasę lotu w locie -- zużywa paliwo na bieżąco (runtime CSS-in-JS jak styled-components). Drugi oblicza trasę przed startem i zapisuje ją w komputerze pokładowym -- podczas lotu nie zużywa dodatkowej energii (zero-runtime CSS-in-JS). W świecie frontendu to różnica między podejściami runtime i zero-runtime do stylowania.

Problem runtime CSS-in-JS

Styled-components i Emotion działają tak: za każdym razem gdy React renderuje komponent, biblioteka w runtime parsuje template literal, generuje unikalny hash klasy i wstrzykuje <style> tag do DOM. To oznacza:

1// Styled-components -- runtime
2const Card = styled.div`
3  background: ${({ theme }) => theme.colors.surface};
4  padding: ${({ theme }) => theme.spacing.lg};
5  border-radius: 12px;
6`;
7// Za każdym renderem: parsuj template -> generuj hash -> wstrzyknij <style>

Problemy:

  • Większy bundle -- biblioteka (~12-15 KB gzip) musi być załadowana
  • Wolniejsze renderowanie -- generowanie stylów w runtime kosztuje czas
  • SSR komplikacje -- style muszą być wyciągnięte i wstrzyknięte do HTML

Czym jest zero-runtime CSS-in-JS?

Zero-runtime CSS-in-JS to podejście, gdzie style są generowane w czasie kompilacji (build time) -- wynikowy kod nie zawiera żadnego JavaScript do obsługi stylów. Piszesz style w TypeScript, a bundler zamienia je na zwykły plik CSS.

Vanilla-extract -- TypeScript-first styling

Vanilla-extract to najpopularniejsza biblioteka zero-runtime. Style definiujesz w plikach .css.ts:

1// styles.css.ts -- plik ze stylami
2import { style } from '@vanilla-extract/css';
3
4export const card = style({
5  background: '#1a1a4e',
6  border: '2px solid #3949ab',
7  borderRadius: '12px',
8  padding: '20px',
9  transition: 'all 0.3s ease',
10  ':hover': {
11    borderColor: '#7c4dff',
12    transform: 'translateY(-2px)',
13  },
14});
15
16export const heading = style({
17  color: '#7c4dff',
18  fontSize: '20px',
19  marginBottom: '16px',
20});

W komponencie React importujesz klasy jak zwykły string:

1// Component.tsx
2import { card, heading } from './styles.css.ts';
3
4function MissionCard({ title, children }) {
5  return (
6    <div className={card}>
7      <h3 className={heading}>{title}</h3>
8      {children}
9    </div>
10  );
11}

Po kompilacji card i heading to zwykłe stringi z nazwami klas CSS -- zero JavaScript w runtime.

Sprinkles -- typowane klasy użytkowe

Vanilla-extract oferuje sprinkles -- typowany odpowiednik klas użytkowych (jak Tailwind, ale z pełnym typowaniem TypeScript):

1// sprinkles.css.ts
2import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
3
4const responsiveProperties = defineProperties({
5  conditions: {
6    mobile: {},
7    tablet: { '@media': 'screen and (min-width: 768px)' },
8    desktop: { '@media': 'screen and (min-width: 1024px)' },
9  },
10  defaultCondition: 'mobile',
11  properties: {
12    display: ['none', 'flex', 'block', 'grid'],
13    padding: { sm: '8px', md: '16px', lg: '24px' },
14    gap: { sm: '8px', md: '16px', lg: '24px' },
15  },
16});
17
18export const sprinkles = createSprinkles(responsiveProperties);

Użycie:

1<div className={sprinkles({
2  display: { mobile: 'block', tablet: 'grid' },
3  padding: { mobile: 'sm', desktop: 'lg' },
4  gap: 'md',
5})}>

Wszystko jest typowane -- TypeScript podpowie dostępne wartości i złapie literówki.

Linaria -- zero-runtime z składnią styled-components

Linaria oferuje składnię identyczną jak styled-components, ale kompiluje style w build time:

1import { styled } from '@linaria/react';
2
3const Card = styled.div`
4  background: #1a1a4e;
5  border: 2px solid #3949ab;
6  border-radius: 12px;
7  padding: 20px;
8  &:hover {
9    border-color: #7c4dff;
10  }
11`;

Wygląda identycznie jak styled-components, ale po kompilacji Card renderuje <div className="hash123"> -- bez runtime JS.

Panda CSS -- nowoczesna alternatywa

Panda CSS to najnowszy gracz -- łączy podejście utility-first (jak Tailwind) z typowaniem i zero-runtime:

1import { css } from '../styled-system/css';
2
3function MissionPanel() {
4  return (
5    <div className={css({
6      bg: 'surface',
7      p: '6',
8      borderRadius: 'lg',
9      border: '2px solid',
10      borderColor: 'border',
11      _hover: { borderColor: 'primary' },
12    })}>
13      Panel misji
14    </div>
15  );
16}

Panda generuje CSS atomowe (jak Tailwind) w build time -- każda właściwość to osobna klasa, co minimalizuje duplikację.

Porównanie: runtime vs zero-runtime

Cechastyled-componentsVanilla-extractLinariaPanda CSS
Runtime JSTak (~12 KB)NieNieNie
Dynamiczne stylePełne (props)Ograniczone (recipes)OgraniczoneOgraniczone
TypeScriptDobreNatywneDobreNatywne
SSRKonfiguracjaBezproblemowyBezproblemowyBezproblemowy
DX (doświadczenie)ŚwietneDobreŚwietneŚwietne

Kiedy wybrać zero-runtime?

  • Duże aplikacje -- każdy kilobajt runtime się liczy
  • SSR (Next.js, Remix) -- brak problemów z hydratacją stylów
  • TypeScript-first -- pełne typowanie wartości CSS
  • Wydajność -- brak kosztu runtime = szybsze renderowanie
Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2import './styles.css';
3
4// Porownanie: runtime vs zero-runtime CSS-in-JS
5// W Sandpack nie mozemy uzywac vanilla-extract/Linaria/Panda,
6// wiec demonstrujemy koncept porownania
7
8function CodeBlock({ title, code, language }) {
9  return (
10    <div className="code-block">
11      <div className="code-header">
12        <span className="code-lang">{language}</span>
13        <span className="code-title">{title}</span>
14      </div>
15      <pre className="code-content">{code}</pre>
16    </div>
17  );
18}
19
20function ApproachCard({ name, desc, runtime, bundle, typescript, ssr, active, onClick }) {
21  return (
22    <div className={`approach-card ${active ? 'approach-active' : ''}`} onClick={onClick}>
23      <h3 className="approach-name">{name}</h3>
24      <p className="approach-desc">{desc}</p>
25      <div className="approach-badges">
26        <span className={`badge ${runtime === 'Zero' ? 'badge-green' : 'badge-red'}`}>
27          Runtime: {runtime}
28        </span>
29        <span className={`badge ${bundle === 'Minimalny' ? 'badge-green' : 'badge-yellow'}`}>
30          Bundle: {bundle}
31        </span>
32        <span className={`badge ${typescript === 'Natywne' ? 'badge-green' : 'badge-blue'}`}>
33          TS: {typescript}
34        </span>
35        <span className={`badge ${ssr === 'Bezproblemowy' ? 'badge-green' : 'badge-yellow'}`}>
36          SSR: {ssr}
37        </span>
38      </div>
39    </div>
40  );
41}
42
43const approaches = [
44  {
45    name: 'Vanilla-extract',
46    desc: 'TypeScript-first, zero-runtime. Style w plikach .css.ts',
47    runtime: 'Zero', bundle: 'Minimalny', typescript: 'Natywne', ssr: 'Bezproblemowy',
48    code: `// styles.css.ts
49import { style } from '@vanilla-extract/css';
50
51export const card = style({
52  background: '#1a1a4e',
53  border: '2px solid #3949ab',
54  borderRadius: '12px',
55  padding: '20px',
56  ':hover': {
57    borderColor: '#7c4dff',
58  },
59});
60
61// Component.tsx
62import { card } from './styles.css.ts';
63<div className={card}>Panel misji</div>`,
64  },
65  {
66    name: 'Linaria',
67    desc: 'Skladnia styled-components, ale zero-runtime',
68    runtime: 'Zero', bundle: 'Minimalny', typescript: 'Dobre', ssr: 'Bezproblemowy',
69    code: `import { styled } from '@linaria/react';
70
71const Card = styled.div\`
72  background: #1a1a4e;
73  border: 2px solid #3949ab;
74  border-radius: 12px;
75  padding: 20px;
76  &:hover {
77    border-color: #7c4dff;
78  }
79\`;
80
81// Wyglada jak styled-components
82// ale kompiluje sie do zwyklego CSS`,
83  },
84  {
85    name: 'Panda CSS',
86    desc: 'Utility-first + zero-runtime. Nowoczesna alternatywa',
87    runtime: 'Zero', bundle: 'Minimalny', typescript: 'Natywne', ssr: 'Bezproblemowy',
88    code: `import { css } from '../styled-system/css';
89
90function Panel() {
91  return (
92    <div className={css({
93      bg: 'surface',
94      p: '6',
95      borderRadius: 'lg',
96      border: '2px solid',
97      borderColor: 'border',
98      _hover: { borderColor: 'primary' },
99    })}>
100      Panel misji
101    </div>
102  );
103}`,
104  },
105  {
106    name: 'Styled-components (runtime)',
107    desc: 'Pelne dynamiczne style, ale z kosztem runtime JS',
108    runtime: '~12KB', bundle: 'Duzy', typescript: 'Dobre', ssr: 'Konfiguracja',
109    code: `import styled from 'styled-components';
110
111const Card = styled.div\`
112  background: \${({ theme }) => theme.colors.surface};
113  border: 2px solid \${({ active }) =>
114    active ? '#7c4dff' : '#3949ab'};
115  border-radius: 12px;
116  padding: 20px;
117\`;
118
119// Pelne dynamiczne style z propsami
120// ale runtime JS parsuje template przy kazdym renderze`,
121  },
122];
123
124export default function App() {
125  const [selected, setSelected] = useState(0);
126
127  return (
128    <div className="app">
129      <h1 className="heading">Zero-runtime CSS-in-JS</h1>
130      <p className="subtitle">Porownanie podejsc: build-time vs runtime</p>
131
132      <div className="grid">
133        {approaches.map((a, i) => (
134          <ApproachCard key={a.name} {...a} active={i === selected} onClick={() => setSelected(i)} />
135        ))}
136      </div>
137
138      <div className="detail-panel">
139        <h2 className="detail-title">{approaches[selected].name} -- przykladowy kod</h2>
140        <CodeBlock
141          title={approaches[selected].name}
142          language={approaches[selected].name === 'Vanilla-extract' ? 'TypeScript' : 'JSX'}
143          code={approaches[selected].code}
144        />
145      </div>
146    </div>
147  );
148}

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Co oznacza 'zero-runtime' w kontekście bibliotek CSS-in-JS?

  2. 2. W jakim formacie pliku definiujemy style w vanilla-extract?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Zaimplementuj porównanie metod stylowania z warunkowym podświetleniem

  • Układanie w poziomie

    Ułóż składnię importu stylu z pliku vanilla-extract w komponencie:

  • Edytor kodu

    Zaimplementuj tabelę porównania podejść CSS-in-JS z podświetleniem

Przydatne artykuły