orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1---
2title: Using Vite with Django in 2026
3slug: using-vite-with-django-in-2026
4date: 2026-04-26
5publish_date: 2026-04-26
6tags: webdev, coding
7description: I ran webpack on every Django project I had for years but in 2026 I use Vite instead, with no wrapper package involved, and this is the whole setup I use.
8cover_image: vite-django-build.webp
9---
10
11I ran webpack on every Django project I had for years and over that time the config kept growing, the builds kept getting slower, and the plugin churn never really stopped. In 2026 I use [Vite](https://vite.dev/) instead and the integration with Django is thin enough that I don't bother with a wrapper package at all. The `vite.config.js` is a single file, Django just sees regular static files, and `{% static %}` works the way it always has.
12
13I use this on [analytics](https://github.com/overshard/analytics) and [status](https://github.com/overshard/status) so most of the snippets below are pulled straight out of those two projects.
14
15## Why I moved off webpack
16
17There were a few reasons for that:
18
19* It's slow, and Vite's dev server doesn't bundle in development at all, it serves source modules over native ESM. The Mews team [cut their builds 80% by switching to Rspack](https://developers.mews.com/goodbye-webpack-hello-rspack-and-80-faster-builds/), and GitLab's [move to Rolldown-Vite took their builds from 2.5 minutes to 22 seconds](https://voidzero.dev/posts/announcing-rolldown-vite).
20* The config is verbose, since a reasonable webpack setup wants `babel-loader`, `css-loader`, `style-loader`, `mini-css-extract-plugin`, `postcss-loader`, `sass-loader`, and `HtmlWebpackPlugin` before you've written a line of app code. Vite ships TypeScript, JSX, CSS, SCSS, and HMR built in and my `vite.config.js` is 30 lines.
21* The energy is mostly elsewhere now, the [State of JavaScript 2025](https://2025.stateofjs.com/) survey put webpack at 86% usage and 14% retention, and the new work is going into Vite, esbuild, Rolldown, and Rspack.
22* Webpack itself moves pretty slowly these days, there's a [public 2026 roadmap](https://webpack.js.org/blog/2026-02-04-roadmap-2026/) but no v6 release on the horizon that I can see. The Hacker News thread ["It's 2026 now. Is webpack 6.x going to happen?"](https://news.ycombinator.com/item?id=46485161) is a good read.
23
24If you're on a giant webpack codebase and a full migration sounds like too much, [Rspack](https://rspack.dev/) is a Rust-based drop-in replacement that works with most webpack configs as-is. For a Django project I'd skip the middle step and go straight to Vite.
25
26## The Vite config
27
28Each Django app has a `static_src/index.js` that imports its own SCSS and scripts, and Vite takes those entry points and writes the bundle out to a single `static/` directory inside the project's main app. This is the `vite.config.js` straight out of analytics:
29
30```javascript
31import { resolve } from "path";
32import { defineConfig } from "vite";
33
34export default defineConfig({
35 base: "/static/",
36 build: {
37 outDir: resolve(__dirname, "analytics/static"),
38 emptyOutDir: true,
39 rollupOptions: {
40 input: {
41 base: resolve(__dirname, "analytics/static_src/index.js"),
42 pages: resolve(__dirname, "pages/static_src/index.js"),
43 properties: resolve(__dirname, "properties/static_src/index.js"),
44 collector: resolve(__dirname, "collector/static_src/index.js"),
45 },
46 output: {
47 entryFileNames: "[name].js",
48 assetFileNames: (assetInfo) => {
49 if (/\.(png|jpg|gif|svg|webp)$/.test(assetInfo.name)) {
50 return "images/[name][extname]";
51 }
52 return "[name][extname]";
53 },
54 },
55 },
56 },
57 css: {
58 preprocessorOptions: {
59 scss: { quietDeps: true },
60 },
61 },
62});
63```
64
65A few notes:
66
67* `base: "/static/"` matches Django's `STATIC_URL`, so anything Vite writes into the bundle (image references, font URLs) resolves against the same path Django serves from.
68* Each Django app gets its own entry under `rollupOptions.input`. Vite emits a `base.js`, `pages.js`, `properties.js`, and `collector.js` into the same output folder and templates load whichever they need.
69* `entryFileNames: "[name].js"` keeps the output names predictable, and WhiteNoise hashes them at `collectstatic` time anyway so I don't need Vite doing it too.
70* `emptyOutDir: true` wipes the output between builds, since Vite refuses to clean a directory outside its project root unless you set it, and without it you get a warning every build and stale files pile up.
71
72## Django settings
73
74I don't use `django-vite` or any other integration package because Django doesn't really need to know that Vite exists, it just sees a static directory full of pre-built files and serves them.
75
76```python
77# settings.py
78
79STATIC_URL = "static/"
80STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
81STATICFILES_DIRS = (BASE_DIR / "analytics/static",)
82STATIC_ROOT = BASE_DIR / "static"
83
84MIDDLEWARE = [
85 "django.middleware.security.SecurityMiddleware",
86 "whitenoise.middleware.WhiteNoiseMiddleware",
87 # ...
88]
89```
90
91`STATICFILES_DIRS` points at Vite's output directory so `collectstatic` and the dev server both pick it up, and `CompressedManifestStaticFilesStorage` hashes every file during `collectstatic` and rewrites the references in CSS to point at the hashed names. Don't append `?v=...` query strings to `{% static %}` though, WhiteNoise expects to control those URLs and the manifest will get out of sync on you.
92
93The templates stay about as plain as they ever were:
94
95```html
96<link href="{% static 'base.css' %}" rel="stylesheet">
97<script type="module" src="{% static 'base.js' %}"></script>
98```
99
100In dev that resolves to `/static/base.css` and in production WhiteNoise rewrites it to `/static/base.abc123.css` for you automatically.
101
102## Running Django and Vite together
103
104Vite has a watch mode that rebuilds on every change and I run it right next to Django's runserver out of a Makefile:
105
106```make
107.PHONY: run runserver vite
108
109run: install
110 ${MAKE} -j2 runserver vite
111
112runserver:
113 uv run python manage.py runserver 0.0.0.0:8000
114
115vite:
116 bun run dev
117```
118
119`make -j2` runs both targets in parallel in the same terminal, and the `dev` script in `package.json` is just `vite build --watch`:
120
121```json
122{
123 "scripts": {
124 "dev": "vite build --watch",
125 "build": "vite build"
126 }
127}
128```
129
130I don't use Vite's dev server myself. It's great for SPAs but on a Jinja-rendered page HMR doesn't really do much of anything for you and you've added an extra port and an integration layer to get it. `vite build --watch` just writes plain files to disk that look identical to what production serves, so I hard refresh in the browser the same way I always did with webpack.
131
132If you really want HMR then [`django-vite`](https://github.com/MrBin99/django-vite) is the package most people reach for and it works well, you can use it if you want! I just don't think it's worth the extra dependency on a multi-page app.
133
134## Per-app static_src
135
136For the analytics dashboard the entry point looks like:
137
138```javascript
139// properties/static_src/index.js
140import "./scripts/property_graphs.js";
141import "./scripts/property_map.js";
142import "./scripts/property_date_select.js";
143import "./scripts/property_filters.js";
144
145import "./styles/print.scss";
146```
147
148And in the dashboard template:
149
150```html
151{% block extra_js %}
152<script type="module" src="{% static 'properties.js' %}"></script>
153{% endblock %}
154```
155
156New JS for an app goes in that app's `static_src/` and shows up as `<app-name>.js` in the static directory, with no webpack chunks config, no `splitChunks`, and no Babel preset to keep current.
157
158## Production
159
160The Dockerfile runs `bun run build` once during the image build and then `collectstatic`, and after that there's no Node or Vite at runtime at all, just Gunicorn serving Django and WhiteNoise serving the hashed files.
161
162```dockerfile
163RUN bun install --frozen-lockfile
164RUN bun run build
165RUN uv run python manage.py collectstatic --noinput
166```
167
168Total build time across `bun install`, `vite build`, and `collectstatic` is under 30 seconds on these projects, where webpack used to take three or four minutes to do the same work.
169
170## Sources
171
172Honza HrubĂ˝'s ["Goodbye Webpack, Hello Rspack"](https://developers.mews.com/goodbye-webpack-hello-rspack-and-80-faster-builds/) is the case for the drop-in path if a full migration isn't realistic. Ratchapol Thaworn's ["Migrating from Webpack to Vite"](https://medium.com/@ratchapol.thaworn/migrating-from-webpack-to-vite-real-world-lessons-from-a-production-frontend-project-ea4bb53a9d58) and HK Lee's ["Vite vs. Webpack in 2026"](https://dev.to/pockit_tools/vite-vs-webpack-in-2026-a-complete-migration-guide-and-deep-performance-analysis-5ej5) go further on the Vite side. The [Vite docs](https://vite.dev/guide/) and the [django-vite README](https://github.com/MrBin99/django-vite) are the references I keep open when I'm actually wiring it up. Saas Pegasus has a [longer Django + Vite walkthrough](https://www.saaspegasus.com/guides/modern-javascript-for-django-developers/integrating-javascript-pipeline-vite/) too if you want one with React and Tailwind on top.
173
174Webpack served me well for a long time, I just don't have much of a reason to keep using it on anything new.