-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Build: Inline CSS modules stylesheet content #72673
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
Conversation
|
Warning: Type of PR label mismatch To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.
Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task. |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
| const jsonFiles = await glob( | ||
| normalizePath( path.join( packageDir, 'src/**/*.json' ) ), | ||
| const assetFiles = await glob( | ||
| normalizePath( | ||
| path.join( packageDir, `src/**/*.${ ASSET_EXTENSIONS }` ) | ||
| ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The build happens as separate passes:
- Transpile the original source into
packages/theme/build-module - Bundle the script into
build/scripts/theme
Evaluating the CSS modules happens at the bundling step, so the module.css files have to be available in the build-module output, hence the changes here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Evaluating the CSS modules happens at the bundling step, so the module.css files have to be available in the build-module output, hence the changes here.
This is something that I'm not sure about. I don't think we should force package users to have a build tool that understands CSS modules imports until this becomes a standard.
So for now, I believe inlining CSS within JS should ideally be done during the transpiling step.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll look at this. It's not straightforward since it appears that the Sass plugin only evaluates CSS modules when bundle: true is configured in the ESBuild build, and we don't want to bundle during the transpilation step.
A few initial ideas:
- I recall seeing something about using a combination of
bundle: trueand defining dependencies as externals to let the transpilation step bundle the result but continue externalizing dependencies - Alternatively, we might need to do something more custom and tap into ESBuild plugin load hooks. This might be something we'd want to do anyways as a solution to (1) duplicate script tags and (2) moving away from PostCSS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we've kinda painted ourselves into a corner by wanting transpilation step to not bundle, but CSS modules be part of the transpilation step, and inlining the styles.
Styles are normally handled as part of the bundling step. Without bundling, ESBuild won't even evaluate the imports of a file, so we can't really alter how those styles are loaded. Similarly, ESBuild's built-in handling for CSS modules only happens when bundling is enabled, and the way they generate class names for CSS modules only protects against conflicts when bundling the entire application.
I tried enabling bundling while keeping "everything" external†, but ESBuild errors when trying to set relative paths as external, and without being able to do that, the bundling causes each transpiled file to include the contents of all its local package files. If we could bundle the package down to a single file, it'd probably work, but I'm guessing we don't want to do that if we need to support importing subpaths (ideally we wouldn't support this in a greenfield project, but we've got a lot of backwards-compatibility).
Another problem we'll have is that this sort of style inlining is inherently a side effect, which creates a lot of challenges around supporting tree-shaking without a lot of maintenance burden around how we maintain sideEffects. Maybe we can use some wildcard patterns to help here, though.
All this kinda had me leaning back toward a solution where we process the individual CSS modules, create a shim JavaScript file in place, and generate a CSS file... before realizing this is the exact solution we already have and are trying to replace 😅 We could probably use an optimized alternative to PostCSS (LightningCSS) to address that concern. I'm personally not as sold on trying to avoid CSS files altogether.
I'll continue to tinker with exploring a solution which essentially combines parts of what we already have (shimming JavaScript), but inlining a script to inject a style tag rather than emit a stylesheet.
† Externals plugin:
{
name: "externals",
setup(build) {
build.onResolve({ filter: /^/ }, (args) => {
return {
path: args.path,
external: !args.path.startsWith(packageDir),
};
});
},
},There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On tree shaking: Would it be fair to say that yes, CSS modules are side effects but they are only needed if the file that imports it is used (the component). Is this kind of side effect something we can "codify" somehow?
On transpiling and bundling I'm not strongly against bundling the packages that have CSS directly into packages/build-* which means that the "bundling step" becomes just a "copying step" for these packages (in addition to adding a global variable if they are scripts). Now I don't really know if that helps or not here? and I don't really know whether this should be default for all packages or not (or if it should be limited to only some of them)
On eliminating CSS files from builds and inlining JS I'm also open to alternatives, that said I do feel like we don't have a good compelling alternative that addresses the need for both "tree shaking" and "lazy loading of modules".
- If I import "Button" ideally I just get the button CSS in my bundle
- If I import a "WP script module" dynamically, I would like for the CSS of the script module to be loaded as well automatically.
I think these are the two things we should aim to solve.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sgomes Do you have a link to some canonical article (or maybe a P2 post you wrote in the past) that describes the CSS-in-JS issues in detail?
I don't have anything up-to-date, because I haven't looked at it in some time (since thankfully CSS-in-JS isn't quite as popular nowadays). The fundamental problem, as I recall, were CSS-in-JS approaches that did their work at runtime, rather than at build time, and the overhead in managing all that. The problem isn't injecting pre-computed style tags at runtime; every lazy CSS approach does that, since we don't have async CSS loading natively on the web platform. The problem is computing dynamic style tags (e.g. ones that depend on component props) at runtime, and managing all the plumbing for that.
This is an old (Dec. 2019) article I remember reading and thinking was very thorough, when I was looking closer at CSS-in-JS: https://calendar.perfplanet.com/2019/the-unseen-performance-costs-of-css-in-js-in-react-apps/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is computing dynamic style tags (e.g. ones that depend on component props) at runtime, and managing all the plumbing for that.
That's good news for us, because with our approach we're not doing this at all. All our styles are static CSS strings. We only use CSS-in-JS as a distribution method: instead of shipping the string in a .css file, we ship it as a JS string and insert it using DOM API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's good news for us, because with our approach we're not doing this at all. All our styles are static CSS strings. We only use CSS-in-JS as a distribution method: instead of shipping the string in a .css file, we ship it as a JS string and insert it using DOM API.
Right, exactly. And I'd always assumed stuffing CSS in JS as a distribution method was a problem in and of itself, but it turns out that the impact of having large string literals inside a JS file is much smaller than I thought.
So while the approach isn't ideal (it's still more expensive than simply pointing the browser at a CSS file, and it's worse for debugging), it didn't seem to be a performance problem in practice, at least as far as I could measure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the useful feedback everyone!
@youknowriad, with all things said so far, I'm OK to move on like this for now, but I'd suggest keeping an eye on it and maybe considering introducing a mechanism for distributing CSS through dedicated asset files at some point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One place where this approach will fail completely is server-side rendering. On the server, styles are shipped differently. Our problem is that the document.head.appendChild calls are hardcoded. If they were abstracted into something like emotionCache.insert(), the build environment could provide the exact implementation.
Maybe we should adopt some tiny subset of Emotion or another CSS-in-JS library 🙂 Otherwise we're just implemeting a bad version of something that already exists.
|
Flaky tests detected in 8c62679. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/18909842186
|
0c7b6df to
7f351af
Compare
These are now loaded dynamically through the bundled JavaScript code
These are no longer generated
youknowriad
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's ship this and iterate on the transpilation of the css modules separately.
|
Size Change: +17 B (0%) Total Size: 2.37 MB
ℹ️ View Unchanged
|
|
With incorporating styles in transpiled output, maybe we can look at a way that avoids it being a side-effect in the way that the styles are appended automatically when the component is imported, and instead make it a side-effect of the actual component rendering. One idea I had here is that because CSS modules are imported as an object, we could (ab)use object getters to append the styles. This would be in conflict with React The "correct" way of handling this would be using |
|
So we don't really support SASS for these inline css imports? Should we? |
Not at the moment. I haven't personally seen a demonstrated need, and at least with the theming work being built out leaning heavily on CSS properties, that's going to be the primary way for accessing "variables". Are there other features we're missing that aren't available in native CSS these days? |
I put together a proof-of-concept branch for this at https://github.com/WordPress/gutenberg/compare/try/bundled-css-modules : It ends up producing something like this in the build: // packages/theme/build-module/style.module.css.js
export default {
get [ "root" ]() {
const style = document.createElement( 'style' );
style.styleSheet.cssText = "._root_th78q_1 {\n\tdisplay: contents;\n}\n";
document.body.appendChild( style );
delete this[ "root" ];
return ( this[ "root" ] = "_root_th78q_1" );
}
}; |
What?
Updates build script to inline contents of CSS modules into built JavaScript rather than emitting separate stylesheets.
Follow-up to #72305
Why?
It may end up simplifying dependency tracking and developer experience for consuming from packages which provide styles through CSS modules. For example, #72305 mentions "Probably move the styles in the stylesheet to an inline style attribute" as a follow-up action item because the inclusion of a separate stylesheet adds additional developer friction.
On the other hand, it may be worth keeping an eye on this and revisiting down the line, since there may be some ramifications:
esbuild-sass-plugingenerates code for adding the style tag (i.e. make it idempotent)Testing Instructions
npm run buildgrep -C 5 _root_ build/scripts/theme/index.js