Dynamic CSS Chunk Loading

In this article we're going to look at doing for CSS what we just did for JS. Splitting and dynamically delivering component sized chunks for each page.

We're going to start where we left off in the last article.
If you need to catch up:

git clone https://github.com/lawwantsin/webpack-course.git
cd webpack-course
git checkout css-extract
npm install

So we've done most of the work to get us there. The only thing left to do is hook up two packages, one by the same engineer who gave us the react-universal-component package and the other made 4 days ago by the creator of webpack. We'll be serving CSS along with our dynamic JS imports.

In terminal:

npm i webpack-flush-chunks mini-css-extract-plugin

We're going to deliver the component CSS in tandem with the component's JS. To illustrate, let's create a new file in src/components:

mkdir src/css
touch src/css/About.css

And let's copy the .profile stuff from main.css into that new file:

Into About.css:

.profile {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  flex-flow: column;
}

.profile > img {
  border-radius: 100%;
  bseriesOrder: 5px;
  width: 300px;
  box-shadow: 0 0 20px black;
}

h1 {
  font-size: 5em;
  font-family: sans-serif;
  color: white;
  text-shadow: 0 0 20px black;
  text-align: left;
}

Now let's import that file into our About.js.

const imagePath = require("../images/link.jpg") // Under this line
import "../css/About.css"

Now, in webpack.dev-client.js and webpack.prod-client.js:

// At the top
const MiniCSSExtractPlugin = require('mini-css-extract-plugin')

// In the loaders.
{
  test: /\.css$/,
  use: [MiniCSSExtractPlugin.loader, "css-loader"]
}

// In the Plugins.
new MiniCSSExtractPlugin()

We're going to replace the ExtractTextPlugin with the MiniCSSExtractPlugin plugin in production. In development, we should return to the good old style-loader to do HMR.

In the plugins array let's add that without arguments.

new MiniCSSExtractPlugin({
  filename: "[name].css",
  chunkFilename: "[name].css"
}),

We can adjust the filename of our css, chunks, though, in production, it will default to using a content hash.

Okay, last let's put some blank css in the other components import them in their js files, and run the server.

In terminal:

cd src/css
touch Article.css Gallery.css

In Gallery.js:

import "../css/Gallery.css"

In Article.js:

import "../css/Article.css"

It's important we place these css files in a separate (non-child) folder from the components involved in the import(). If you remember in Routes.js we're pulling dynamic pages from src/components. So I place the css in src/css.

Let' see where we're at. In terminal:

npm run dev

Passing Chunks

Inside the production side of express.js let's grab the client build stats object out of the stats object which is one of the arguments passed to the function we're defining to run after the production compile is done.

We can use that to restore logging to our production builds.

webpack([clientConfigProd, serverConfigProd]).run((err, stats) => {
  const clientStats = stats.toJson().children[0]
  const render = require("../../build/prod-server-bundle.js").default
  // Put the output back in the console.
  console.log(
    stats.toString({
      colors: true
    })
  )
  server.use(
    expressStaticGzip("dist", {
      enableBrotli: true
    })
  )
  server.use(render({ clientStats }))
  done()
})

And let's pass that as an object to the render function inside render.js:

Now, let's import them into our middleware file. In server/render.js:

import { flushChunkNames } from "react-universal-component/server"
import flushChunks from "webpack-flush-chunks"

He'll be the first to admit that flushChunks is a pretty weird, gross name for a package that give us now, what's been thought incredibly complicated for quite some time.

Out of the flushChunks function we have an object, which we can pull out the js styles.

Those are all ready to be embedded in a template or sting as is. They come out as nice script tags or link tags. In render.js

export default ({ clientStats }) => (req, res) => {
  const app = renderToString(
    <StaticRouter location={req.url} context={{}}>
      <Routes />
    </StaticRouter>
  )

  const { js, styles, cssHash } = flushChunks(clientStats, {
    chunkNames: flushChunkNames()
  })

  res.send(`
    <html>
      <head>
        ${styles}
      </head>
      <body>
        <div id="react-root">${app}</div>
        ${js}
        ${cssHash}
      </body>
    </html>
  `)
}

Finally we notice a Loading... where there should be html. In webpack.dev-server.js let's add the LimitChunkCountPlugin to our plugins array:

plugins: [
  new webpack.optimize.LimitChunkCountPlugin({
    maxChunks: 1
  }),

Awesome and it's working.

In Sum

When we rerun our server and go back to our browser, we see we have what we were promised, the mythical double chunk, coming at you. You'll notice that About holds the h1 statement, so h1 is un-styled until those rules come down. Really cool stuff.

git checkout css-chunks-final

Up Next

Now that we've got an optimal setup, let's build this blog out a bit in preparation for the next section. Where we expand our blog into a multi-domain super site. We've come a long way from humble beginnings, but we've added a lot of power to our setup. Let's use that power, in the next section.