DEV Community

Cover image for CSS-in-JS: Why is this bad?
Valerii Smirnov
Valerii Smirnov

Posted on

CSS-in-JS: Why is this bad?

This article presumes your familiarity with various styling methods.

CSS-in-JS: Solutions it Aims to Provide:

  • Scoped styles: Prevents style leakage to unrelated components.
  • Dynamic styles: Enables style adaptability based on properties, state, or other dynamic data.
  • Collocation: Enhances maintainability by co-locating styles and components.

The Mechanics of CSS-in-JS:

  • Parsing styles: Transforms styles from JavaScript objects or tagged template literals into CSS format.
  • Generating unique class names: Crafts a unique, hash-based class name for each style set, scoping them to designated components.
  • Handling dynamic styles: Updates styles based on component properties or state changes, generates new class names if required, and injects the updated styles into the DOM.
  • Injecting styles into the DOM: Constructs a <style> element, attaches it to the <head>, and updates its content with the generated CSS. This action prompts the browser to recalculate styles.
  • Managing the CSS cache: Upholds a cache of generated styles to enhance performance and prevent unnecessary re-rendering.
  • Server-side rendering: Extracts generated styles on the server and includes them in the initial HTML payload.
// Simplified CSS-in-JS implementation function parseStyles(styles) {...} function generateClassName(styles) {...} function injectStyles(css) {...} function updateDynamicStyles(component) {...} const styles = {color: "red", fontSize: "14px"}; const css = parseStyles(styles); const className = generateClassName(styles); injectStyles(css); element.className = className; updateDynamicStyles(component); 
Enter fullscreen mode Exit fullscreen mode

The Hurdles of styled HOC

At its core, styled offers a way to encapsulate styles within HTML elements.

const Button = styled.button` /* ... */ `; 
Enter fullscreen mode Exit fullscreen mode

However, this approach hinders style reusability across elements. Libraries offer the as prop as a solution, but this often leads to poor abstractions and complicates TypeScript typing.

Check this example from styled-components types:

export interface ThemedStyledFunction< C extends keyof JSX.IntrinsicElements | React.ComponentType<any>, T extends object, O extends object = {}, A extends keyof any = never > extends ThemedStyledFunctionBase<C, T, O, A> { // Fun thing: 'attrs' can also provide a polymorphic 'as' prop // My head already hurts enough so maybe later... attrs< U, NewA extends Partial<StyledComponentPropsWithRef<C> & U> & { [others: string]: any; } = {} >( attrs: Attrs<StyledComponentPropsWithRef<C> & U, NewA, T> ): ThemedStyledFunction<C, T, O & NewA, A | keyof NewA>; // ... } 
Enter fullscreen mode Exit fullscreen mode

Alternatively, we could utilize classes, circumventing the ordeal of identifying or modifying component HTML elements.

<Button as="a" />; // vs <> <button className={styles.button} /> {/* WOW: classes are making a comeback in frontend development */} <button className={clsx(styles.button, styles.hoverable)} /> <a className={styles.button} /> </>; 
Enter fullscreen mode Exit fullscreen mode

Issues with CSS-in-JS:

  • Overhead due to runtime transformations
  • Slower rendering phase compared to traditional CSS
  • Additional CSS parser load on the browser
  • Repeated CSS parsing and injection when values change

Measuring Performance Overhead:

This benchmark was used in styled-components repo for performance overview (original repo).
But it was't fair enough because, styled-components used inline styles for dynamic styles, that can't tell
anything about performance of dynamic styles.

How i fixed this issue

Original version:

const Dot = styled(View).attrs((p) =&gt; ({ style: { borderBottomColor: p.color }, }))` position: absolute; cursor: pointer; width: 0; height: 0; border-color: transparent; border-style: solid; border-top-width: 0; transform: translate(50%, 50%); margin-left: ${(props) =&gt; `${props.x}px`}; margin-top: ${(props) =&gt; `${props.y}px`}; border-right-width: ${(props) =&gt; `${props.size / 2}px`}; border-bottom-width: ${(props) =&gt; `${props.size / 2}px`}; border-left-width: ${(props) =&gt; `${props.size / 2}px`}; `; 
Enter fullscreen mode Exit fullscreen mode

So i just replaced inline styles with styled

const Dot = styled(View)` /* ... */ border-bottom-color: ${(props) =&gt; props.color}; /* ... */ `; 
Enter fullscreen mode Exit fullscreen mode

Overhead can range from [0.5x-2x] depending on the device or library, but they all use the same technique for styling

Benchmark was executed on a laptop with Ryzen 4600h, GTX 1650, 32 GB RAM.

Benchmark results:
CSS in JS vs CSS

Feel free to experiment with the benchmark playground: here

source code

Fixed benchmark results:

CSS in JS vs CSS

Benchmark Overhead of CSS in JS (styled-components)
Mounting deep tree 20%
Mounting wide tree 13.1%
Updating dynamic 489.1%
Updating static 68.8%

The performance of styled-components decreases fourfold when borderBottomColor is shifted from inline styles to styled.

Solutions:

  • Limit the dynamic nature of style templates
  • Utilize selectors and variables for dynamic components
  • Transition to build-time CSS-in-JS libraries, such as Linaria
  • For greenfield projects, contemplate the utility CSS approach (like Tailwind)

Benefits of Utility CSS Approach (Tailwind):

  • Framework agnostic
  • JIT compilation of classes for build-time dynamic classes
  • Faster performance
  • Transparent, zero-abstraction styling
  • Easy setup in any environment
  • Less code and adaptive abstractions

Top comments (1)

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍