-
Notifications
You must be signed in to change notification settings - Fork 30.6k
Description
Link to the code that reproduces this issue
https://github.com/jantimon/reproduction-webpack-css-order
To Reproduce
Clone the repository and checkout the turbo branch
pnpm install
pnpm run dev
see that the button is blue (but should be orange)
Current vs. Expected behavior
While analyzing a CSS ordering problem in our monorepo, I traced it down to an interesting combination of module graph building and tree-shaking. The core of the issue appears to be in how the module graph handles CSS imports when sideEffects: false is set (or sideEffects: ["*.css"]
Looking at webpack's buildChunkGraph.js (https://github.com/webpack/webpack/blob/5e21745e98eb90a029e1f5374d4e4ac338fbe7c7/lib/buildChunkGraph.js#L683-L708), I found that the module traversal order changes once webpack is able to remove a barrel file.
That’s quite a bad DX for most developers because it means that the CSS order changes can be caused by JavaScript refactoring that seems completely unrelated to styles
Here's a concrete example from the reproduction - changing from:
import { CarouselButton } from '@segments/carousel';to:
import { CarouselButton } from '@segments/carousel/buttons';can unexpectedly reorder CSS across the entire application. This means that code cleanup like splitting up barrel files or moving components between packages can silently break styles in seemingly unrelated components.
I've done some testing across different bundlers to understand how they handle this scenario:
| Bundler | Consistent CSS Order | CSS Treeshaking | CSS Output |
|---|---|---|---|
| webpack | ❌ Order depends on barrel files & sideEffects | ✅ Excludes unused.module.css | main.css |
| vite | ✅ Button → Teaser → TeaserButton | ✅ Excludes unused.module.css | index.css |
| parcel | ✅ Button → Teaser → TeaserButton | ✅ Excludes unused.module.css | index.5ff2b6c6.css |
| turbopack | ❌ Order depends on barrel files & sideEffects | N/A (no production build tested) | N/A |
What's interesting is that both Vite and Parcel manage to maintain consistent CSS ordering while still being able to tree-shake. So we might be able to find a middle ground that keeps the benefits of tree-shaking and allows a consistent CSS order
To better understand the issue, I've created a minimal reproduction: https://github.com/jantimon/reproduction-webpack-css-order
The tricky part is that this only manifests when several conditions align:
// @libraries/teaser/src/teaser.ts
import { CarouselButton } from '@segments/carousel'; // via barrel file
import styles from './teaser.module.css';When building with sideEffects: false, the CSS order becomes unpredictable. Here's the output:
.hDE5PT5V3QGAPX9o9iZl { ... }
.yqrxTjAG22vkATE1VjR9 { background-color: orange; } /* Should be last */
.R_y25aX9lTSLQtlxA1c9 { ... }Here are the module graphs for the 3 scenarios.
The postOrder is the index which is used for the css order:
graph TD
subgraph "sideEffects: true ✅"
A2["@applications/base/src/index.ts preOrder: 0, postOrder: 8"]
B2["@libraries/teaser/src/index.ts preOrder: 1, postOrder: 7"]
C2["@libraries/teaser/src/teaser.ts preOrder: 2, postOrder: 6"]
D2["@segments/carousel/src/index.ts preOrder: 3, postOrder: 3"]
E2["@segments/carousel/src/buttons.ts preOrder: 4, postOrder: 2"]
F2["@segments/carousel/src/button.module.css preOrder: 5, postOrder: 1"]
G2["button.module.css|0|||}} preOrder: 6, postOrder: 0"]
H2["@libraries/teaser/src/teaser.module.css preOrder: 7, postOrder: 5"]
I2["teaser.module.css|0|||}} preOrder: 8, postOrder: 4"]
A2 --> B2
B2 --> C2
C2 --> D2
D2 --> E2
E2 --> F2
F2 --> G2
C2 --> H2
H2 --> I2
style A2 fill:#0a0a4a,stroke:#333
style F2 fill:#294b51,stroke:#333
style G2 fill:#294b51,stroke:#333
style H2 fill:#294b51,stroke:#333
style I2 fill:#294b51,stroke:#333
end
graph TD
subgraph "No Barrel ✅"
A3["@applications/base/src/index.ts preOrder: 0, postOrder: 6"]
B3["@libraries/teaser/src/teaser.ts preOrder: 1, postOrder: 5"]
E3["@segments/carousel/src/buttons.ts preOrder: 2, postOrder: 2"]
F3["@segments/carousel/src/button.module.css preOrder: 3, postOrder: 1"]
G3["button.module.css|0|||}} preOrder: 4, postOrder: 0"]
H3["@libraries/teaser/src/teaser.module.css preOrder: 5, postOrder: 4"]
I3["teaser.module.css|0|||}} preOrder: 6, postOrder: 3"]
A3 --> B3
B3 --> E3
E3 --> F3
F3 --> G3
B3 --> H3
H3 --> I3
style A3 fill:#0a0a4a,stroke:#333
style F3 fill:#294b51,stroke:#333
style G3 fill:#294b51,stroke:#333
style H3 fill:#294b51,stroke:#333
style I3 fill:#294b51,stroke:#333
end
graph TD
subgraph "sideEffects: false ❌"
A1["@applications/base/src/index.ts preOrder: 0, postOrder: 6"]
B1["@libraries/teaser/src/teaser.ts preOrder: 1, postOrder: 5"]
C1["@libraries/teaser/src/teaser.module.css preOrder: 2, postOrder: 1"]
D1["teaser.module.css|0|||}} preOrder: 3, postOrder: 0"]
E1["@segments/carousel/src/buttons.ts preOrder: 4, postOrder: 4"]
F1["@segments/carousel/src/button.module.css preOrder: 5, postOrder: 3"]
G1["button.module.css|0|||}} preOrder: 6, postOrder: 2"]
A1 --> B1
B1 --> C1
C1 --> D1
B1 --> E1
E1 --> F1
F1 --> G1
style A1 fill:#0a0a4a,stroke:#333
style C1 fill:#294b51,stroke:#333
style D1 fill:#294b51,stroke:#333
style F1 fill:#294b51,stroke:#333
style G1 fill:#294b51,stroke:#333
end
For me common suggestions like "just use Tailwind" or "increase specificity" miss the point - vanilla CSS with simple, understandable selectors should be an option. The unpredictable ordering creates harder to read code where developers need to constantly guard against CSS specificity bugs using &&& or !important.
The reproduction repo includes branches for different scenarios and bundlers, making it easy to verify the behavior:
- webpack with barrel files: https://github.com/jantimon/reproduction-webpack-css-order/tree/main
- webpack without barrel files: https://github.com/jantimon/reproduction-webpack-css-order/tree/no-barell
- For comparison vite & parcel branches
Provide environment information
anyWhich area(s) are affected? (Select all that apply)
Turbopack, Webpack
Which stage(s) are affected? (Select all that apply)
next dev (local), next build (local), next start (local)
Additional context
Related webpack issue:
webpack/webpack#18961