Misago Removes React and Adopts htmx

title
Misago Removes React and Adopts htmx
type
summary
summary
Why Misago dropped React for server-rendered htmx islands, and what its JS bundles did after
tags
web, frontend, django, htmx, forum-software
created
2026-07-29
updated
2026-07-29

rafalp, author of the Django forum Misago, writing on the project's own forum about removing React.js from the codebase. The plan was posted while the migration was still scheduled for "later 2024"; the concrete results at the bottom were appended as the work landed.

The double render

Misago's request path at the time went like this. Django gathers the data for a thread list and renders a template containing nearly complete HTML, plus a JSON blob holding the same data, with the forms disabled so nothing is interactive. JavaScript then downloads, reads the embedded JSON, and replaces most of the server-rendered HTML with the equivalent React components. Buttons wake up, timestamps swap.

The consequences of maintaining both halves are what the post is really about:

  • Many pages exist twice, as Django templates and as React components. Anyone customizing HTML edits the templates, sees their change flash on screen for a second, and then watches React overwrite it with markup they did not know they also had to change.
  • Any view reachable by both users and guests has to be built twice, as a Django view with templates and as a React route with components, which in turn requires an API and JSON serializers to feed it.
  • Serializing that JSON to pre-bake the React state slows down response generation.
  • Translations are duplicated across django.po and djangojs.po, and the JavaScript catalogue adds to the initial download.
  • Plugins that want to inject or replace HTML have to ship both a Django template and a React component, which would have forced a JavaScript build step into misago-docker and required plugin authors to know both stacks.

The two options he rejected

Going further into the SPA: keep Django views and templates only in a minimal form for search engine bots, and put all UI behind an API and a React app. Or reduce Django to an API and put a server-rendering JavaScript framework such as Next.js or Remix in front of it.

What stopped him was noticing that plenty of forum software still renders as much as possible on the server, sprinkles JavaScript where it helps, and has users who are happy with it β€” while having none of the problems above. Forum interactivity is real but local: moderation actions, watching a thread, writing a reply, checking notifications, voting in a poll. All of it was done without React for years, before full page reloads became something to be avoided on principle.

What htmx does here

htmx lets a template mark regions of the page as dynamic islands that get swapped for new server-rendered HTML on interaction. The thread list is one such (large) island. Switching category pulls fresh HTML for just that region while the rest of the page, including something like already-loaded navbar search results, stays put. The backend change is narrow: when the request comes from htmx, return the island instead of the whole page. No JSON serialization, no dedicated JavaScript.

His own framing is that this is a declarative version of the $.get("url", "#outlet") everyone wrote in jQuery twenty years ago, or of Rails Turbolinks, and he says he cannot quite believe he is writing it. This is the opposite direction from where client-side frameworks went with fine-grained reactivity β€” see signal-push-pull-algorithm for the machinery htmx is declining to have β€” and it belongs to the same instinct as dark-mode-web-standards: work out what the platform and the server can do before adding a runtime to do it for you.

The admin panel is explicitly excluded. It runs on a set of reusable Django views that already handle about 90% of the work, where adding a page means picking the right base view, filling in the blanks, defining a form or two, and writing basic templates. He likes that and is not moving it to a SPA.

He also warns about the middle of the migration: parts of Misago will have neither React nor htmx, which means clicking a link or submitting a form reloads the page. He calls this a multi-page application and says it only sounds scary. Where a reload would be genuinely unbearable, such as liking a post or voting in a poll, he plans placeholder AJAX or htmx.

The numbers

Baseline, Misago 0.39:

  • vendor.js β€” 679 kB, 214 kB gzipped
  • misago.js β€” 615 kB, 124 kB gzipped
  • django-i18n.js β€” 102 kB, 25.4 kB gzipped
  • lazily loaded: hljs.js 144 kB / 49 kB gzipped, zxcvbn.js 820 kB / 430 kB gzipped

Two results were appended later. The "Forum options" page was replaced with "Account settings" (PR #1742), built entirely from Django views with htmx for dynamic updates on some pages: misago.js fell to 578 kB, 107 kB gzipped, a saving of 37 kB raw and 17 kB gzipped. Then the thread lists were rewritten close to from scratch on Django views and htmx (PR #1758): 530 kB, 99 kB gzipped, another 48 kB raw and 8 kB gzipped.

Cumulatively that is 615 kB down to 530 kB, 124 kB down to 99 kB gzipped β€” around 14% of the application bundle raw and 20% gzipped, from two of the largest pages in the product. vendor.js, which holds React itself and is the single biggest download at 214 kB gzipped, does not move at all until the last component is gone. That shape is worth remembering when reading incremental de-framework migrations: the reported savings stay modest until the framework can actually be deleted, and the payoff is back-loaded.