I recently switch from Nextjs to Remix for my personal website. One thing I struggled with was to have it merge individual .css
files into one. So I solved it with the Parcel CLI. This blog post demonstrates how.
The problem
Note, first of all, this talks about the global CSS. You can and should still employ CSS Modules or something equivalent for CSS that is tied directly to a React component.
But global CSS has its place and purpose. The problem is that there's no convenient way to bundle multiple little .css
files into one which you can then nest into routes in Remix.
The way you inject CSS into a Remix page is like this:
import highlight from "~/styles/highlight.css";
import blogpost from "~/styles/blogpost.css";
...
export function links() {
return [
{ rel: "stylesheet", href: highlight },
{ rel: "stylesheet", href: blogpost },
}
And for the record, suppose you have a nested route that needs those, and another one you do:
import banner from "~/styles/banner.css";
import { links as rootLinks } from "./_index";
...
export function links() {
return [
...rootLinks().filter((x) => !x.extra),
{ rel: "stylesheet", href: banner },
];
}
This will nicely pick up those source .css
files, minify them and produce in the final HTML SSR output:
<link rel="stylesheet" href="/build/_assets/highlight-KI4AX52K.css"/>
<link rel="stylesheet" href="/build/_assets/blogpost-75V4EYTP.css"/>
Nice. Http2 is famously good at parallel downloads. But even that has its physical limits. Especially if you have many little .css
files that make up all the CSS you need. Now you have multiple files that can get stuck on the network. Yes, you might be able to update 1 and keep caching the others if their fingerprint don't change, but this is likely to be rare.
Parcel to the rescue
I solved it by using the Parcel CLI. In package.json
I have:
"parcel:build": "parcel build --dist-dir app/styles/build app/*.css",
And in app/global.css
I have this:
/* This is app/global.css */
@import "../node_modules/@picocss/pico/css/pico.css";
@import "./styles/globals.css";
@import "./styles/message.css";
@import "./styles/nav.css";
@import "./styles/comments.css";
@import "./styles/carbonads.css";
@import "./styles/carbonads-outer.css";
@import "./styles/modal-search.css";
That means, that Parcel will bundle all of these app/*.css
files into 1 app/styles/build/global.css
Now, I can refer to that built on in the Remix app:
import global from "~/styles/build/global.css";
...
export function links() {
return [
{ rel: "stylesheet", href: global },
]
}
Build vs. dev
Ok, so that explains how to bundle individual CSS files before you actually use the bundled CSS files. Remix doesn't care (a good thing).
At this point, we've modularized the problem. Now Parcel can do what it does best (CSS bundling (among other things it can do)) and Remix can do what it does (serving the .css
files into the HTML).
But just like it's ergonomically pleasant to bundle CSS files like this, we still want it so that you don't have to manually run a separate step to build the bundle every time you edit an individual source .css
file (e.g. app/styles/nav.css
)
Here's how I solved that split up by Dev and Build
Build
"scripts": {
- "build": "remix build",
+ "build": "npm run parcel:build && remix build",
+ "parcel:build": "parcel build --dist-dir app/styles/build app/*.css",
Now, npm run build
will do both things.
Dev
"scripts": {
"dev": "npm-run-all build --parallel \"dev:*\"",
"dev:node": "cross-env NODE_ENV=development esrun --watch ./server.ts ",
"dev:remix": "remix watch",
+ "dev:parcel": "parcel watch --dist-dir app/styles/build app/global.css",
In conclusion
I admit, I'm a CSS Modules fan-boy and it saddens me how much global CSS I have. One thing at a time, I guess. They both have their powers; global and modular CSS, but I'll admit that my own personal site still relies a bit too much on global CSS. At least, little goes to waste because Remix makes it relatively easy to pick exactly which files you need for individual routes.
Comments