Repository: next.js
Issue: Unexpected CSS Module Ordering in Dev/Prod When Using Tree-Shaking
Labels: linear: next,linear: turbopack,CSS

### 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:
```ts
import { CarouselButton } from '@segments/carousel';
```
to:
```ts
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](https://github.com/jantimon/reproduction-webpack-css-order/blob/side-effect/%40applications/base/dist/main.css) |
| vite | ✅ Button → Teaser → TeaserButton | ✅ Excludes unused.module.css | [index.css](https://github.com/jantimon/reproduction-webpack-css-order/blob/vite-with-side-effect/%40applications/base/dist/index.css) |
| parcel | ✅ Button → Teaser → TeaserButton | ✅ Excludes unused.module.css | [index.5ff2b6c6.css](https://github.com/jantimon/reproduction-webpack-css-order/blob/parcel-with-side-effect/%40applications/base/dist/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:
```ts
// @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:
```css
.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:

[`sideEffects: true` example](https://github.com/jantimon/reproduction-webpack-css-order/tree/turbo-side-effects-true/%40applications/base)

```mermaid
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
```

[no barrel example](https://github.com/jantimon/reproduction-webpack-css-order/tree/turbo-no-barrel/%40applications/base)

```mermaid
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
```

[`sideEffects:false` example](https://github.com/jantimon/reproduction-webpack-css-order/tree/turbo/%40applications/base)

```mermaid
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

```bash
any
```


### Which 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:
https://github.com/webpack/webpack/issues/18961