Emscripten compiled output can either be run directly in a JS shell from command line, or hosted on a web page. When hosting asm.js and WebAssembly compiled pages as .html for browsers to execute, Emscripten provides a default HTML shell file that serves as a launcher to run the code, simplified to get started with development. However when getting ready to release and host the content on a web site, a number of extra features and customizations are likely needed to polish the visitor experience. This guide highlights things to pay attention to when deploying sites to the public.
-s WASM=1 -o out.html, the compiled code is stored in a file
out.wasm and the runtime lives in a file
out.js. When targeting asm.js there exists an additional binary file
out.mem that contains the static memory section of the compiled code. This part is embedded in the
out.wasm file when targeting WebAssembly.
Additional build output files can also exist, depending on which features are used. If the Emscripten file packager is used, a binary
out.data package is generated, along with an associated
out.data.js loader file. Also Emscripten pthreads and Fetch APIs have their own associated Web Worker related script
.js output files.
emcc -o out.js), the developer is expected to manually create the
out.html main page in which the code is run in browsers. When targeting HTML with
emcc -o out.html (the recommended build mode), Emscripten will generate the HTML shell file automatically. This shell file can be customized by using the
emcc -o out.html --shell-file path/to/custom_shell.html linker directive. Copy the default minimal HTML shell file from Emscripten repository to your project tree to get a good starting template for a customized shell file.
The following sections offer tips for improving the site experience.
.wasm files give on average 60-75% size reductions compared to uncompressed ones, so it practically never makes sense to serve uncompressed files.
Content-Encoding: gzip. This instructs web browsers that the downloaded content should be transparently uncompressed before handing the data off to the page itself.
-s WASM=1linker flag. WebAssembly is an evolution of asm.js, and if your project already successfully compiles to asm.js, it is likely to already work with WebAssembly as well. Compressed WebAssembly output files can be around 20% smaller than compressed asm.js files, but for builds with debugging and profiling information, the difference can even be up to 50%, so the benefit is large.
.mem) should be served with the header
Content-Type: application/octet-stream. WebAssembly
.wasmfiles should be served with
--preload-filelinker flag. This data file is loaded up before the Emscripten compiled application starts to execute
main()function at all, so all files that are stored in this package can greatly slow down the time to start up. Prefer to break up downloaded asset files to multiple separate packages and using asynchronous asset download APIs in Emscripten which can operate while the application is running.
In addition to downloading the page, other parts of the startup sequence can sometimes also be slow. Things to consider here are:
onloadevent of the script tag is called. This can be used to time how long asm.js compilation takes on Safari, Opera and Chrome.
WebAssembly.Moduleobjects can be manually persisted to IndexedDB, which avoids the compilation step altogether on the second run. (see next section)
main()function entry point of the application itself. This is because these two actions are run closely back to back to each other. It is worthwhile to be precise to profile these two actions separately, check out the
src/preamble.jswhich kicks off the execution of application
main()code. If executing
main()takes too long time, consider splitting it out to separate operations that are driven by multiple
setTimeout()calls or by the
While the first run experience of visiting a page can take some time to finish all downloads, the second run experience of the page can be made much faster by making sure that the results of the first visit are cached by the browser.
.datafiles are manually cached to IndexedDB by the main page. The Emscripten linker option
--use-preload-cachecan be used to have Emscripten implement this, although it can be desirable to manually manage this on the html page in a custom manner, since that allows taking control of which database the assets are cached to, and what kind of scheme will be used to evict data from it.
WebAssembly.Moduleobjects to IndexedDB. This feature should be always leveraged, since it allows skipping the whole compilation process on the second page visit.
main()that could be skipped on the second load, use either IndexedDB or the localStorage APIs to cache the results of this computation across page runs. IndexedDB is suitable for storing large files, but it works asynchronously. The localStorage API on the other hand is fully synchronous, but its usage is restricted to storing small cookie style data fields only.
An inherent property of asm.js and WebAssembly applications is that they need a linear block of memory to represent the application heap. This is often the single largest memory allocation that an Emscripten compiled page does, and therefore is the one that is at the biggest risk of failing if the user’s system is low on memory.
Because this memory allocation needs to be contiguous, it can happen that the user’s browser process does have enough memory, but only the address space of the process is too fragmented, and there is not enough linear address space available to satisfy the allocation. To avoid this issue, the best practice is to allocate the
WebAssembly.Memory object (
ArrayBuffer for asm.js) up front at the top of the main page, before any other allocations or page script load actions are done. This ensures that the allocation has best chances to succeed. See the fields
Module['wasmMemory'] for more information.
Additionally, it is possible to opt in to content process isolation specifically for a web page that needs this kind of a large allocation. To utilize this machinery, specify the HTTP response header
Large-Allocation: <MBytes> when serving the main html page. This support is currently implemented in Firefox 53.
Last, it is easy to accidentally cling to large unneeded blocks of memory after the page has loaded. For example, in WebAssembly, once the WebAssembly Module has been instantiated to a
WebAssembly.Instance object, the original
WebAssembly.Module object is no longer needed in memory, and it is best to clear all references to it so that the garbage collector can reclaim it, because the Module object can be dozens of megabytes in size. Similar, make sure that all XHRed files, asset data and large scripts are not referenced anymore when not used. Check out the browser’s memory profiling tool, and the
about:memory page in Firefox to perform memory profiling to ensure that memory is not being wasted.
To provide the best possible user experience, make sure that the different ways that the page can fail are taken into account, and good error reporting is provided to the user. In particular, proceed through the following checklist for best practices.
Aim to fail as early as possible. A large source of frustration for users comes from scenarios where user’s system is not ready to run the given page, but the error only becomes apparent after having waited for a minute to download 100MB worth of assets. For example, try to allocate the needed heap memory up front before actually loading up the page. This way if the memory allocation fails, the failure is immediate and no asset downloads need to be attempted at all.
If a particular browser is known to not be supported, resist the temptation to read
navigator.userAgent field to gate users with that browser, if at all possible. For example, if your page needs WebGL 2 but Safari is known not to support it, do not exclude out Safari users with a following type of check:
if (navigator.userAgent.indexOf('Safari') != -1) alert('Your browser does not support WebGL 2!');
but instead, detect the actual errors:
if (!canvas.getContext('webgl2')) alert('Your browser does not support WebGL 2!'); // And look for webglcontextcreationerror here for an error reason.
This way the page will be future compatible once support for the particular feature later becomes available.
Test various failure cases up front by simulating different issues and browser limitations. For example, on Firefox, it is possible to manually disable WebGL 2 by navigating to
about:config and setting the preference
false. This allows you to debug what kind of error reporting your page will present to the user in such a scenario. To disable WebGL support altogether for testing purposes, set the preference
When working with IndexedDB, prepare to handle out of quota errors when user is about to run out of free disk space or allowed quota for the domain.
Simulate out of memory errors by allocating unrealistically much memory for
WebAssembly.Memory object and for the preloaded file packages, if using any. Make sure that out of memory errors are flagged correctly as such (and reported to the user or to an error database).
Simulate download timeouts either intrusively by programmatically aborting XHR downloads, physically disconnecting network access, or by using external tools such as Fiddler. These types of tools can show up a lot of unexpected failure cases and help diagnose that the error handling path for such scenarios is as desired.
Use a network limiter tool to constrain download or upload bandwidth speeds to simulate slow network connections. This can uncover bugs related to timing dependencies for network transfers. For example, a small network transfer may be implicitly assumed to finish before a large one, but that might not always be the case.
When developing the page locally, perform testing by using a local web server and not just via
file:// URLs. The script
emrun.py in Emscripten source tree is designed to serve as an ad hoc web server for this purpose. Emrun is preconfigured to handle serving gzip compressed files (with suffix
.gz), and enables support for the
Large-Allocation header, and allows command line automation runs of compiled pages.
Catch all exceptions that come from within entry points that call to compiled asm.js and WebAssembly code. There are three distinct exception classes that compiled code can throw:
- C++ exceptions that are represented by a thrown integer and not caught by the C++ program. This integer points to a memory location in the application heap that contains pointer to the thrown object.
- Exceptions caused by Emscripten runtime calling the
abort()function. These correspond to a fatal error that execution of the compiled code cannot recover from. For example, this can occur when calling an invalid function pointer.
- Traps caused by compiled WebAssembly code. These correspond to fatal errors coming from the WebAssembly VM. This can occur for example when performing an integer division by zero, or when converting a large floating point number to an integer when the float is out of range of the numbers representable by that integer type.
Implement a final “catch all” error handler on the page by implementing a
window.onerror script. This will be called as a last resort if no other source handled an exception that was raised on the page. See window.onerror documentaton on MDN.
Do not let the html page “freeze” and bury the error message in the web page console, because most users will not know how to find it there. Strive to provide meaningful error reports to the user on the main html page, preferably with hints on how to act. If updating a browser version or GPU drivers, or freeing up some space on disk might have a chance to help the page to run, let the user know what they could try out. If the error in question is completely unexpected, consider providing a link or an email address where to report the issue to.
Provide meaningful and interactive loading progress indicators to show the user whether the loading progress is still proceeding and what is going to happen next. Try to prevent leading the user to a “I wonder if it is still loading or if it hung?” state of mind.
When planning a testing matrix before pushing a site live, the following items can be a good idea to review.
visibilitychangeDOM events to react to page hide and show events. This is relevant in particular for applications that perform audio playback.
window.devicePixelRatio(DPI) settings, in particular when using WebGL. See Khronos.org: HandlingHighDPI. On Windows and macOS, try changing the desktop display scaling setting to test different values of
window.devicePixelRatiothat the browser reports.
emscripten_set_main_loop()function) to drive rendering, be aware that the rate at which the function is called is not always 60 Hz, but can vary at runtime e.g. when moving the browser window from one display to another in a multimonitor setup, if the displays have different refresh rates. Update intervals such as 75Hz, 90Hz, 100Hz, 120Hz, 144Hz and 200Hz are becoming more common.