You're adding a Kotlin/Wasm browser frontend to an existing KMP library. How do you structure source sets and decide between wasmJs, Kotlin/JS, and Kotlin/Native for the web?
answer
- commonMain = shared, wasmJsMain = JS interop
- expect/actual bridges common to Wasm
- wasmJs: fast + host GC, but modern runtimes only
- Kotlin/JS: runs everywhere today, dynamic, npm
- Kotlin/Native: not a web-frontend option
basics
~20 sPut shared logic in commonMain and Wasm-specific JS interop code in wasmJsMain. Choose wasmJs for a Wasm browser app with JS interop, Kotlin/JS if you need broad runtime support today, and Kotlin/Native is not a web-frontend option.
solid answer
~40 sAdd `wasmJs { browser() }` (with `@ExperimentalWasmDsl`) to the `kotlin { }` block; the plugin creates `wasmJsMain`/`wasmJsTest` source sets that depend on `commonMain`. Keep pure logic in `commonMain` and isolate JS-interop/DOM code in `wasmJsMain` using `external`/`js("...")` and the `JsAny` family — provide `expect`/`actual` if common code needs platform behavior. For the **web frontend** decision: **wasmJs** gives near-native performance and host-GC interop but requires WasmGC+EH-capable runtimes (modern browsers/Node) and the ecosystem is younger; **Kotlin/JS (IR)** compiles to JavaScript, runs anywhere JS runs today, and has a mature library/`dynamic` story but lower ceiling on perf. **Kotlin/Native** produces machine code and is **not** a browser-frontend target (it's for iOS/desktop/embedded), so it's out for the web. Decision drivers: target browsers, library availability, perf needs, and tolerance for experimental status.
code
kotlin · 9 lines// commonMain
expect fun nowMillis(): Double
// wasmJsMain
actual fun nowMillis(): Double = js("Date.now()")
fun main() {
println("started at ${nowMillis()}")
}go deeper
Knows shared code goes in commonMain and Wasm-specific code in wasmJsMain, and that Native isn't for the browser.
Sets up the wasmJs browser target and source sets, uses expect/actual, and gives basic wasmJs vs Kotlin/JS tradeoffs.
Makes a justified target choice from runtime support, library availability, perf, and experimental risk, and structures source sets to avoid interop leakage.
Owns the strategy: dual-target fallback plans, ecosystem-maturity risk, migration path, and aligning the choice with product runtime requirements.
## Source-set structure in KMP A Kotlin Multiplatform module organizes code into **source sets** that form a hierarchy. **`commonMain`** holds platform-agnostic code; each target gets its own leaf source set that **depends on** common. Declaring the Wasm browser target: ```kotlin kotlin { @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) wasmJs { browser() binaries.executable() } sourceSets { val commonMain by getting { /* shared logic */ } val wasmJsMain by getting { // JS-interop / DOM code lives here } } } ``` This gives you **`wasmJsMain`** and **`wasmJsTest`**, both depending on `commonMain`/`commonTest`. ### What goes where - **`commonMain`** — domain models, business logic, serialization, anything with no JS dependency. - **`wasmJsMain`** — `external` declarations, `js("...")` snippets, DOM access, `JsAny` conversions, the app entry point. - Use **`expect`/`actual`** when common code needs a capability that only the Wasm side can implement (e.g. `expect fun currentTimeMillis(): Long` with a `wasmJs` actual using `js("Date.now()")`). ## Choosing the web target ### wasmJs - Compiles to a **WebAssembly module** running beside a JS host. - **Pros**: strong performance, host-GC integration, the path for Compose Multiplatform on the web. - **Cons**: requires **WasmGC + exception-handling** runtimes (modern browsers, current Node); younger ecosystem; **experimental** (`@ExperimentalWasmDsl`); stricter typed interop (no `dynamic`). ### Kotlin/JS (IR) - Compiles to **JavaScript**. - **Pros**: runs anywhere JS runs **today**, mature npm/library interop, supports `dynamic`, broad browser reach. - **Cons**: JS performance ceiling; larger/slower for compute-heavy work than Wasm. ### Kotlin/Native - Compiles to **native machine code** via LLVM for iOS/macOS/Linux/Windows/embedded. - **Not a browser-frontend technology** — you don't ship Native to a web page. (Wasm, not Native, is the in-browser path.) - So for a **web frontend** it's simply **not a contender**; mentioning it is mainly to rule it out. ## Decision checklist - **Target runtimes**: must they include old browsers/Safari versions lacking WasmGC? -> lean Kotlin/JS. - **Library needs**: depend on npm libs / `dynamic`-heavy interop? -> Kotlin/JS today. - **Performance/compute**: heavy in-browser computation, Compose for Web? -> wasmJs. - **Risk tolerance**: can you accept experimental status and proposal-stabilization churn? -> wasmJs. - **Web frontend at all?** -> never Kotlin/Native. ## Pitfalls - Leaking JS-interop types into `commonMain` (breaks other targets) — keep them in `wasmJsMain`. - Forgetting `@ExperimentalWasmDsl` opt-in. - Assuming all KMP libraries publish a `wasmJs` artifact — verify, since support varies.
- Why keep JS-interop code out of commonMain?commonMain is compiled for every target; JsAny/external/js("...") only exist in wasmJs, so putting them in common would break or fail to compile other targets like jvm or native.
- When would you ship both a wasmJs and a js target?To get Wasm performance on modern browsers while keeping a Kotlin/JS fallback for environments lacking WasmGC/EH support, sharing logic via commonMain.
saying these in an interview costs you the question
- Proposing Kotlin/Native to render a browser frontend
- Putting external/js("...") declarations in commonMain
- Assuming every KMP library has a wasmJs artifact
- Ignoring runtime-support constraints when picking wasmJs
- Treating Kotlin/JS and wasmJs interop as interchangeable