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

Most approaches to using webpack with Django work until the JavaScript app is tiny. What happens when the bundle grows?

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 bundle is too big we need to apply code splitting to it.

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 (mine neither).

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.

Conclusions

Unfortunately I don't have an easy solution here. Bigger bundles need code splitting and many chunks. As of today I'm not aware of any magic Django plugin for making splitChunks work.

In the end I came to realize that there's no need to fight Django and webpack chunks:

  • if the JavaScript app is tiny (under 200KB), you can use the approach outlined here.
  • if the SPA is big instead, you can simply load the resulting index.html from CRA, Vue-cli, or what not, as the main template for the Django application. Here's an example.

Thanks for reading!

Valentino Gagliardi

Hi! I’m Valentino! Educator and consultant, I help people learning to code with on-site and remote workshops. Looking for JavaScript and Python training? Let’s get in touch!

More from the blog: