Using webpack with Django: it's not easy as you think

Using webpack with Django

These days I'm seeing new tutorials on using webpack with Django popping up. Like:

I don't mean to bash on them, but, the problem with the approaches showed there is that they work for smaller JavaScript applications. I mean tiny applications.

Imagine instead a medium-size React/Vue application with some state management solution like Redux or Vuex. Imagine also a bunch of JavaScript libraries needed by this application, and imagine a JavaScript bundle resulting from this app that goes over 200KB.

Let's see what I mean.

webpack and Django without code splitting

A typical webpack configuration for Django configured to produce a JavaScript bundle in the static folder looks like the following:

const path = require("path");

module.exports = {
entry: "./index.js",
output: {
path: path.resolve(__dirname, "../static/custom_webpack_conf_2/js"),
filename: "[name].js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: { loader: "babel-loader" }
}
]
}
};

With this configuration, given an entry point at ./index.js, webpack produces the corresponding bundle in ../static/custom_webpack_conf_2/js.

In the Django template you'll load the bundle as:

{% load static %}
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello Django!</h1>
<div id="root"></div>
</body>
<script src="{% static "custom_webpack_conf_2/js/main.js" %}"></script>
</html>

Again, this approach works fine for a single bundle. But, if the resulting files are too big we need to apply code splitting.

webpack splitChunks

webpack offers a powerful optimization technique called splitChunks. In webpack.config.js you can add an optimization property:

const path = require("path");

module.exports = {
entry: "./index.js",
output: {
path: path.resolve(__dirname, "../static/custom_webpack_conf_2/js"),
filename: "[name].js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: { loader: "babel-loader" }
}
]
},
optimization: {
splitChunks: {
chunks: "all"
}
}
};

It's this little bugger here that hurts Django, but it's great for optimizing the bundle:

  optimization: {
splitChunks: {
chunks: "all"
}
}

Why it hurts Django? If you bundle up your JavaScript with splitChunks, webpack generates something like this in static:

└── js
├── main.js
└── vendors~main.js

There's even a more powerful technique for splitting each dependency with splitChunks:

  optimization: {
runtimeChunk: "single",
splitChunks: {
chunks: "all",
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];

// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace("@", "")}`;
}
}
}
}
}

With this setup you get a static folder like the following (don't mind development dependencies like prop-types, I'm on local):

└── js
├── main.js
├── npm.babel.js
├── npm.hoist-non-react-statics.js
├── npm.invariant.js
├── npm.object-assign.js
├── npm.prop-types.js
├── npm.react-dom.js
├── npm.react-is.js
├── npm.react.js
├── npm.react-redux.js
├── npm.redux.js
├── npm.regenerator-runtime.js
├── npm.scheduler.js
├── npm.webpack.js
├── npm.whatwg-fetch.js
└── runtime.js

Consider also a variation with chunkFilename, where each chunk gets a hash:

const path = require("path");

module.exports = {
entry: "./index.js",
output: {
path: path.resolve(__dirname, "../static/custom_webpack_conf_2/js"),
filename: "[name].js",
chunkFilename: "[id]-[chunkhash].js" // < HERE!
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: { loader: "babel-loader" }
}
]
},
optimization: {
runtimeChunk: "single",
splitChunks: {
chunks: "all",
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];

// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace("@", "")}`;
}
}
}
}
}
};

Are you sure you want to see the result? Here you go:

└── js
├── main-791439bfb166c08db37c.js
├── npm.babel-475b0bf08859ce1594da.js
├── npm.hoist-non-react-statics-73d195f4296ad8afa4e6.js
├── npm.invariant-578b16a262ed0dd4eb92.js
├── npm.object-assign-a4287fbbf10266685ef6.js
├── npm.prop-types-6a9b1bb4f5eaf07ed7a2.js
├── npm.react-9f98897e07d8758f6155.js
├── npm.react-dom-484331d02f3838e95501.js
├── npm.react-is-692e5a605d1565b7f5fa.js
├── npm.react-redux-bad2d61a54d8949094c6.js
├── npm.redux-9530186d89daa81f17cf.js
├── npm.regenerator-runtime-b81478712fac929fd31a.js
├── npm.scheduler-4d6c90539714970e0304.js
├── npm.webpack-f44e5b764778a20dafb6.js
├── npm.whatwg-fetch-033a6465c884633dbace.js
└── runtime.js

How do you load all these chunks in Django templates, in the exact order, and with the exact chunk name? This is a question that most tutorials can't answer.

Why do we need this madness?

Why don't we "just" decouple Django with DRF, and make the frontend a single page application? Good question! As I already said in Django REST with React there are mainly three ways to use Django and a JavaScript frontend together:

Option 1. React/Vue/Whatever in its own frontend Django app: load a single HTML template and let JavaScript manage the frontend.

Option 2. Django REST as a standalone API + React/Vue/Whatever as a standalone SPA.

Option 3. Mix and match: mini React/Vue/Whatever apps inside Django templates (not so maintainable in the long run?).

Option 2 seems more convenient over option 1, but keep in mind that the moment you decouple the backend from the frontend, you need to think about authentication. Not authentication based on sessions (unless JavaScript is in the same domain as Django), but tokens, specifically JWT, which have their own problems.

With option 1 instead, since the JavaScript bundle continues to live inside a Django template you can use Django's built-in authentication, which is totally fine for most projects.

How about django-webpack-loader?

There is this package django-webpack-loader that was supposed to make Django and webpack work seamlessly, until it didn't anymore when webpack 4 introduced splitChunks.

Maintaining open source projects is hard. This issue about splitChunks in django-webpack-loader is still open, and so this one.

I touched the topic in my talk Decoupling Django with Django REST suggesting a Django package like Rails webpacker.

A solution with Nginx

There is a solution for integrating Django and a single-page application together. If you control your own Django deployments with Nginx, or with an equivalent reverse proxy, serving say React inside a Django project becomes extremely less complex:

  • in Django, you can use a TemplateView to serve your SPA index.html
  • in Nginx, you point a /static location to the JS build folder

Here's an example:

Django and React with Nginx

Thanks for reading!

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!