How to build high-performance front-end applications based on multithreading
2023-12-29
Motivation
As modern front-end applications become larger, using multiple CPU cores to improve performance is becoming increasingly important.
Front-end applications often run in a single browser window, where JavaScript executes on a single thread. This means that common web applications cannot fully utilize a CPU’s multiple cores. As applications become larger and more complex, this can lead to performance problems and a poor user experience.
The good news is that modern browsers widely support different types of workers, including Shared Workers. As older browsers and Safari versions without Shared Worker support are gradually phased out, workers become a more practical foundation for multithreaded front-end architecture. Shared Workers allow multiple JavaScript execution contexts to communicate with the same worker instance, making them useful for building shared front-end application services.
Multithreaded front-end applications offer several benefits. They can move computation-intensive or slow-running JavaScript away from the rendering path, improving responsiveness. They can also isolate request-heavy logic so it does not block the main application flow.
Therefore, we aim to explore a web application framework that leverages multithreading.
Web application with Multithreading
In a multithreaded web architecture, we can leverage the Shared Web Apps concept of reactant-share to expand upon general multithreading programming.
Shared Web Apps allow web applications to run across multiple browser windows or workers. The model uses a single front-end server, such as a Shared Worker, to share code, local storage, state, and other resources across web app instances. Regardless of how many browser windows are opened, there is only one server application instance shared among multiple client applications. This lets web tabs focus on rendering, better uses multiple CPU cores, and improves application responsiveness.
Shared Web Apps offers the following benefits:
- Reduces the mental burden of multithreaded programming through isomorphism in a universal modular model. Isomorphism means the same code can execute on the server thread, client thread, or other worker threads.
- Keeps the front-end server thread responsive by transferring compute-intensive tasks to another thread. This lets the server thread focus on business logic and the client thread focus on rendering.
- Improves request concurrency through a more efficient multithreading model.
Coworker based on reactant-share
Building upon reactant-share, we implemented the Coworker model. It supports state sharing across multiple threads, synchronizes state changes, and uses patches to minimize transfer size during multithreaded execution.

The Coworker model comprises three types of threads:
- client thread: The rendering thread. It receives shared state and renders the web UI. Keeping it lightweight helps preserve smooth rendering.
- server thread: The main application logic thread. It executes most business logic, so responsiveness is also important here.
- Coworker thread: A worker thread for compute-intensive or request-heavy logic. It prevents these workloads from blocking the server thread.
In ‘Base’ mode, Reactant Shared Apps has only two threads: the Tab thread and the Coworker thread. By default, the Coworker thread utilizes a Web Worker.
Implementation of Coworker
For details on the underlying principles of Reactant-Share, see: https://reactant.js.org/blog/2021/10/03/how-to-make-web-application-support-multiple-browser-windows
Coworker comprises two modules:
- CoworkerAdapter: Provides a communication channel between the server thread and the coworker thread.
- CoworkerExecutor: Manages the synchronization of shared state between threads and custom Coworker type modules (used for proxy execution of coworkers). Coworker state is synchronously sent to the main thread in one direction. Each time a Coworker synchronizes its state, it includes a sequence tag. If the sequence is out of order, a complete Coworker state synchronization is automatically triggered to ensure the consistency of the shared state between the Coworker and the main thread.
Core Concepts and Advantages of Coworker
- Isomorphism: The ability for all threads to execute the same code enhances the maintainability of multithreading programming in JavaScript.
- Thread interaction based on the Actor model: By leveraging the Actor model, this approach reduces the cognitive load of multithreaded programming in JavaScript.
- Generic Transport Model: Coworker supports any transport mechanism based on data-transport (https://github.com/unadlib/data-transport), enabling it to run in any container that supports transport, including SharedWorker. The following is a list of supported transports:
- iframe
- Broadcast
- Web Worker
- Service Worker
- Shared Worker
- Browser Extension
- Node.js
- WebRTC
- Electron
- Any other port based on data-transport
- High performance based on Mutative: Mutative provides high-performance immutable updates. Patches generated from shared state updates are used for state synchronization.
- High performance: Because Coworker handles many request-heavy and compute-intensive tasks, the main thread and rendering thread can stay more responsive.
- Support for Large Applications: Reactant offers a complete module model design, including dependency injection and a class-first approach, along with various modular design patterns and dynamic module injection capabilities.
- Separation of service and rendering view modules: Service modules focus on business logic and can execute independently from view modules. This improves separation of concerns and allows each thread to have its own container.
- Graceful Degradation: If the JavaScript host environment does not support SharedWorker, Coworker gracefully degrades to a regular SPA. This does not affect the behavior of existing applications.
API
delegate() - This function forwards execution to the specified module and function proxies within the Coworker, drawing inspiration from the Actor model.
Examples
We will create a Counter application using Coworker based on the Base pattern.
- First, create
app.tsx, which contains theProxyCountermodule that will be executed in the Coworker.
The call delegate(this.proxyCounter, 'increase', []) is the same style used in general Shared Web Apps. Whether it executes through a Coworker proxy depends on the createApp configuration.
1 | import React from "react"; |
- Create the main file,
index.ts. Here, we configureProxyCounteras a Coworker module and setisCoworkertofalse.
1 | import { render } from 'reactant-web'; |
- Create the Coworker file,
coworker.ts. Here, we also configureProxyCounteras a Coworker module, but this time setisCoworkertotrue.
1 | import { |
At this point, we have created a basic application using Coworker. Users trigger delegate(this.proxyCounter, 'increase', []) in the main thread through the UI. This action is forwarded to the Coworker, which executes proxyCounter.increase(). The shared state then synchronizes back to the main thread, and the rendering update is handled by the useConnector() Hook.
Q&A
1. What are the challenges of multithreaded programming with Coworker based on reactant-share?
State sharing and synchronization across threads are inherently complex. reactant-share improves robustness through a consistent shared state design, but dependencies between isomorphic modules inside Coworker still need careful design. During development, concepts such as Domain-Driven Design can help prevent poor module boundaries.
2. What are the potential use cases for Coworker?
- Request Queue: Coworker is particularly well-suited for modules handling a high volume of requests. Running these within Coworker prevents them from occupying the main thread’s request queue, allowing other main thread requests to execute unimpeded.
- Large Task Execution Blocking: To avoid blocking the application’s main thread during the execution of computationally intensive tasks, such tasks are ideal for asynchronous execution within Coworker.
- Isolatable Modules: Coworker can also serve as a sandbox to isolate the execution of specific modules.
3. Are there specific examples demonstrating how Coworker can improve application performance?
In production, we used Coworker for modules that perform text matching over large datasets. In those scenarios, performance improved substantially, in some cases up to 10x. Previously, computationally intensive text matching could block the page for more than 1 second. After moving the work into Coworker, blocking time was reduced to less than 100 ms. The actual improvement depends on data size and workload shape.
4. Is Coworker usable across different browsers, or is its support limited to within browser tabs? Can Coworker be used across tabs in different domains?
Coworker is a multithreaded model built on reactant-share, which is based on data-transport. Using the WebRTC transport from data-transport inside Coworker’s CoworkerAdapter can support cross-browser communication. To support tabs across different domains, Coworker can also be implemented with an iframe and SharedWorker approach.
Conclusion
Front-end development is at a turning point, driven by advances in browser capabilities and multi-core devices. Workers, including Shared Workers, can now be used more effectively in front-end development. Shared Web Apps with Coworker introduce a multithreaded model for front-end applications that can improve performance, user experience, and code maintainability in suitable scenarios. For developers, this creates more technical choices, more challenges, and more opportunities.
Multithreaded programming for front-end applications is likely to become a key solution for enhancing front-end performance. This will result in a smoother, more efficient, and more responsive user experience.
- reactant-share Document:https://reactant.js.org/docs/shared-app
- reactant-share Repo: https://github.com/unadlib/reactant/tree/master/packages/reactant-share