Home
Jul 5, 2026

Hot Reload for Scala.js with Vite

scalascalajsvitetooling

The biggest myth about Scala.js is that the dev loop must be miserable because “Scala compiles slowly”. My current setup gives me save-to-browser in about a second, and it’s two watchers taped together. Here’s the wiring.

The Two Halves

The trick is realizing there are two independent pipelines, and each already has a great watch mode:

  1. Mill watches Scala sources and incrementally recompiles + relinks to JavaScript.
  2. Vite watches that JavaScript output and hot-reloads the browser.
1mill -w frontend.fastLinkJS

Terminal one. Incremental compilation plus the incremental fastLinkJS linker means a typical edit relinks in well under a second, the linker only redoes the modules whose IR actually changed.

1npm run dev

Terminal two, plain Vite. Its entry point just imports the linker’s output:

1// main.js
2import "./out/frontend/fastLinkJS.dest/main.js";

Mill rewrites the file, Vite notices, browser updates. That’s the whole integration, no plugin required, although the vite-plugin-scalajs plugin exists and mostly just abstracts this path for you.

Why This Works Better Than It Should

Scala.js emits ES modules (with ModuleSplitStyle.SmallModulesFor, one per package if you like), so Vite treats the output like any other modern JS codebase, including proper HMR boundaries instead of full page reloads for CSS and small changes.

1object frontend extends ScalaJSModule {
2  def scalaJSVersion = "1.18.1"
3  def moduleKind = ModuleKind.ESModule
4  def moduleSplitStyle =
5    ModuleSplitStyle.SmallModulesFor(List("journal.frontend"))
6}

The SmallModulesFor part matters more than I expected: with one big module, every change relinks and reloads everything; split into small modules, an edit to one Laminar component touches one small file and Vite only pushes that.

Full page state doesn’t survive a reload, of course, Scala.js has no React-Fast-Refresh equivalent that preserves component state. In practice this bothers me less than expected, because Laminar apps tend to keep state in a few top-level Vars, and I keep a tiny dev hack that stashes them in sessionStorage on unload.

Production Is the Same Shape

The nice part is the prod build is the same pipeline with the knobs turned:

1mill frontend.fullLinkJS   # full optimizer + Closure
2vite build                 # hashing, minify, asset pipeline

Vite doesn’t know or care that the JS was born in Scala. Whatever the JS ecosystem does next, this seam should keep working.

Coming from the sbt + webpack days (the old scalajs-bundler setup, which broke if you looked at it wrong), this feels like cheating. Two tools, each doing the thing it’s best at, connected by a single import statement.