Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unexpected CSS Module Ordering in Dev/Prod When Using Tree-Shaking #72846

Open
jantimon opened this issue Nov 15, 2024 · 1 comment
Open

Unexpected CSS Module Ordering in Dev/Prod When Using Tree-Shaking #72846

jantimon opened this issue Nov 15, 2024 · 1 comment
Labels
bug Issue was opened via the bug report template. linear: turbopack Confirmed issue that is tracked by the Turbopack team. Turbopack Related to Turbopack with Next.js. Webpack Related to Webpack with Next.js.

Comments

@jantimon
Copy link
Contributor

jantimon commented Nov 15, 2024

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:

sideEffects: true example

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
Loading

no barrel example

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
Loading

sideEffects:false example

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
Loading

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:

Provide environment information

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:
webpack/webpack#18961

@jantimon jantimon added the bug Issue was opened via the bug report template. label Nov 15, 2024
@github-actions github-actions bot added Turbopack Related to Turbopack with Next.js. Webpack Related to Webpack with Next.js. labels Nov 15, 2024
@sokra
Copy link
Member

sokra commented Nov 20, 2024

Currently a side-effect-free flagged import don't care about the import order anymore. The assumption behind that: It doesn't have a side effect so order shouldn't matter.

But I think we should change that (for JS and CSS). If a side-effect-free flagged module is imported, it should import it in the original order.

@timneutkens timneutkens added the linear: turbopack Confirmed issue that is tracked by the Turbopack team. label Nov 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Issue was opened via the bug report template. linear: turbopack Confirmed issue that is tracked by the Turbopack team. Turbopack Related to Turbopack with Next.js. Webpack Related to Webpack with Next.js.
Projects
None yet
Development

No branches or pull requests

3 participants