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.