In this post, I describe porting a small application, a calculator called SpeedCrunch, to WebAssembly and some common issues that need to be addressed. I hope you may find some solutions helpful and save time should you want to target the WebAssembly platform with your code.
For those eager to see the result, the application is available here: SpeedCrunch (WebAssembly)
Introduction
Porting a Qt application to WebAssembly (WASM) presented some unique challenges. Qt, known for its cross-platform capabilities, initially posed difficulties when targeting WebAssembly, but improved support in later versions smoothed the process. Announced in 2015 and first released in March 2017, WebAssembly was added as an officially supported target to Qt with Qt 5.13.
The application I wanted to port is a small but highly practical calculator called SpeedCrunch. The desktop version was my trusted companion for more than a decade, particularly for work involving binary number manipulation, thanks to its efficient binary digits editor. This feature was invaluable for working with binary representations, a common task in my professional workflow as a CPU architect. Beyond work, I also used the application for various personal calculations.
Click to enlarge the image.
Understanding the Landscape
WebAssembly (WASM) is a low-level, binary instruction format designed for efficient execution on web browsers. It enables high-performance applications to run in the browser by providing a compilation target for languages like C, C++, and Rust, allowing near-native performance. WASM is supported by all major browsers and is designed to work alongside JavaScript, enabling complex applications to run smoothly in web environments.
Qt is designed for cross-platform development and supports desktop, mobile, and embedded systems. However, WebAssembly introduces an entirely different execution environment, requiring adaptations to achieve a seamless transition. Early Qt versions provided limited support for WebAssembly, creating hurdles in the initial stages of the porting process. Even today, only about a dozen or so Qt modules are ported to WebAssembly (major modules like “Qt Core,” “Qt GUI,” and “Qt Widgets”), and even those have functions that are still not supported.
Qt targets WebAssembly through Emscripten. Emscripten is a toolchain for compiling C and C++ code to WebAssembly. Emscripten also provides a compatibility layer for standard libraries and APIs.
Qt versions are closely tied to Emscripten versions because the level of WebAssembly support in Qt often depends on the features and updates provided by Emscripten. Earlier versions of Qt had limited or experimental support for WebAssembly, and compatibility issues were common. As Emscripten matured, subsequent Qt versions improved their integration, resulting in better performance, broader module support, and a more reliable build process.
This Qt document describes which Emscripten version works with which Qt version and how to activate it. Emscripten needs to be downloaded and configured separately from Qt.
Another important consideration is that WebAssembly applications must be served by a web server to function correctly; they cannot be run by simply opening the .html file in a browser. This is because WebAssembly relies on browser security protocols that restrict local file access and enforce strict content delivery methods. Even for local testing, running a lightweight development server—such as Python’s http.server or Node.js’s http-server—is necessary to run an application. However, when developing in QtCreator, the IDE simplifies this process by automatically setting up and launching a local web server to serve the WebAssembly code during testing. This feature allows developers to focus on development and debugging without manually configuring and running a server.
Multi-Threaded vs. Single-Threaded WebAssembly
When you run the Qt Maintenance Tool, you’ll notice it offers two WebAssembly versions: single-threaded and multi-threaded. So, what’s the difference when setting up a Qt project for WebAssembly?

Single-threaded WebAssembly is the straightforward option. Everything runs on one thread, which keeps things simple but can be limiting if your app needs to juggle heavy tasks. The upside? It plays nicely with almost every browser, and you don’t have to jump through hoops with security configurations. If your app isn’t crunching massive data sets and you want the broadest compatibility, this is the easier path.
Multi-threaded WebAssembly, though, is where the performance magic happens. It uses multiple threads, letting your app handle bigger workloads in parallel. This is perfect for apps that need to do some heavy lifting. But, there’s a catch—it requires browsers that support SharedArrayBuffer and some specific security headers (Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy). So, while your app will be faster, setting it up is a bit more of a headache, and not every browser will support it. You may get an error like this:

To sum up: if you’re chasing speed and don’t mind a bit of extra setup, go for multi-threaded. If you want it to work everywhere without too much hassle, single-threaded is your friend. For this application, single-threaded worked just fine.
Major Issues with Recompiling Using the Newest Qt/WebAssembly Version
The latest prebuilt (and released as binary) SpeedCrunch version was from September 2016, using Qt 5.6.2, and there has not been a newer official release, which means most people (who don’t rebuild such apps themselves) are running an almost decade-old version.
Luckily, the source code isn’t stale. It has been receiving numerous changes in the meantime, including a port to a more recent Qt version 6. Since I had Qt 6.7.2 installed, I tried first compiling it using that version, but the app behaved badly—it would hang whenever I clicked on any menu, and it kept losing focus. Then, I installed a newer Qt 6.8.2 (and the updated Emscripten version), and that (eventually) worked.
The very first problem I faced with version 6.8.2 was a compilation issue. Qt/WASM (6.8.2 + 3.1.56) could not handle compiling this construct:
<QHash<QMap<QString, Rational>, Unit> Units::m_matchLookup;
This simply wouldn’t compile. I tried several other variations on that data structure, but the Emscripten C++ libraries would not handle them, giving various errors. That particular code was part of the units lookup (expressions involving “newton,” watt”). In the end, I commented it out as the rest of the code should autogenerate unit name as products of base units.
Although I encountered this particular compilation issue with the QHash construct in this specific application, it is important to recognize that similar major issues could arise with other codebases or different versions of Qt/Emscripten. With each new version, the code may simply stop compiling or start randomly exhibiting various issues, and you should be prepared to try different versions and, in some cases, even forgo the newest version if it does not work.
File Access in WebAssembly
One of the more complex challenges when porting a Qt application to WebAssembly is handling file access. WebAssembly runs in a secure, sandboxed environment within the browser, which means it doesn’t have direct access to the user’s local file system. Instead, it uses virtual file systems provided by Emscripten, such as the in-memory MEMFS or the persistent IDBFS, which utilizes the browser’s IndexedDB for storage.
Loading and saving files becomes a multi-step process. For instance, to allow a user to open a file, you must rely on browser-provided file input elements or drag-and-drop interfaces. Once a file is selected, it can be read into the WebAssembly virtual file system. Saving files is equally restricted. The application needs to trigger a download or store the file within IndexedDB to preserve it for future sessions.
One key challenge is managing the temporary nature of these virtual systems. Files stored in MEMFS are lost when the session ends, and although IDBFS offers persistence, syncing files requires explicit calls to save or load data, which can be cumbersome.
For apps designed initially with traditional file access in mind, these restrictions demand significant rewrites or workarounds. Applications that require extensive, low-latency access to local files or rely on continuous file system interaction are generally not feasible to port to WebAssembly.
I found that simplifying the file operations and relying on user-initiated actions like file uploads and downloads (through the app’s menu system) was the most reliable approach. While it’s not as seamless as direct file access, it aligns with browser security policies and keeps the application user-friendly.
Fortunately, Qt for WebAssembly provides several functions specifically designed to handle local file access in this restricted environment. Functions like QFileDialog::getOpenFileContent() and QFileDialog::saveFileContent() are supported and allow users to load and save files as they would in a traditional desktop application. Under the hood, these dialogs interact with the browser’s file picker interface, ensuring users can select and save files without breaking the sandbox model.
In contrast, if you make calls to QFileDialog::getOpenFileName() or QFileDialog::getSaveFileName(), Qt will open a file picker dialogs that are tied to its sandbox file system, as shown here:

Addressing Corner Cases and Minor Bugs and Irritants
One issue I faced was with the About dialog. Initially, it was implemented as a modal dialog, which meant it blocked the main application until the user closed it. In the WebAssembly environment, this behavior caused the app to hang. Execution couldn’t continue until the dialog was dismissed, but in some cases, the dialog didn’t even render correctly, leaving the app stuck. The solution was to make the About dialog non-modal, allowing the main application to continue running while the dialog remained open.
Another irritant I encountered was with the context menu in the status widget. Currently, Qt/WASM (6.8.2 + 3.1.56) cannot correctly handle Qt::ActionsContextMenu from the status widget, resulting in the engine hanging. To resolve this, I rewrote the menu to use Qt::CustomContextMenu instead. This change ensured that the context menu behaved correctly without causing the application to freeze.
That said, I want to emphasize the need to thoroughly test every feature, option, and menu while staying alert to subtle issues. Even if you know that a function or feature works in a different environment, testing it in WebAssembly is essential, as it may behave differently or not function at all.
Qt continues to face challenges in maintaining consistent multi-platform compatibility for WebAssembly, making thorough testing critical to catch and address issues specific to this platform.
Updating and Improving SpeedCrunch
Working on a new release of SpeedCrunch opened some avenues for refinement.
One improvement was reintroducing the ability to toggle the main menu using CTRL+M. This feature had been part of older app versions but was removed at some point. However, some users requested its return, so I coded it back in. Now, with a simple keyboard shortcut, users can quickly toggle the menu.
Another improvement I made was to the vertical scroll bar. I often find some “modern UI” decisions perplexing. As monitors have become larger and denser, essential UI elements seem to be disappearing—window edges shrink to a single pixel, scroll bars vanish, and application bars are removed. For instance, in MS Word, it’s unclear where to click to drag the application window. Resizing a window can feel like a challenge, requiring the mouse to hover over just the right pixel (especially on Windows). Meanwhile, wasted space between menu items keeps increasing. Okay, rant over.
I increased the width of the scroll bar selector. Maintaining the app’s clean look and feel is still reasonably thin, but it’s much easier to grab and use now.
I also updated the web links within the app. The most important update was fixing the donation link, which had been pointing to the wrong address. It’s now correctly directing users to the intended destination.
Lessons Learned
The biggest lesson from this experience was learning about the specific restrictions and tradeoffs that come with WebAssembly. If I were to write an app with WebAssembly support in mind from the start, I would have a clearer idea about which Qt modules and function calls to use—and which to avoid—to ensure better compatibility and performance. Keeping the Qt version and Emscripten toolchain up to date was crucial in avoiding many issues that affected earlier versions. I also learned to expect tradeoffs—some desktop-specific features needed redesigning or omitting to ensure the correct web experience.
One of the most challenging aspects was debugging WebAssembly. While I’m sure it’s possible, I found it very difficult, if not impossible, to debug effectively. Instead, I resorted to rebuilding for the desktop target when I needed to debug something, which proved to be a much simpler approach.
Conclusion
In this article, I have presented some of the problems encountered while porting the SpeedCrunch application to WebAssembly and the solutions that worked at the time of writing. Porting a Qt project to WebAssembly is an intricate but rewarding process. With improved support in recent Qt versions and diligent problem-solving, it is possible to achieve a browser-based experience that closely mirrors the desktop application.
Links:
Original SpeedCrunch source: heldercorreia / speedcrunch — Bitbucket
Location of my fork: gdevic / speedcrunch — Bitbucket
Qt for WebAssembly: Qt for WebAssembly | Qt 6.8
Emscripten: Building to WebAssembly – Emscripten
Notes:
- While you can serve WebAssembly files from Apache or Nginx, running on your local PC requires that you run a web server locally. Emscripten SDK provides emrun (link), Python has http.server module that you can install and run, and there are also several applications to do the job, for example, “Simple Web Server” (link).
- File system differences are just one of several major areas where an application might need custom coding for this platform. Font access is another challenge. Multiple system fonts are readily available on most platforms, but Qt’s WebAssembly package includes only three built-in fonts. If you need to use a different font that would typically be available on other platforms, you must explicitly add it to your application’s resources and load it manually within the app.