Lua in the Browser: WASM vs Pure JS Ports
Running the Lua programming language in the browser typically involves two main approaches: compiling the native C Lua runtime to WebAssembly (WASM) using tools like Emscripten, or using a pure JavaScript reimplementation such as Fengari. While both methods allow Lua code to execute in web environments, they differ significantly in raw execution speed, JavaScript interoperability, memory management, bundle size, and development complexity.
Performance and Execution Speed
Compiling native C Lua (such as PUC-Rio Lua 5.4) to WebAssembly offers superior raw computational performance. WebAssembly executes as low-level bytecode directly supported by the browser engine, allowing CPU-intensive Lua scripts—such as math operations, game loops, and algorithms—to run at near-native speeds.
In contrast, pure JS ports like Fengari are written entirely in JavaScript to simulate the Lua virtual machine. This introduces an extra layer of abstraction: the browser's JavaScript engine (like V8) must interpret or JIT-compile the JavaScript code that is interpreting Lua bytecode. Consequently, pure JS implementations are noticeably slower for compute-heavy tasks.
JavaScript Interoperability and DOM Manipulation
Fengari excels at JavaScript interoperability. Because it is written in native JavaScript, Lua objects and JavaScript objects can interact seamlessly. Fengari provides an interop library that maps Lua tables, functions, and variables directly to the browser's DOM and JavaScript APIs without requiring serialization, manual memory allocation, or complex bridge layers. Calling a JavaScript function or mutating a DOM element from Lua feels natural and requires minimal boilerplate.
WASM-compiled Lua operates within a linear memory sandbox isolated from JavaScript. Calling JavaScript APIs or interacting with the DOM requires bridging via an interface layer (like Emscripten's Embind or WebIDL). Data passed between Lua and JavaScript must often be marshaled, copied, or referenced across the WASM boundary, adding overhead and developmental friction for DOM-heavy applications.
Memory Management and Garbage Collection
Fengari relies on JavaScript's memory space, but it also maintains its own Lua garbage collector to manage Lua-specific constructs. While this enables easy referencing of JS objects, it can occasionally lead to complex lifecycles and memory leaks if references between the Lua registry and JavaScript's garbage collector are not properly decoupled.
A WASM compilation manages its own linear memory block. Native Lua handles memory via its internal allocator and garbage collector inside this isolated space. While this isolates Lua memory management and prevents complex garbage collector clashes with the browser, it also imposes a fixed or dynamically growing memory footprint that does not automatically shrink back to the host system as efficiently.
Bundle Size and Startup Time
Pure JS ports generally have smaller initial footprints and fit cleanly into standard npm-based web toolchains. Fengari can be bundled, tree-shaken, and loaded directly like any other JavaScript library.
WASM builds require loading both a .wasm binary file and
a JavaScript "glue" wrapper. While the binary itself is compact, the
overhead of the runtime loader and initial memory instantiation can
increase initial load and parse times compared to a lightweight JS
script, particularly on lower-powered mobile devices.
Language Conformance and Ecosystem
Compiling the original C source to WASM guarantees 100% standard Lua specification compliance, including precise behavior for floating-point math, string hashing, and coroutines. It also makes it easier to compile native C-based Lua libraries (such as LPeg) directly into the same binary.
Pure JS implementations must manually mirror Lua's behavior in JavaScript. Fengari implements Lua 5.3 with high fidelity, but differences in underlying JavaScript number representations, regular expressions, or edge cases in metamethods can occasionally surface. Furthermore, native C extensions cannot be compiled into Fengari; only pure Lua modules can be imported.
Summary
Choose native Lua compiled to WASM when your application demands heavy computation, 100% C-Lua standard compliance, or the inclusion of native C extensions. Choose pure JavaScript ports like Fengari when the priority is tight DOM manipulation, seamless integration into modern JavaScript frameworks, and lighter interoperability overhead.