Home
Feb 3, 2026

What Scala.js Actually Compiles To

scalascalajscompilersjavascript

Before using Scala.js for anything serious, I wanted to understand what it actually does to my code. “Scala that compiles to JavaScript” hides a two-stage pipeline that’s worth knowing about, because it explains both why the output is good and why the tooling looks the way it does.

Stage 1: Compiler → .sjsir

The Scala.js compiler plugin doesn’t emit JavaScript. It emits .sjsir files, Scala.js Intermediate Representation, one per class, sitting next to your .class files.

The IR is a typed tree format that still knows about Scala things: classes, methods, types, modules. Libraries published for Scala.js (the %%% dependencies) are just jars full of .sjsir files. This is why any pure-Scala library can work on JS: it gets compiled to IR once, and the IR is platform-neutral from that point on.

Stage 2: The Linker

The actual JavaScript is produced by the linker (this is the fastLinkJS / fullLinkJS task you run). It takes the IR of your code plus all your dependencies and does whole-program optimization:

  1. Reachability analysis, starting from your main method and exported APIs, it figures out every method that can possibly be called, and throws away the rest.
  2. Optimizations, inlining, dead branch elimination, unboxing, all with full knowledge of the whole program.
  3. Emission, generate one (or a few) JavaScript modules.

The dead-code elimination is the star. The Scala standard library is big, but you only pay for what’s reachable. A hello-world links to a few tens of KB; a real app with the collections library lands somewhere around 100–300 KB gzipped, which is comparable to a typical React app bundle.

fastLinkJS skips the expensive optimizations for fast dev iteration; fullLinkJS adds the full optimizer plus the Google Closure Compiler for production.

The Semantics Corners

Scala.js promises “the same semantics as the JVM”, with a few documented exceptions. The ones that actually matter:

  • Numbers: Int is a real 32-bit integer (using JS bitwise ops), but Long has no native JS equivalent, so it’s emulated, correct, just slower. Float is usually just a Double underneath.
  • Undefined behaviour: on the JVM, a bad cast throws ClassCastException, reliably. In fullLinkJS these checks are removed for performance, you declared the cast safe, the optimizer takes your word for it. In fastLinkJS they still throw, so you catch them in dev.
  • Reflection: there is none. No Class.forName, no runtime surprises. Everything must be known at link time, which is exactly what makes the dead-code elimination possible.

That last trade-off is the whole design in one sentence: give up dynamic features, get a full-program optimizer in return. Coming from the JVM where the JIT does everything at runtime, it’s a fun inversion, Scala.js moves all the cleverness to link time.