We are proud to announce the first public release of Polygen, our innovative toolkit for running WebAssembly in React Native applications, with near-native performance.
Introduction
React Native is an extraordinary piece of technology. It allows you to use JavaScript and TypeScript—the languages we all know—together with React to build performant mobile applications. Contrary to other solutions on the market, React Native leverages native UI components for each platform, making the application fit the ecosystem and the platform’s appearance rather than recreating it.
Let’s not forget that it started as a library for creating single-page applications (SPAs) on the Web. It quickly gained traction and support in the community due to its simplicity and unique approach for working with layout changes—Virtual DOM was something only few people heard of back then.
Today, we can create both native applications and web apps using a single (assuming the usage of react-native-web) codebase using React. We can share business logic and have separate UI layers, or share everything using react-native-web. This gives us a great developer experience, fast iteration times, and faster time to market.
React Native makes it easy to create a mobile application for an already existing React web app by sharing logic code and the other way around, creating a web app from your React Native application. Both journeys are great starting points, but there are quite a few missing pieces and rough edges that we, as a community, are constantly working to fix.
The problem
Unfortunately, there’s one thing that you can stumble upon when trying to bring your React Web App to React Native. If your Web App is working with heavy computations or any other CPU intensive tasks, you are probably using an external library using WebAssembly, as it is the only thing in the Web toolkit that is available. This is what enables efficient 3D graphics in the browser (among other things).
However, there was no easy way to run them in React Native until now. When you wanted to migrate them to React Native, you had two choices:
Use react-native-webassembly
This is probably the easiest approach. This package provides a WebAssembly runtime powered by wasm3. While it works, it has the overhead of interpreting the modules. If you seek performance, this might be a no-go, depending on the module you are trying to run.
You cannot use a faster runtime that performs just-in-time (JIT) compilation of those modules, because it is not allowed on Apple mobile platforms.
Create a Turbo Module for each external dependency
This is the most performant solution, as you have access to native platform APIs. You can, for example, spin up native threads for heavy computations to achieve the best performance. However, you need to write (and most importantly, maintain) the Turbo Module for each native dependency, which includes exposing C or C++ API that can be called from Turbo Modules and bridging the API to React Native using JSI. That’s a lot of work.
Introducing Polygen
At Callstack, we were not satisfied with the available options. While we are huge fans of cross-platform code sharing, we don’t want to sacrifice runtime performance for end-users.
After weeks of work, we are introducing to the React Native Community a new approach for this problem: Polygen, a toolkit for creating native React Native Modules from WebAssembly modules.
How it works
While most WebAssembly runtimes interpret the modules or perform just-in-time compilation at application runtime, Polygen takes a novel approach of compiling the modules to native code (C code specifically), which becomes part of the application itself during compilation and linking.
Each module is compiled ahead of time, providing near-native performance while using the Web API you already know and use.
Consider this simple WebAssembly module, which exports a single function add, that returns the sum of two numbers:
To use it in React Native using Polygen, you consume it as you would on the Web (with the sweet metro bundler integration):
Because the code is precompiled, you are free to use it in the App Store ecosystem, where just-in-time compilation is prohibited.
The possibilities
WebAssembly is a well-defined binary target supported by multiple languages. It is also platform and architecture-agnostic, which makes it a perfect fit for the multi-platform environment React Native.
This allows us to:
- make code written in different languages (e.g., Rust) to be easily embedded and used in React Native applications,
- share and consume precompiled libraries without building them from source,
- simplify native modules in out-of-tree React Native platforms,
- sandbox external modules.
The limitations
Not everything is perfect yet. There are some limitations to be aware of:
- No dynamic modules—Because of the approach taken, you cannot load arbitrary modules and execute them at runtime. Each WASM module must be known and defined ahead of time.
- Execution on JS thread—The WebAssembly functions you call are being executed on a JS thread, which may block it. In the future, this could be solved by adding support to Web Workers. We are also actively researching alternative solutions to this problem using alternative APIs.
- WebAssembly is in the early stages—WebAssembly is a young standard. It is supported in all major browsers today and the modules are already faster than analogous JavaScript.
It is not quite there yet compared to native code, though. The instruction set and environment are limited, which may negatively impact performance. Due to those limitations, some languages cannot be reliably compiled to WebAssembly. Compiling native code to WebAssembly usually results in a slightly slower execution time.
The good news is that this will only get better with time. There are numerous proposals to enhance the WebAssembly standard with new instructions and new functionalities, which will lower the impact of the mentioned issues.
How to start
Head to our Polygen repo and follow the README to get started.
What’s next
We are constantly working on improving Polygen, so there are a few things that are not there yet but will be in the near future. These include (but are not limited to):
- Android Support—Right now, only iOS is supported because Polygen integrates with CocoaPods. We are working on integrating Gradle in the upcoming releases.
- WASI support—Most WebAssembly modules expect some kind of system interface that needs to be provided by the embedder (you). WebAssembly System Interface is just that—a collection of interfaces that can be used to communicate with the host system. Today, when you use modules that depend on WASI, you must provide them manually, and due to React Native Runtime, you cannot easily use Node packages that provide WASI.
- Better support for External Languages output—Each language that compiles to WebAssembly usually also generates some JavaScript (or TypeScript) glue code. The code simplifies usage of the module by wrapping the WebAssembly WebAPI and exposing typed functions. Due to multiple approaches to bundling in the JS ecosystem and multiple runtimes, generated JS wrappers do not work in some cases because of runtime assumptions (e.g., generating glue code for the node runtime expects the fs module to exist).
Let’s get in touch
Polygen is still in the heavy development phase. If you are interested, make sure to subscribe to our Callstack Incubator newsletter. And, of course, let us know what you think!