You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Many people have reported that the order of CSS in Webpack is inconsistent with their expectations, leading to the problem of incorrect styles. In fact, in Webpack, the order of CSS is related to splitChunks and is very unstable. A JS module might import a.css before b.css, but after configuring splitChunks, the final order of a and b may be completely inconsistent with their import order.
A Little Example
First, let's look at the code.
import"./a.css";// a.css: body { background: red }import"./b.css";// b.css: body { background: blue }
We see that the JS file first imports a.css to turn the browser background red, then it imports b.css to change the background to blue.
Up to this point, according to our experience, the final page should be blue, right? Because b.css should override the styles of a.css.
Well, it's indeed blue. Next, I want to configure splitChunks to put a.css and b.css into different chunks.
We use mini-css-extract-plugin to handle CSS and configure splitChunks to put a.css and b.css in different chunks.
Then we build again, open the page, and you will miraculously find that the page has turned red!
For more examples, refer to the repository created by ahabhgk who is one of our core team members to illustrate how uncertain the order of CSS in Webpack with SplitChunks can be. The above code comes from that repository as well.
Next, I will explain these examples:
Here's a table:
Import represents how CSS is handled, with options of style-loader, mini-css-extract-plugin, and Webpack's own experiments.css (that's right, Webpack can actually package CSS without any loader or plugin🤩)
CSS represents how the style.js file mentioned above is imported by the entry file. If it's import './shared/style.js' then it's static; if it's import('./shared/style.js') then it's dynamic.
splitChunks.chunks is about which chunks should be split—a value of all means any chunk can be split, while async means only chunks created by dynamic imports can be split.
splitChunks.priority represents the priority within splitChunks' cacheGroups, meaning if a module meets multiple cacheGroups, it will be decided by priority which group will handle it. Here, if a is greater or less than b, it corresponds to the priority relationship between a and b in cacheGroups.
Color refers to the final color of the page, whether it is blue or red.
From the above table, it is simply not possible to know whether the final page will be blue or red 😅
Prerequisite Knowledge: CSS Processing Methods
I will first introduce three common methods of handling CSS in Webpack. If you are already farmilliar with those, please skip ahead.
In Webpack, there are generally 3 usual ways to handle CSS files.
style-loader + css-loader
mini-css-extract-webpack-plugin + css-loader
experiments.css
Let me first introduce the differences between these three methods.
css-loader
css-loader has postcss built-in. The purpose of this loader is simple: to parse CSS syntax and resolve css-modules. It is not responsible for how CSS is ultimately displayed on the page. This loader is the foundation for many CSS processing loaders. It accepts a CSS string and returns a JS string, which exports the CSS content, mappings for css-modules names, etc.
style-loader
style-loader is used to insert CSS into the page. This loader creates style tags at runtime and then inserts CSS into the page through these tags. This loader injects into the page at runtime, i.e., when the corresponding CSS module is executed, it will be inserted into the page. The order of CSS insertion is completely consistent with the import order. Therefore, in the color table above, using style-loader, the final page color is completely stable and always blue.
mini-css-extract-plugin
This plugin, in contrast to style-loader which dynamically inserts style tags at runtime, combines CSS files into chunks during packaging and eventually outputs them to the product. If you have configured splitChunks, then chunks comprised of CSS can also go through your personalized package-splitting process.
experiments.css
This is a Webpack's own CSS handling feature. Once enabled, CSS is treated as a first-class citizen in Webpack, with built-in support, and it will also go through your personalized package splitting. Overall, it's quite similar to mini-css-extract-plugin. In fact, for some simple demo projects, you can directly enable experiments.css without installing other loaders or plugins like css-loader or style-loader.
Prerequisite Knowledge: SplitChunks
Before we start, we also need to briefly introduce the principle of splitChunks. If you understand the concept of Chunk Group, you can skip this part.
It is necessary to differentiate between Code Splitting and splitChunks. Code splitting is a feature native to Webpack, which uses the import('package') statement to move certain modules to a new Chunk. Configuring splitChunks will not affect the code splitting strategy.
SplitChunks is essentially a further splitting of the Chunks produced by code splitting.
Chunk Group
In Webpack, there's a concept called Chunk Group. When we talk about SplitChunks and packaging, we're talking about splitting a Chunk into a bunch of smaller Chunks to form a group, i.e., a ChunkGroup for loading. We can customize maxRequests to control how many Chunks the browser can request at once, that is, how many Chunks each ChunkGroup can contain.
After code splitting, many Chunks will be created, and each Chunk will correspond to one ChunkGroup. SplitChunks is essentially splitting Chunk into more Chunks to form a group and to load groups together, for example, under HTTP/2, a Chunk could be split into a group of 20 Chunks for simultaneous loading.
In Webpack, loading and executing of javascript modules are two separate phases; the same module can appear in multiple Chunks but will only be executed once. You can freely split some modules from a Chunk to form a new Chunk.
On the other hand, lighter weight runtime build tools like Rollup or Esbuild, although they have code splitting, their products execute upon loading, and therefore their flexibility in splitting chunks is more limited compared to Webpack.
The Principle of CSS Order Instability
After the introduction, let’s explain the table regarding the instability of CSS order one by one.
Using style-loader
First, notice that in the table, in scenarios where style-loader is used, the final page color is consistently blue. This is because the style-loader essentially turns CSS into JS modules, which dynamically insert style tags at runtime. Therefore, using style-loader ensures the final order of CSS is stably consistent with the order in which the CSS modules are imported.
Using experiments.css and mini-css-extract-plugin
The principles behind these two are actually similar and can be discussed together.
First, look at these three lines. The first is marked as static, meaning the entry module statically imports the style.js module. 'all' indicates that splitChunks.chunks is configured as 'all', meaning this package-splitting configuration will affect all Chunks. Equals sign means that the two cacheGroups have the same priority, with the final page showing red.
The import relationship is as follows:
The splitChunks configuration is as follows:
The final resulting ChunkGroup and Chunks are as follows:
As we can see, a.css and b.css have been split by splitChunks from the main chunk. They are in the same group, so they will be loaded together. Now, the problem arises: when loading together, there is always an order, but how is the order in the same group determined?
The answer is there is no guarantee.
The principle of SplitChunks is to first iterate through all the modules and find all the CacheGroups rules that the module satisfies.
a.css corresponds to cacheGroups['a'] b.css corresponds to cacheGroups['b']
Then, the modules are grouped according to their corresponding CacheGroups, and each group will eventually form a Chunk.
In Webpack, the grouping strategy also takes into account factors such as the Chunk to which the module belongs, and the collection of Chunks to which the module belongs.
Then the Chunk will be split based on the selected group, and the strategy for selecting a group is as follows:
If the cacheGroup has a priority, select the group with the highest priority to start the split.
Based on the number of Chunks in the group, select the group with the most Chunks in the current Map to start the split.
Based on the product of Size and the number of chunks minus one, select the group with the largest Size composed of modules in the Map to start the split.
When a module satisfies multiple CacheGroups, there will be an index, and the split with the smallest index will be selected.
Based on the number of modules, select the group with the most modules to start the split.
Sorted based on the module names.
In our example, since we didn't configure any priority and both a.css and b.css belong to the same number of chunks, which is 1, their total size multiplied by the size of chunk-1 is 0. Since both of their cacheGroups only satisfy one condition, the index is also 0, and the module count is the same. Therefore, the final decision will be based on the sorting of module names.
The group where b ends up is split first. When splitting, the chunk that is split first will be placed at the front.
Then, group a is split. After splitting:
The order in which the chunks are loaded in the final page is determined by the order of the chunks in the ChunkGroup. In this case, b.css is loaded first, followed by a.css, resulting in a red-colored page.
Now, let's consider the second scenario. With all other conditions unchanged, both a and b have priorities set, and the priority of a is higher than b. This means that a is split first, followed by b, resulting in a blue-colored page.
Lastly, in the third scenario, b has a higher priority than a. Therefore, b is split first, resulting in a red-colored page.
The next three items are:
Since splitChunks.chunks is configured as async, it will only split chunks created by import(). Since a.css and b.css both belong to the main chunk, which is the entry chunk, they are not split by splitChunks. As a result, a and b are in the same chunk, and their order is guaranteed.
Now let's look at the next few items. By changing the imports to dynamic imports, they will all go through chunk splitting. Therefore, the result is the same as our analysis of the first three items.
Finally, mini-css-extract-plugin and experiments are similar, so their results are the same.
Summary
The problem is that loading CSS will also have side effects, namely the application of styles. If splitChunks is used to put certain CSS into specific files, it will inevitably disrupt the original order, and it is difficult to automatically infer the desired result in terms of order. This leads to a disruption of the order of side effects (attach style). The fundamental solution is to separate the loading of CSS from the attaching of styles. Using style-loader is an imperfect but doable solution as it allows for mounting CSS styles during JS execution but results in runtime overhead and optimizations such as splitChunks and minimize do not apply to CSS and are not friendly for Server Side Rendering.
Webpack's issue on supporting experiments.css mentions 5 ways to import CSS, two of which can better solve this problem:
import stylesheet from "./style.css" asset { type: "css" };, which refers to Native CSS Module (currently being promoted as a standard). Importing using this method only loads the CSS, while attaching styles requires developers to manually invoke document.adoptedStyleSheets.push(stylesheet);.
new URL("./style.css", import.meta.url);, which is already a compliant writing format. Compared with Native CSS Module, it is more primitive as it only resolves the URL for styles, requiring developers to manually initiate network requests for loading styles.
However, both methods require developers to handle more tasks themselves and are more redundant in terms of syntax compared with commonly used import './style.css';'. It should be noted that import './style.css'; does not follow with specifications.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
Webpack CSS Order Issue
chinese version
Foreword
Many people have reported that the order of CSS in Webpack is inconsistent with their expectations, leading to the problem of incorrect styles. In fact, in Webpack, the order of CSS is related to splitChunks and is very unstable. A JS module might import a.css before b.css, but after configuring splitChunks, the final order of a and b may be completely inconsistent with their import order.
A Little Example
First, let's look at the code.
We see that the JS file first imports
a.css
to turn the browser background red, then it importsb.css
to change the background to blue.Up to this point, according to our experience, the final page should be blue, right? Because
b.css
should override the styles ofa.css
.Well, it's indeed blue. Next, I want to configure splitChunks to put
a.css
andb.css
into different chunks.We use mini-css-extract-plugin to handle CSS and configure splitChunks to put
a.css
andb.css
in different chunks.Then we build again, open the page, and you will miraculously find that the page has turned red!
For more examples, refer to the repository created by ahabhgk who is one of our core team members to illustrate how uncertain the order of CSS in Webpack with SplitChunks can be. The above code comes from that repository as well.
Next, I will explain these examples:
Here's a table:
style.js
file mentioned above is imported by the entry file. If it'simport './shared/style.js'
then it's static; if it'simport('./shared/style.js')
then it's dynamic.all
means any chunk can be split, whileasync
means only chunks created by dynamic imports can be split.From the above table, it is simply not possible to know whether the final page will be blue or red 😅
Prerequisite Knowledge: CSS Processing Methods
I will first introduce three common methods of handling CSS in Webpack. If you are already farmilliar with those, please skip ahead.
In Webpack, there are generally 3 usual ways to handle CSS files.
style-loader
+css-loader
mini-css-extract-webpack-plugin
+css-loader
experiments.css
Let me first introduce the differences between these three methods.
css-loader
css-loader
has postcss built-in. The purpose of this loader is simple: to parse CSS syntax and resolve css-modules. It is not responsible for how CSS is ultimately displayed on the page. This loader is the foundation for many CSS processing loaders. It accepts a CSS string and returns a JS string, which exports the CSS content, mappings for css-modules names, etc.style-loader
style-loader
is used to insert CSS into the page. This loader creates style tags at runtime and then inserts CSS into the page through these tags. This loader injects into the page at runtime, i.e., when the corresponding CSS module is executed, it will be inserted into the page. The order of CSS insertion is completely consistent with the import order. Therefore, in the color table above, usingstyle-loader
, the final page color is completely stable and always blue.mini-css-extract-plugin
This plugin, in contrast to
style-loader
which dynamically inserts style tags at runtime, combines CSS files into chunks during packaging and eventually outputs them to the product. If you have configured splitChunks, then chunks comprised of CSS can also go through your personalized package-splitting process.experiments.css
This is a Webpack's own CSS handling feature. Once enabled, CSS is treated as a first-class citizen in Webpack, with built-in support, and it will also go through your personalized package splitting. Overall, it's quite similar to
mini-css-extract-plugin
. In fact, for some simple demo projects, you can directly enableexperiments.css
without installing other loaders or plugins likecss-loader
orstyle-loader
.Prerequisite Knowledge: SplitChunks
Before we start, we also need to briefly introduce the principle of splitChunks. If you understand the concept of Chunk Group, you can skip this part.
It is necessary to differentiate between Code Splitting and splitChunks. Code splitting is a feature native to Webpack, which uses the
import('package')
statement to move certain modules to a new Chunk. Configuring splitChunks will not affect the code splitting strategy.SplitChunks is essentially a further splitting of the Chunks produced by code splitting.
Chunk Group
In Webpack, there's a concept called Chunk Group. When we talk about SplitChunks and packaging, we're talking about splitting a Chunk into a bunch of smaller Chunks to form a group, i.e., a ChunkGroup for loading. We can customize maxRequests to control how many Chunks the browser can request at once, that is, how many Chunks each ChunkGroup can contain.
After code splitting, many Chunks will be created, and each Chunk will correspond to one ChunkGroup. SplitChunks is essentially splitting Chunk into more Chunks to form a group and to load groups together, for example, under HTTP/2, a Chunk could be split into a group of 20 Chunks for simultaneous loading.
In Webpack, loading and executing of javascript modules are two separate phases; the same module can appear in multiple Chunks but will only be executed once. You can freely split some modules from a Chunk to form a new Chunk.
On the other hand, lighter weight runtime build tools like Rollup or Esbuild, although they have code splitting, their products execute upon loading, and therefore their flexibility in splitting chunks is more limited compared to Webpack.
The Principle of CSS Order Instability
After the introduction, let’s explain the table regarding the instability of CSS order one by one.
Using style-loader
First, notice that in the table, in scenarios where
style-loader
is used, the final page color is consistently blue. This is because thestyle-loader
essentially turns CSS into JS modules, which dynamically insert style tags at runtime. Therefore, usingstyle-loader
ensures the final order of CSS is stably consistent with the order in which the CSS modules are imported.Using experiments.css and mini-css-extract-plugin
The principles behind these two are actually similar and can be discussed together.
First, look at these three lines. The first is marked as static, meaning the entry module statically imports the
style.js
module. 'all' indicates thatsplitChunks.chunks
is configured as 'all', meaning this package-splitting configuration will affect all Chunks. Equals sign means that the two cacheGroups have the same priority, with the final page showing red.The import relationship is as follows:
The splitChunks configuration is as follows:
The final resulting ChunkGroup and Chunks are as follows:
As we can see, a.css and b.css have been split by splitChunks from the main chunk. They are in the same group, so they will be loaded together. Now, the problem arises: when loading together, there is always an order, but how is the order in the same group determined?
The answer is there is no guarantee.
The principle of SplitChunks is to first iterate through all the modules and find all the CacheGroups rules that the module satisfies.
a.css
corresponds to cacheGroups['a']b.css
corresponds to cacheGroups['b']Then, the modules are grouped according to their corresponding CacheGroups, and each group will eventually form a Chunk.
In Webpack, the grouping strategy also takes into account factors such as the Chunk to which the module belongs, and the collection of Chunks to which the module belongs.
The result after grouping is similar to:
Then the Chunk will be split based on the selected group, and the strategy for selecting a group is as follows:
In our example, since we didn't configure any priority and both a.css and b.css belong to the same number of chunks, which is 1, their total size multiplied by the size of chunk-1 is 0. Since both of their cacheGroups only satisfy one condition, the index is also 0, and the module count is the same. Therefore, the final decision will be based on the sorting of module names.
The group where
b
ends up is split first. When splitting, the chunk that is split first will be placed at the front.Then, group
a
is split. After splitting:The order in which the chunks are loaded in the final page is determined by the order of the chunks in the
ChunkGroup
. In this case,b.css
is loaded first, followed bya.css
, resulting in a red-colored page.Now, let's consider the second scenario. With all other conditions unchanged, both
a
andb
have priorities set, and the priority ofa
is higher thanb
. This means thata
is split first, followed byb
, resulting in a blue-colored page.Lastly, in the third scenario,
b
has a higher priority thana
. Therefore,b
is split first, resulting in a red-colored page.The next three items are:
Since
splitChunks.chunks
is configured asasync
, it will only split chunks created byimport()
. Sincea.css
andb.css
both belong to the main chunk, which is the entry chunk, they are not split bysplitChunks
. As a result,a
andb
are in the same chunk, and their order is guaranteed.Now let's look at the next few items. By changing the imports to dynamic imports, they will all go through chunk splitting. Therefore, the result is the same as our analysis of the first three items.
Finally,
mini-css-extract-plugin
andexperiments
are similar, so their results are the same.Summary
The problem is that loading CSS will also have side effects, namely the application of styles. If splitChunks is used to put certain CSS into specific files, it will inevitably disrupt the original order, and it is difficult to automatically infer the desired result in terms of order. This leads to a disruption of the order of side effects (attach style). The fundamental solution is to separate the loading of CSS from the attaching of styles. Using style-loader is an imperfect but doable solution as it allows for mounting CSS styles during JS execution but results in runtime overhead and optimizations such as splitChunks and minimize do not apply to CSS and are not friendly for Server Side Rendering.
Webpack's issue on supporting experiments.css mentions 5 ways to import CSS, two of which can better solve this problem:
import stylesheet from "./style.css" asset { type: "css" };
, which refers to Native CSS Module (currently being promoted as a standard). Importing using this method only loads the CSS, while attaching styles requires developers to manually invokedocument.adoptedStyleSheets.push(stylesheet);
.new URL("./style.css", import.meta.url);
, which is already a compliant writing format. Compared with Native CSS Module, it is more primitive as it only resolves the URL for styles, requiring developers to manually initiate network requests for loading styles.However, both methods require developers to handle more tasks themselves and are more redundant in terms of syntax compared with commonly used
import './style.css';'
. It should be noted thatimport './style.css';
does not follow with specifications.Beta Was this translation helpful? Give feedback.
All reactions