CSS Module Order Changes When Using `sideEffects: false` with Barrel Files
Connected Issues
- #7094 (CSS Order Differs Between Development & Production Modes when Treeshaking)
Description
When using CSS Modules in a monorepo setup with barrel files (index.ts) and sideEffects: false, webpack produces an incorrect CSS ordering in the output bundle. This might be connected to the long-standing CSS ordering issues with tree-shaking
Key Findings
- The issue is specific to webpack 5 - other bundlers (rspack, vite, parcel) handle this correctly (see links in the end)
- The problem occurs when all these conditions are met:
- Using barrel files (index.ts) for imports
- Having
sideEffects: falsein package.json - Using CSS modules (probably also normal css)
- Being in a monorepo setup
- Setting
"sideEffects": ["*.css"]does NOT fix the issue - it behaves the same as"sideEffects": false
After some debugging we traced it to the build chunk graph logic:
https://github.com/webpack/webpack/blob/5e21745e98eb90a029e1f5374d4e4ac338fbe7c7/lib/buildChunkGraph.js#L683-L708
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
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
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
Working Variations
There are two workarounds:
- Set
"sideEffects": truein package.json
OR
- Import directly from implementation file instead of barrel:
// Instead of: import { CarouselButton } from '@segments/carousel'; import { CarouselButton } from '@segments/carousel/buttons';
But I believe we should fix it in webpack because it works in all other bundlers I tried (rspack, vite, parcel)
Reproduction
Full reproduction repository: https://github.com/jantimon/reproduction-webpack-css-order
The issue can be reproduced by:
- Having this import structure:
// @libraries/teaser/src/teaser.ts
import { CarouselButton } from '@segments/carousel'; // via barrel file
import styles from './teaser.module.css';
- Building with
sideEffects: falseproduces incorrect CSS order ❌
The repository has dist files checked in so you can see the full main.css
.hDE5PT5V3QGAPX9o9iZl { ... }
.yqrxTjAG22vkATE1VjR9 { background-color: orange; } /* ❌ Should be last */
.R_y25aX9lTSLQtlxA1c9 { ... }
- Building with
"sideEffects": ["*.css"]produces incorrect CSS order ❌
See also main.css
.hDE5PT5V3QGAPX9o9iZl { ... }
.yqrxTjAG22vkATE1VjR9 { background-color: orange; } /* ❌ Should be last */
.R_y25aX9lTSLQtlxA1c9 { ... }
- Building with
sideEffects: trueproduces correct order ✅
See also main.css
.R_y25aX9lTSLQtlxA1c9 { ... }
.hDE5PT5V3QGAPX9o9iZl { ... }
.yqrxTjAG22vkATE1VjR9 { background-color: orange; }
- Building without a barrel file with
sideEffects: falsealso produces correct order ✅
See also main.css
.R_y25aX9lTSLQtlxA1c9 { ... }
.hDE5PT5V3QGAPX9o9iZl { ... }
.yqrxTjAG22vkATE1VjR9 { background-color: orange; }
Comparison with Other Bundlers
Each bundler repository branch contains the same code structure and dependencies, only changing the build configuration for the respective bundler:
Environment
- Webpack 5.96.1
- Node.js >= 20
- pnpm 8.15.4
As we also see this issue in combination with Next.js, we initially thought it was a Next.js bug.
The following issues may be related and eventually also resolved by this fix.
https://github.com/vercel/next.js/issues/64921 https://github.com/vercel/next.js/pull/70087
This is not a webpack issue and unfortunately we can't fix it here. This applies to how we work with CSS (css -> js -> extract CSS) and to MiniCssExtractPlugin.
Bare JS imports are considered side free by default and can therefore be moved to any location in the file.
It should be fixed in https://github.com/webpack/webpack/issues/14893 (I will add test). We can also fix this here - https://github.com/webpack-contrib/mini-css-extract-plugin.
Ah super interesting!
That might also explain why all other bundlers work but Turbopack shows exactly the same behaviour: project__53bcfa._.css
/cc @ahabhgk is rspack solve this problem? or just don't support it sideEffects?
@alexander-akait I did a quick test (on the rspack branch of the reproduction)
I added a simple side effect:
@libraries/teaser/src/side-effect.ts
@@ -0,0 +1 @@
+console.log("hello world")
@libraries/teaser/src/index.ts
@@ -1 +1,2 @@
export * from './teaser';
+export * from "./side-effect"
then I compiled it twice - onece with package.json "sideEffects": true and once with "sideEffects": false
for the javascript part both bundlers rspack and webpack worked the same way: console.log("hello world") was only part of the bundle for "sideEffects": true
@jantimon I looked deeper into your example and I want to say that you have the wrong sideEffects settings. You can't have sideEffects: false in packages that import CSS and inject it into DOM, any DOM manipulations are sideEffects. That's why you got the wrong order.
I looked at other bundles and it seems they just don't support sideEffects or support it selectively.
If you want to achieve the absence of unnecessary initial requests, I recommend using import(...), if there are problems in some library you can set it at the loader level just using - https://webpack.js.org/configuration/module/#rulesideeffects.
Also - https://github.com/webpack/webpack/issues/15610#issuecomment-1085777006
Let me remind you that there is no such thing in js like import styles from './teaser.module.css';, this is processed by loaders and plugins and always automatically brought into the DOM and create side effects, so if you want to use sideEffects: false or sideEffects": ["*.css"] and have initial bare CSS most likely you made a mistake
I get your point about CSS imports being side effects and side effects in javascript should not affect the order, but I want to share why the current behavior is causing real headaches in production
In the reproduction repo, you can see that just changing or removing a barrel file (like @segments/carousel/index.ts) can change up the CSS order across the entire app without touching a single css file or import. This isn't just a small issue - in larger apps, this means a simple refactor can silently break styles in completely unrelated components because a class like .teaserCarouselButton class could appears in a different order in the final bundle
Other bundlers might handle sideEffects "less correct" for CSS, but at least they give us a consistent CSS order we can work with and understand. For me the "order can not be guaranteed" part just doesn't make much sense for CSS (even as a css modules user)
When discussing about those problems you very often hear things like just use tailwind or just use styled-components or just increase the specificity or just use !important or ensure that two CSS files never target the same dom element.
And although those might be options I believe vanilla CSS with simple understandable selectors should also be possible
For me the ideal tradeoff would be something in between - keep the good parts of JS tree-shaking but make CSS ordering predictable (like in rspack, vite and parcel)
I added a new unused export to @segments/carousel which is never used:
import styles from './unused.module.css';
export const CarouselUnused = ({
className = '',
}) => {
return `<div class="${styles.unused + (
className ? ` ${className}` : ''
)}">Unused</div>`;
};
This created the following results:
- Vite and Parcel achieve both goals: consistent ordering and proper treeshaking
- Only webpack and turbopack have unpredictable ordering
| 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 |
After update Rspack to v1.1.1 (>=1.0) it should have the same result as webpack https://github.com/ahabhgk/reproduction-webpack-css-order/commit/835fc430050c2f1626eeb1677772f56cf4be6f48
thanks @ahabhgk I removed rspack it from the table now that it has the exact same behaviour
please be aware that your fix will change the css order in a very unpredictable way and might therefore break websites - you should probably marked it as breaking change
Uff we're struggling with the same issue, not necessarily a typical barrel file.
A typography & card component, both from the same package, where the card component uses the typography component with some styling overrides. Both are used in the app, but the typography styling ends up after the card component styling, thus having a higher priority than the styling overrides from the card
graph TD;
Typography-->App;
Typography-->Card;
Card-->App;
ends up in
<Card styling>
<Typography styling>
<App styling>
instead of
<Typography styling>
<Card styling>
<App styling>
Pretty much the provided test case in the PR actually 😅
We also suffer the same issue.
This issue is major pain to use webpack with any ui library. Bundle becomes unreliable: you don't have any confidence in the order of css. Puzzled it labelled as "enhancement" where that's major if not crucial bug. Hoping fix will settle.
thank you for your excellent work. Is there a merge plan for this? We have encountered a similar issue.