Suggest an editImprove this articleRefine the answer for “CSS-in-js problems and solutions”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**CSS-in-JS problems** - runtime performance overhead from style injection, bundle size from the library runtime (~20KB), and SSR hydration mismatches when styles are not extracted server-side. ```jsx // Problem: new hash on every render, cache miss every time const style = css`color: ${props.color}`; // Fix: memoize dynamic styles const style = useMemo(() => css`color: ${color}`, [color]); ``` **Key:** For static UIs, CSS Modules or Tailwind avoid these problems entirely. For dynamic themes, memoize styles and configure SSR extraction.Shown above the full answer for quick recall.Answer (EN)Image**CSS-in-JS** - a styling pattern where CSS lives as JavaScript template literals or objects inside component files, with the library generating unique hashed class names at runtime. ## Theory ### TL;DR - Analogy: recipe card stapled to the dish - styles travel with the component and never leak into other components - Main trade-off: prop-based dynamic styles, but 10-50KB of JS runtime and extra parse time on every render - The three real problems: render performance, bundle size, SSR hydration mismatches - Decision rule: dynamic styles over ~20% of the app - use CSS-in-JS; mostly static layout - use CSS Modules or Tailwind - For SSR-heavy apps: Linaria or Vanilla Extract extract CSS at build time, zero runtime cost ### Quick example ```jsx // styled-components: class generated at runtime, scoped automatically import styled from 'styled-components'; const Button = styled.button` background: ${props => props.primary ? 'blue' : 'gray'}; color: white; padding: 10px; `; // <Button primary>Click</Button> // Output: <button class="sc-a-xyz123"> with a <style> tag injected in <head> // Class name is a hash of the style rules - zero collision with global CSS ``` The library hashes the template literal, injects a rule via `sheet.insertRule()`, and caches it. On re-render with identical props, the cached hash is reused - no DOM write. ### The three problems **1. Render performance** Styled-components and Emotion inject styles into the DOM as JavaScript runs. The browser parses JS, executes style functions, hashes the output, and only then paints. In a 1000-item list without memoization, that means 1000 unique `insertRule()` calls per render cycle. Benchmarks from the React community show this can drop frame rate from 60fps to around 10fps on large lists. **2. Bundle size** Styled-components ships ~20KB gzipped at runtime. Tailwind after PurgeCSS sits around 10KB with no runtime at all. For most apps the gap is not visible. For performance-critical pages like product listings or dashboards loading on mobile, it matters. **3. SSR hydration mismatch** If styles inject only client-side, the server sends HTML without class definitions. React hydrates, styles appear, and the user sees a flash of unstyled content (FOUC). The libraries have fixes - `ServerStyleSheet` in styled-components, `extractCritical` in Emotion - but they require explicit setup. ### When to use - Theme-heavy app with runtime color or spacing switching - styled-components or Emotion - Static marketing site or blog - CSS Modules - Server-rendered app where first paint speed is measured - Linaria (zero-runtime) - Rapid prototype with a team unfamiliar with CSS architecture - Tailwind ### Comparison table | Aspect | CSS-in-JS (runtime) | CSS Modules | Tailwind | |--------|---------------------|-------------|----------| | Scoping | Runtime hashed class names | Build-time local IDs | Utility classes | | Dynamic styles | Native via props | Workarounds needed | Arbitrary values only | | Bundle impact | Runtime JS (~10-50KB) | Zero runtime | ~10KB after purge | | SSR setup | Requires extra config | Works by default | Works by default | | When to use | Dynamic UIs, theme systems | Static components | Prototyping, MVPs | ### How it works internally Styled-components uses a Babel plugin to extract template literals at parse time. At runtime, V8 executes the style function, hashes the result via an internal `hashObject()`, and injects rules into the CSSOM via `CSSStyleSheet.insertRule()`. A `StyleSheet` Map acts as a cache - if props are shallow-equal, the hash is reused and no DOM write happens. For SSR, `ServerStyleSheet.collectStyles()` captures all rules during server render and injects them inline in `<head>` before the client hydrates. ### Common mistakes **1. Generating styles inside loops without memoization** ```jsx // Wrong: new hash per item, 1000 rules inserted on every render const ListItem = ({ id }) => { const style = css`color: hsl(${id % 360}, 70%, 50%);`; return <li css={style}>Item {id}</li>; }; // Fix: compute hash once per unique id const ListItem = memo(({ id }) => { const style = useMemo( () => css`color: hsl(${id % 360}, 70%, 50%);`, [id] ); return <li css={style}>Item {id}</li>; }); ``` Without `useMemo`, every parent re-render creates a new hash and triggers a DOM write. This is the most common source of CSS-in-JS slowdowns in production. **2. Skipping SSR style extraction** ```jsx // Wrong: styles inject post-hydration, user sees FOUC export default function App() { return <ThemeProvider theme={theme}><Page /></ThemeProvider>; } // Fix with styled-components in Next.js _document.js const sheet = new ServerStyleSheet(); ctx.renderPage = () => originalRenderPage({ enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />), }); // Styles shipped in <head> before client hydration ``` **3. Forwarding non-style props to the DOM** ```jsx // Wrong: React warns about unknown DOM attribute, prop leaks to HTML const Box = styled.div` opacity: ${props => props.isVisible ? 1 : 0}; `; // Fix: tell styled-components not to forward the prop const Box = styled.div.withConfig({ shouldForwardProp: (prop) => prop !== 'isVisible', })` opacity: ${props => props.isVisible ? 1 : 0}; `; ``` **4. Using runtime CSS-in-JS with React Server Components** Server Components run without a browser JS context. Styled-components and Emotion cannot inject styles there. The fix: switch to Linaria or Vanilla Extract, which extract CSS at build time and produce static files that work anywhere. ### Real-world usage - Vercel dashboard: styled-components with `ThemeProvider` for per-user theme preference switching - Chakra UI: Emotion with `shouldForwardProp` to keep the DOM clean - Remix: Linaria extracts to static CSS files for server-rendered routes - Storybook: Stitches for design token props in shared UI kits - Gatsby static blogs: Emotion with `babel-plugin-emotion` for zero-runtime overhead ### Follow-up questions **Q:** What is the actual runtime cost of styled-components in a 1000-item virtualized list? **A:** The library itself is ~50KB gzipped. The real cost is cache misses - if styles recalculate on each render, you get 1000 `insertRule()` calls per paint cycle. Use `React.memo` and memoize dynamic style objects to reduce that to near zero. **Q:** How does CSS-in-JS handle SSR hydration? **A:** The server collects all styles during render via `ServerStyleSheet.collectStyles()` or `extractCritical()`, then injects them as a `<style>` block in the HTML head. On the client, React matches the pre-rendered HTML. Without this setup, the first paint has no class definitions and FOUC appears. **Q:** When does Tailwind beat CSS-in-JS on bundle size? **A:** For static apps almost always - Tailwind purges unused utilities and ships ~10KB with no runtime. Styled-components adds ~20KB of JS that must execute before styles appear. In a highly dynamic app with hundreds of unique runtime-generated styles, total CSS from CSS-in-JS can be smaller because unused rules simply do not exist. **Q:** How do you share a CSS-in-JS style cache across micro-frontends using Module Federation? **A:** Expose `StyleSheet.getInstance()` as a singleton via the webpack provider. Without this, each micro-frontend initializes its own cache and duplicate rules get injected. This pattern is used in Nx monorepos with shared Emotion instances. **Q:** Why does switching to zero-runtime Linaria break dynamic props? **A:** Linaria extracts styles at build time, so it cannot evaluate runtime values like `props.color`. You replace dynamic styles with CSS custom properties (`var(--color)`) set inline on the element. You keep component-scoped class names but lose JS interpolation inside style definitions. ## Examples ### Basic: button with prop-based styles ```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>Save</Button> <Button>Cancel</Button> </ThemeProvider> ); } // Output: two buttons, unique hashed classes, both isolated from global CSS ``` `ThemeProvider` injects the theme via React context. Any styled component inside it reads `props.theme` without prop drilling. Change the theme object once and every component that uses it updates. ### Intermediate: memoized metric cards in a live dashboard ```jsx import { css } from '@emotion/react'; import { memo, useMemo } from 'react'; // Defined outside component - hash computed once, cached for the app lifetime 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 }) => { // Only recalculates when color changes, not on every parent re-render 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> ); }); // Usage in a real-time dashboard <MetricCard label="Revenue" value="$10k" color="#22c55e" /> <MetricCard label="Users" value="1.2k" color="#3b82f6" /> ``` `cardBase` hashes once on module load. `valueStyle` only recalculates when `color` actually changes. In a dashboard where 20 cards re-render every few seconds on incoming data, this pattern eliminates the bulk of unnecessary DOM writes. ### Advanced: SSR style extraction in 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, // Styles injected into <head> before client-side hydration styles: [initialProps.styles, sheet.getStyleElement()], }; } finally { sheet.seal(); } } } ``` Without `ServerStyleSheet`, Next.js sends HTML with the correct DOM structure but no styled-components class definitions. React hydrates, styles inject, and there is a visible flash. With this setup, the server collects every style rule during `renderPage`, ships them inline in `<head>`, and the client receives a fully styled document from the first byte.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.