Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Проблеми та рішення CSS-in-js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**CSS-in-JS проблеми** - накладні витрати рантайму при вставці стилів, розмір бандлу від самої бібліотеки (~20KB) та помилки SSR-гідратації коли стилі не витягуються на сервері. ```jsx // Проблема: новий хеш на кожен рендер, промах кешу щоразу const style = css`color: ${props.color}`; // Рішення: мемоізуємо динамічні стилі const style = useMemo(() => css`color: ${color}`, [color]); ``` **Ключове:** Для статичних інтерфейсів CSS Modules або Tailwind вирішують ці проблеми. Для динамічних тем - мемоізуй стилі і налаштуй SSR-екстракцію.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**CSS-in-JS** - підхід до стилізації компонентів, де CSS записується як JavaScript template literals або об'єкти прямо у файлах компонентів, а бібліотека генерує унікальні хешовані класи під час виконання. ## Теорія ### TL;DR - Аналогія: рецептна картка приклеєна до страви - стилі подорожують разом з компонентом і не виходять за його межі - Головний компроміс: динамічні стилі через пропси, але 10-50KB JS-рантайму і додаткове навантаження на кожен рендер - Три реальні проблеми: продуктивність рендерингу, розмір бандлу, SSR-гідратація - Правило вибору: динамічні стилі більше ніж ~20% застосунку - CSS-in-JS; переважно статична верстка - CSS Modules або Tailwind - Для SSR-застосунків: Linaria та Vanilla Extract витягують CSS під час збірки, без рантайм-витрат ### Швидкий приклад ```jsx // styled-components: клас генерується під час виконання, скоп автоматичний import styled from 'styled-components'; const Button = styled.button` background: ${props => props.primary ? 'blue' : 'gray'}; color: white; padding: 10px; `; // <Button primary>Клік</Button> // Результат: <button class="sc-a-xyz123"> з <style> тегом у <head> // Ім'я класу - хеш стилів, жодного конфлікту з глобальним CSS ``` Бібліотека хешує template literal, вставляє правило через `sheet.insertRule()` і кешує результат. При повторному рендері з тими самими пропсами береться кешований хеш, DOM не чіпається. ### Три проблеми **1. Продуктивність рендерингу** Styled-components та Emotion вставляють стилі в DOM під час виконання JS. Браузер парсить JS, запускає функції стилів, хешує результат і тільки тоді малює. У списку на 1000 елементів без мемоізації це означає 1000 окремих викликів `insertRule()` на кожен цикл рендерингу. На практиці таке падіння виглядає як деградація з 60fps до ~10fps на великих списках. **2. Розмір бандлу** Styled-components важить ~20KB gzipped у рантаймі. Tailwind після PurgeCSS - близько 10KB без рантайму взагалі. Для більшості застосунків різниця непомітна. Але для сторінок де швидкість завантаження вимірюється - лістингів продуктів або дашбордів на мобільних - вона має значення. **3. Помилки SSR-гідратації** Якщо стилі вставляються тільки на клієнті, сервер відсилає HTML без визначення класів. React гідратує, стилі з'являються - і користувач бачить flash of unstyled content (FOUC). Рішення є: `ServerStyleSheet` у styled-components, `extractCritical` у Emotion - але їх потрібно налаштовувати явно. ### Коли використовувати - Застосунок з тематизацією і runtime-перемиканням кольорів - styled-components або Emotion - Статичний маркетинговий сайт або блог - CSS Modules - SSR-застосунок, де швидкість першого рендеру вимірюється - Linaria (zero-runtime) - Прототип для команди без глибоких знань CSS-архітектури - Tailwind ### Порівняння підходів | Аспект | CSS-in-JS (runtime) | CSS Modules | Tailwind | |--------|---------------------|-------------|----------| | Скопінг | Хешовані класи в рантаймі | Локальні ID під час збірки | Utility-класи | | Динамічні стилі | Нативно через пропси | Потрібні обхідні шляхи | Тільки arbitrary values | | Розмір бандлу | Runtime JS (~10-50KB) | Zero runtime | ~10KB після purge | | Налаштування SSR | Потребує окремого конфігу | Працює за замовчуванням | Працює за замовчуванням | | Коли використовувати | Динамічні UI, теми | Статичні компоненти | Прототипи, MVP | ### Як це працює всередині Styled-components використовує Babel-плагін для виділення template literals під час парсингу. В рантаймі V8 виконує функцію стилів, хешує результат через внутрішній `hashObject()` і вставляє правила через `CSSStyleSheet.insertRule()`. `StyleSheet` Map виступає кешем - якщо пропси не змінилися (shallow-equal), хеш береться з кешу і DOM залишається незміненим. Для SSR `ServerStyleSheet.collectStyles()` збирає всі правила під час серверного рендерингу і вставляє їх у `<head>` до того, як клієнт гідратується. ### Типові помилки **1. Генерація стилів у циклах без мемоізації** ```jsx // Неправильно: новий хеш на кожен елемент, 1000 insertRule-викликів const ListItem = ({ id }) => { const style = css`color: hsl(${id % 360}, 70%, 50%);`; return <li css={style}>Item {id}</li>; }; // Правильно: хеш обчислюється раз на кожен унікальний id const ListItem = memo(({ id }) => { const style = useMemo( () => css`color: hsl(${id % 360}, 70%, 50%);`, [id] ); return <li css={style}>Item {id}</li>; }); ``` Без `useMemo` кожен рендер батьківського компонента створює новий хеш і записує в DOM. Це найчастіша причина деградації продуктивності CSS-in-JS у продакшені. **2. Відсутня SSR-екстракція стилів** ```jsx // Неправильно: стилі вставляються після гідратації, FOUC гарантований export default function App() { return <ThemeProvider theme={theme}><Page /></ThemeProvider>; } // Правильно: збираємо стилі на сервері (Next.js _document.js) const sheet = new ServerStyleSheet(); ctx.renderPage = () => originalRenderPage({ enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />), }); // Стилі йдуть у <head> до гідратації клієнта ``` **3. Пропси потрапляють у DOM** ```jsx // Неправильно: React попереджає про невідомий атрибут, проп просочується в HTML const Box = styled.div` opacity: ${props => props.isVisible ? 1 : 0}; `; // Правильно: вказуємо styled-components не передавати цей проп далі const Box = styled.div.withConfig({ shouldForwardProp: (prop) => prop !== 'isVisible', })` opacity: ${props => props.isVisible ? 1 : 0}; `; ``` **4. Runtime CSS-in-JS з React Server Components** Server Components виконуються без контексту браузерного JS. Styled-components та Emotion не можуть вставляти стилі там. Рішення - Linaria або Vanilla Extract: вони витягують CSS під час збірки і генерують статичні файли, які працюють у будь-якому середовищі. ### Де зустрічається в реальних проєктах - Vercel dashboard: styled-components з `ThemeProvider` для перемикання теми за налаштуваннями користувача - Chakra UI: Emotion з `shouldForwardProp` для чистоти DOM - Remix: Linaria витягує CSS у статичні файли для SSR-роутів - Storybook: Stitches для design token пропсів у shared UI kit - Gatsby-блоги: Emotion з `babel-plugin-emotion` без рантайм-накладних ### Питання на співбесіді **Q:** Яка реальна вартість styled-components у віртуалізованому списку на 1000 елементів? **A:** Сама бібліотека важить ~50KB gzipped. Реальна вартість - промахи кешу. Якщо стилі перераховуються на кожен рендер, виходить 1000 викликів `insertRule()` за один цикл малювання. `React.memo` і мемоізація стилів знижують це до нуля. **Q:** Як CSS-in-JS вирішує проблему SSR-гідратації? **A:** Сервер збирає всі стилі під час рендерингу через `ServerStyleSheet.collectStyles()` або `extractCritical()` і вставляє їх блоком `<style>` у `<head>`. На клієнті React порівнює пре-рендерений HTML. Без цього перше малювання відбувається без класів і виникає FOUC. **Q:** Коли Tailwind виграє у CSS-in-JS за розміром бандлу? **A:** Майже завжди для статичних застосунків: Tailwind після purge важить ~10KB без рантайму. Styled-components додає ~20KB JS, який повинен виконатись до появи стилів. У дуже динамічному застосунку з сотнями унікальних runtime-генерованих стилів CSS-in-JS може видати менший загальний CSS, бо невикористані правила просто не існують. **Q:** Як розшарити кеш CSS-in-JS між мікрофронтендами в Module Federation? **A:** Потрібно виставити `StyleSheet.getInstance()` як singleton через webpack provider. Без цього кожен мікрофронтенд ініціалізує власний кеш і дублює правила. Цей патерн використовується в Nx-монорепозиторіях зі спільними Emotion-інстансами. **Q:** Чому перехід на zero-runtime Linaria ламає динамічні пропси? **A:** Linaria витягує стилі під час збірки і не може обчислювати runtime-значення типу `props.color`. Замість цього динамічні значення передаються через CSS custom properties (`var(--color)`) і встановлюються inline на елементі. Скопінг класів зберігається, але JS-інтерполяція всередині стилів зникає. ## Приклади ### Базовий: кнопка з пропсами для стилів ```jsx import styled, { ThemeProvider } from 'styled-components'; const theme = { colors: { primary: '#007bff' } }; const Button = styled.button` background: ${props => props.primary ? props.theme.colors.primary : 'gray'}; color: white; padding: 10px 20px; border: none; border-radius: 4px; `; function App() { return ( <ThemeProvider theme={theme}> <Button primary>Зберегти</Button> <Button>Скасувати</Button> </ThemeProvider> ); } // Результат: два компоненти з унікальними хешованими класами, ізольовані від глобального CSS ``` `ThemeProvider` передає тему через React context. Будь-який styled-компонент всередині читає `props.theme` без прокидання пропсів вниз. Зміна теми в одному місці автоматично оновлює всі компоненти, що її використовують. ### Середній рівень: мемоізовані картки метрик у дашборді ```jsx import { css } from '@emotion/react'; import { memo, useMemo } from 'react'; // Визначається поза компонентом - хеш обчислюється раз при завантаженні модуля const cardBase = css` border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); `; const MetricCard = memo(({ label, value, color }) => { // Перераховується тільки при зміні color, а не на кожен рендер батька const valueStyle = useMemo( () => css`color: ${color}; font-size: 24px; font-weight: 600;`, [color] ); return ( <div css={cardBase}> <span>{label}</span> <span css={valueStyle}>{value}</span> </div> ); }); <MetricCard label="Дохід" value="$10k" color="#22c55e" /> <MetricCard label="Користувачі" value="1.2k" color="#3b82f6" /> ``` `cardBase` живе поза компонентом і хешується раз при завантаженні модуля. `valueStyle` перераховується тільки коли `color` дійсно змінюється. У дашборді, де 20 карток оновлюються щосекунди, такий підхід усуває більшість зайвих записів у DOM. ### Просунутий рівень: SSR-екстракція в Next.js ```jsx // pages/_document.js import Document, { Html, Head, Main, NextScript } from 'next/document'; import { ServerStyleSheet } from 'styled-components'; export default class MyDocument extends Document { static async getInitialProps(ctx) { const sheet = new ServerStyleSheet(); const originalRenderPage = ctx.renderPage; try { ctx.renderPage = () => originalRenderPage({ enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />), }); const initialProps = await Document.getInitialProps(ctx); return { ...initialProps, // Стилі вставляються у <head> до клієнтської гідратації styles: [initialProps.styles, sheet.getStyleElement()], }; } finally { sheet.seal(); } } } ``` Без `ServerStyleSheet` Next.js надсилає HTML з правильною DOM-структурою, але без визначень класів styled-components. React гідратує, стилі вставляються - і користувач бачить короткий спалах нестилізованого контенту. З цим налаштуванням сервер збирає всі правила під час `renderPage`, відправляє їх у `<head>`, і клієнт отримує повністю стилізований документ з першого байта.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.