Do I Really Need Svelte in My Django Project? — A Practical Checklist I Wrote After Comparing Vanilla JS vs. Frameworks
Introduction
While adding features like a hamburger menu, OAuth login, and per-user settings to a side project, I started to feel the limits of plain HTML/CSS/JavaScript (hereafter vanilla JS). As stateful widgets multiplied, so did the DOM spaghetti. That raised the million-dollar question:
“Should I adopt Svelte (or SvelteKit), or keep pushing with vanilla JS?”
This post distills a hands-on checklist for balancing framework benefits against resource constraints.
1. When did I actually need a framework?
Requirement | Pain with vanilla JS | Svelte (Kit) advantage |
---|---|---|
Shared state across multiple components | Long querySelector chains & ad-hoc event buses |
$: reactivity, Stores for global state |
Client-side routing (SPA feel) | Must hand-roll History API logic | File-based routing built-in |
SEO + SSR | Django template handles SSR, but JS widgets ship as empty <div> |
SvelteKit server-side render & prerender |
Bundle optimization | Manual Webpack/Vite tuning | Vite-powered build & code splitting by default |
Team & feature growth | No conventions → onboarding cost ↑ | Component/file conventions baked in |
TL;DR — The more shared state, reusable components, and SEO you need, the faster SvelteKit pays for itself.
2. Risks of staying vanilla
- DOM spaghetti — tracking who mutates the DOM becomes a nightmare.
- State desync bugs — login/logout, dark-mode toggles, etc. easily drift.
- Testing overhead — E2E tests require verbose DOM selectors.
- Bundle fatigue — every new page demands manual caching & split-chunk tweaks.
3. Option comparison
flowchart LR
subgraph "👟 Vanilla JS"
A1[HTML Template] -->|DOM manip| A2[script.js]
end
subgraph "🧩 Svelte Components"
B1[Django Template] --> B2[<script src="build.js">]
B2 --> B3[Svelte Widget]
end
subgraph "🚀 SvelteKit + API"
C1[SvelteKit SSR] -->|REST| C2[Django API]
end
Model | Pros | Cons |
---|---|---|
Vanilla JS | No Node runtime → low memory | Must implement state & routing yourself |
Svelte Components | Drop-in interactive widgets | Full page reload between Django views |
SvelteKit | SPA feel and SSR/SEO | Extra Node server to operate |
4. A Python-first, resource-minimal architecture
Assumptions: SEO/GEO only mattered for the landing page, I was on Northflank’s free tier, and I prefer Python-centric ops.
- Backend — Django (templates + REST API)
- Frontend
- Landing page: Django template with SEO meta tags
- Dashboard & settings: Svelte bundle served as static assets
- Build pipeline (multi-stage Docker)
FROM node:20 AS client-build WORKDIR /app COPY frontend/ . RUN npm ci ; npm run build FROM python:3.11-slim AS runtime WORKDIR /srv COPY --from=client-build /app/build/ /srv/static/ COPY requirements.txt . ; pip install --no-cache-dir -r requirements.txt COPY . /srv CMD gunicorn config.wsgi --bind 0.0.0.0:$PORT
→ The runtime image ships with no Node binary, trimming RAM usage.
5. Cloudflare Worker BFF & Edge Caching
I needed to hide API keys and off-load traffic without burning my free Northflank quota, so I put a tiny Cloudflare Worker in front of the API.
sequenceDiagram
participant Browser
participant CF as Cloudflare
participant Worker
participant Django
participant External as External API
Browser->>CF: GET /api/orders
CF-->>Worker: Route /api/*
Worker->>External: GET /v1/orders (add API-KEY)
External-->>Worker: JSON payload
Worker-->>CF: 60 s edge-cached response
CF-->>Browser: JSON + CORS headers
Browser->>Django: (separate) GET /
Django-->>Browser: SEO-ready HTML
Key points:
- Security — API keys live in Worker secrets; never reach the browser.
- Speed —
cf: { cacheTtl: 60 }
caches JSON at 300+ global edges. - Cost — Workers Free plan → 2.5 M requests/mo at zero dollars.
// cf-worker.js (snippet)
export default {
async fetch(request, env) {
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', ... } });
}
const api = new URL(env.UPSTREAM_URL);
api.pathname = '/v1/orders';
api.search = new URL(request.url).search;
const upstream = await fetch(api, {
headers: { 'X-API-KEY': env.API_KEY },
cf: { cacheTtl: 60, cacheEverything: true }
});
const res = new Response(upstream.body, upstream);
res.headers.set('Access-Control-Allow-Origin', '*');
return res;
}
};
6. Dev Container workflow
“It works on my machine” is usually a Docker-image mismatch problem. Developing inside the very same container I push to Northflank eliminates that class of bugs.
Why bother?
- Zero env drift — VS Code (or Codespaces) launches inside the runtime image you deploy, so Python, Node, OS libs all match production bits.
- Instant reload — Source is bind-mounted; Django autoreload & Vite HMR trigger on save.
- First-class debugging — VS Code Python debugger and Svelte Inspector attach straight to container PIDs.
- One-shot onboarding — New contributor:
F1 ▸ Dev Containers: Reopen in Container
→ ready in minutes.
Minimal setup
1) .devcontainer/devcontainer.json
{
"name": "myapp-dev",
"dockerComposeFile": ["../docker-compose.dev.yml"],
"service": "web", // Django container
"workspaceFolder": "/srv",
"shutdownAction": "stopCompose",
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
],
"postCreateCommand": "pip install -r requirements.txt"
}
2) docker-compose.dev.yml
version: "3.9"
services:
web:
build:
context: .
target: runtime // reuse prod runtime stage
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ..:/srv // live code mount
ports:
- "8000:8000"
env_file: .env.dev
client:
build:
context: ./client
dockerfile: Dockerfile.dev
command: npm run dev -- --host
volumes:
- ../client:/app
ports:
- "5173:5173"
3) client/Dockerfile.dev
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install --legacy-peer-deps
CMD ["npm", "run", "dev", "--", "--host"]
Tips & caveats
Tip | Reason |
---|---|
Re-use runtime stage |
Guarantees 100 % parity with Northflank image |
Mount only source, not node_modules |
Keeps host-container I/O light |
First open can be slow | Container image pulls & installs once |
Keep forwardPorts in devcontainer.json |
So Codespaces auto-exposes 8000 / 5173 |
7. CI/CD & Deployment Flow
- Local commit
git add . ; git commit -m "feat: landing SEO + CF worker" ; git push origin develop
- Northflank pipeline
- Auto-detects push, builds multi-stage Dockerfile, deploys container.
- Optional manual deploy
docker build -t myapp:latest . docker push myrepo/myapp:latest ; nf deploy myapp
- Cloudflare Worker
wrangler publish --env production
8. Resource-saving levers
Lever | Why it helps |
---|---|
Drop Node at runtime | ~100 MB RAM saved; fewer CVEs to watch |
Edge cache JSON & static assets | Django handles fewer requests; lower CPU quota |
LocMemCache → Redis add-on (later) | Start cheap, scale only when needed |
Multi-stage Docker | Final image < 200 MB, faster cold start |
9. Future roadmap
- UI complexity ↑ → Switch to
SvelteKit adapter-static
, still no Node server. - Real-time feed → Add
Django Channels + Redis
or Server-Sent Events. - i18n & theming → Use Svelte stores or
htmx
partial swaps. - Traffic spikes → Scale Northflank replicas + tune Cloudflare cache TTL.
10. Decision checklist (quick recap)
- Will you exceed ~10 distinct pages?
- Do at least two places need shared global state (auth, theme, notifications)?
- Are most pages SEO-critical, or only the landing page?
- Can you afford to run and monitor an extra Node server?
- Does someone on the team already know a JS framework?
If you answer yes to 3 or more, reach for SvelteKit. Otherwise, vanilla JS + selective Svelte widgets will likely suffice.
Conclusion
Adopting a framework is insurance against future tech debt. Vanilla JS accelerates early momentum, but as features, team size, and SEO requirements grow, maintenance cost skyrockets.
Weigh today’s needs against tomorrow’s complexity to choose the right moment to migrate. A phased path—Svelte widgets → SvelteKit—remains totally valid.
💡 Tooling is secondary to the value you ship and your team’s productivity. Pick what lets you move fastest today without boxing you in tomorrow.