I did a fun/experimental project using Popcorn, a library that lets you run Elixir in the browser!
It bundles your Elixir app with AtomVM, a minimal BEAM implementation, and runs it on a WASM runtime.
It’s a portfolio page (that will perhaps help me getting a job in Elixir which is not gambling or user tracking tech ): https://marcin-koziej.cahoots.pl/
Source on Github
Thanks @mat-hek and the SoftwareMansion team for this impressive work!
Some thoughts and learnings:
-
Early stage tooling: Popcorn is in active development, which meant I had to do some debugging, trial and error, as well as reading the Popcorn source code. The documentation could use contributions, but the examples provided with the source code are a great starting point.
-
WASM vs non-WASM context: The bundled Elixir app runs on the WASM runtime, but running IEx or tests runs on a standard BEAM VM. In the BEAM, the NIF module required by JS interop isn’t available, so your code will raise
:nif_not_loaded
when usingPopcorn.Wasm
APIs. This means the interop code is best isolated, making it easy to mock out/disable for testing Elixir code or for interactive exploration in the IEx REPL (I do this a lot).Perhaps I could run tests and IEx on a WASM runtime in the shell? I didn’t explore this direction.
-
Elegant JS interop: You can do
GenServer.call
andGenServer.cast
from the JavaScript side to the Elixir side, and you can execute JS functions with arguments from the Elixir side. Popcorn provides an object proxy calledPopcorn.TrackedValue
, which is a reference to a value on the JS side (DOM node, object, string, etc.). When it’s garbage collected on the Elixir side (e.g., when the process holding it in its state stops), it will be released on the JS side. There’s a special cleanup callback you can use if cleanup is more complex. Basic Elixir types are converted to JS and vice versa (atoms are one-way converted to strings). Some types (like Pid or Reference) don’t work. -
Use cases: I have a feeling that for UI-heavy apps, there’s a lot of JS interop, which results in lots of small JS snippets scattered around your codebase (which is a liability because of #2). It probably makes more sense to put a “backend” Elixir app in the browser and use a more typical JS frontend framework to drive the UI. Migrating a web app into an offline-first desktop app comes to mind.
-
AtomVM limitations: AtomVM doesn’t implement all BEAM modules, which generates non-obvious errors. For example, the
timer_manager
module doesn’t work, which meansProcess.send_after
will crash your app. Same forLogger
, which has a timestamp in its default formatting, which in turn breaksDynamicSupervisor
(which does some logging by default).Generally, this work required a lot of printf-style debugging and trying out what works versus what will silently crash the app.
-
Deployment: You can deploy to any static hosting which allows you to set COOP and COEP headers (latest security requirement to run WASM). These are not supported in Github Pages, so I am deploying to Netlify. The WASM assets sizes:
31K static/wasm/AtomVM.mjs.gz
190K static/wasm/AtomVM.wasm.gz
3.4M static/wasm/bundle.avm.gz
1.5K static/wasm/popcorn_iframe.js.gz
2.9K static/wasm/popcorn.js.gz