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

CSS Module Order Changes When Using sideEffects: false with Barrel Files #18961

Open
jantimon opened this issue Nov 12, 2024 · 13 comments
Open
Labels

Comments

@jantimon
Copy link
Contributor

jantimon commented Nov 12, 2024

Connected Issues

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

  1. The issue is specific to webpack 5 - other bundlers (rspack, vite, parcel) handle this correctly (see links in the end)
  2. The problem occurs when all these conditions are met:
    • Using barrel files (index.ts) for imports
    • Having sideEffects: false in package.json
    • Using CSS modules (probably also normal css)
    • Being in a monorepo setup
  3. 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:

const refOrdinal = /** @type {number} */ getModuleOrdinal(refModule);
const activeState = /** @type {ConnectionState} */ (
blockModules[i + 1]
);
if (activeState !== true) {
const connections = /** @type {ModuleGraphConnection[]} */ (
blockModules[i + 2]
);
skipConnectionBuffer.push([refModule, connections]);
// We skip inactive connections
if (activeState === false) continue;
} else if (isOrdinalSetInMask(minAvailableModules, refOrdinal)) {
// already in parent chunks, skip it for now
skipBuffer.push(refModule);
continue;
}
// enqueue, then add and enter to be in the correct order
// this is relevant with circular dependencies
queueBuffer.push({
action: activeState === true ? ADD_AND_ENTER_MODULE : PROCESS_BLOCK,
block: refModule,
module: refModule,
chunk,
chunkGroup,
chunkGroupInfo
});

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
Loading

Working Variations

There are two workarounds:

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:

  1. Having this import structure:
// @libraries/teaser/src/teaser.ts
import { CarouselButton } from '@segments/carousel'; // via barrel file
import styles from './teaser.module.css';
  1. Building with sideEffects: false produces 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 { ... }
  1. Building with "sideEffects": ["*.css"] produces incorrect CSS order ❌

See also main.css

.hDE5PT5V3QGAPX9o9iZl { ... }
.yqrxTjAG22vkATE1VjR9 { background-color: orange; }  /* ❌ Should be last */
.R_y25aX9lTSLQtlxA1c9 { ... }
  1. Building with sideEffects: true produces correct order ✅

See also main.css

.R_y25aX9lTSLQtlxA1c9 { ... }
.hDE5PT5V3QGAPX9o9iZl { ... }
.yqrxTjAG22vkATE1VjR9 { background-color: orange; }
  1. Building without a barrel file with sideEffects: false also 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:

  • css built by rspack ✅
  • css built by vite ✅
  • css by parcel ✅

Environment

  • Webpack 5.96.1
  • Node.js >= 20
  • pnpm 8.15.4
@devbrains-com
Copy link

devbrains-com commented Nov 12, 2024

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.

vercel/next.js#64921
vercel/next.js#70087

@alexander-akait
Copy link
Member

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 #14893 (I will add test). We can also fix this here - https://github.com/webpack-contrib/mini-css-extract-plugin.

@jantimon
Copy link
Contributor Author

Ah super interesting!
That might also explain why all other bundlers work but Turbopack shows exactly the same behaviour: project__53bcfa._.css

@alexander-akait
Copy link
Member

/cc @ahabhgk is rspack solve this problem? or just don't support it sideEffects?

@jantimon
Copy link
Contributor Author

@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

@alexander-akait
Copy link
Member

alexander-akait commented Nov 12, 2024

@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.

@alexander-akait
Copy link
Member

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.

@alexander-akait
Copy link
Member

Also - #15610 (comment)

@alexander-akait
Copy link
Member

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

@jantimon
Copy link
Contributor Author

jantimon commented Nov 13, 2024

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)

@jantimon
Copy link
Contributor Author

jantimon commented Nov 13, 2024

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

@ahabhgk
Copy link
Contributor

ahabhgk commented Nov 13, 2024

After update Rspack to v1.1.1 (>=1.0) it should have the same result as webpack ahabhgk/reproduction-webpack-css-order@835fc43

@jantimon
Copy link
Contributor Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants