DEV Community

Cover image for Optimizing Tailwind CSS for React Applications: Our Journey
Ali Navidi
Ali Navidi

Posted on • Edited on

5 2 2 2 2

Optimizing Tailwind CSS for React Applications: Our Journey

When we began restructuring our website, the frontend team chose the latest Next.js version paired with Tailwind CSS v4. This decision marked the beginning of an optimization journey that ultimately led us to a solution that works well for our specific use case.

Why We Chose Tailwind

Our design system contains numerous variables that needed consistent implementation across the site. For our team, Tailwind CSS made sense because:

  • We could easily export design tokens and convert them to Tailwind variables using style-dictionary
  • The ecosystem provided excellent tooling and documentation for our needs

Our Struggle with Utility-First Philosophy

While Tailwind's utility-first approach works well for many teams, we quickly identified challenges in our specific context:

  • Component JSX became cluttered with lengthy class strings
  • Readability suffered with our particularly complex components
  • Our team found code reviews more difficult with so much styling in the markup

Our Styling Journey

First Approach: CSS Modules with @apply

Initially, we created CSS modules for each component and used @apply directives:

/* Button.module.css */
.primary {
 @apply bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded;
}
Enter fullscreen mode Exit fullscreen mode
import styles from './Button.module.css';

const Button = () => <button className={styles.primary}>Click Me</button>;
Enter fullscreen mode Exit fullscreen mode

The CSS Modules Concerns

While this approach improved our component readability significantly, we encountered several concerns that made us question this strategy:

  1. Tailwind's official recommendations

    • The Tailwind documentation explicitly advises against heavy @apply usage
    • It states this approach works against Tailwind's utility-first philosophy
    • The docs suggest it can create abstraction layers that reduce maintainability
  2. Potential build performance impact

    • We read that CSS modules might increase build times
    • Each module requires separate processing during compilation
    • For large applications with many components, this could add up significantly
  3. Possible interference with optimization

    • The @apply directive might interfere with Tailwind's unused CSS removal
    • Utilities used within @apply aren't directly visible in HTML for purging
    • This could potentially lead to larger than necessary CSS bundles

These concerns, particularly around build performance as our component library grew, pushed us to explore alternatives.

Second Approach: Regular CSS Files with @apply

After reading that CSS modules might impact build times, we switched to regular CSS files:

/* button.css */
.primary {
 @apply bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded;
}
Enter fullscreen mode Exit fullscreen mode
import './button.css';

const Button = () => <button className="primary">Click Me</button>;
Enter fullscreen mode Exit fullscreen mode

The Global CSS Problem

This approach revealed a significant issue we hadn't anticipated. Unlike CSS modules which scope styles to specific components, these regular CSS files created global styles that were bundled into our main CSS file.

The consequences were severe for our application:

  • No tree-shaking of unused styles - Our bundler couldn't determine which styles were actually needed
  • Rapidly growing CSS file - As we added more components, our CSS file grew disproportionately large
  • Potential naming conflicts - Without CSS modules' automatic unique class generation, we risked style collisions

This approach created an unacceptably large CSS file that would impact initial page loads and hurt our performance metrics. Users would download styles for components they'd never see.

Final Decision: The Real Trade-Off

After experimenting with different approaches, we realized we faced a fundamental choice between two viable options:

Option 1: Inline Utility Classes

const Button = () => (
  <button className="bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded">
    Click Me
  </button>
);
Enter fullscreen mode Exit fullscreen mode

Option 2: CSS Modules with @apply

/* Button.module.css */
.primary {
  @apply bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded;
}
Enter fullscreen mode Exit fullscreen mode
import styles from './Button.module.css';
const Button = () => <button className={styles.primary}>Click Me</button>;
Enter fullscreen mode Exit fullscreen mode

Comparing the Trade-offs

Consideration Inline Utility Classes CSS Modules with @apply
CSS Bundle Size Smaller (only used utilities) Slightly larger (extracted classes)
HTML/JSX Size Larger (verbose class strings) Smaller (single class names)
Build Time Faster Slower
Component Readability Lower Higher
Class Name Conflicts None None (scoped by module)

Making Our Decision

We analyzed these trade-offs carefully and realized that the choice came down to:

  1. Slightly smaller CSS bundle + larger HTML with inline utilities
  2. Slightly larger CSS bundle + smaller HTML with CSS modules

An important insight emerged during our analysis: CSS is downloaded once and cached by the browser, while HTML is re-downloaded with each page request. Since we operate a high-traffic website with many repeat visitors, reducing HTML size would benefit user experience more than minimizing CSS size.

Given this context, we decided to return to CSS modules with selective @apply usage, accepting the trade-off of slightly longer build times and marginally larger CSS for the benefits of smaller HTML payloads and improved code maintainability.

Conclusion

This was the right decision for our specific needs, though we recognize other teams with different requirements might make different choices. Tailwind is flexible enough to support various implementation styles, and we chose what works best for our specific needs, team preferences, and user base.

Our journey helped us understand that there's no one-size-fits-all approach to CSS architecture even within the Tailwind ecosystem. What matters most is making intentional decisions based on your specific constraints and priorities.

Special thanks to: @mohamadreza_dakhili_f3f04

Top comments (0)

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay