Main Thread Script
The Main Thread Script is a JS script that can be executed on the main thread. The most common use cases are smooth animations and gesture handling. It addresses the response delay inherent in Lynx's multi-threaded architecture, aiming to achieve a near-native interactive experience.
Event Response Delay in Lynx
Here is a simple animation: a small square that moves in sync with a scroll-view. In the component, we listen to the scroll event, retrieve the current scroll position from the event parameters, and update the square's position immediately:
You can try scrolling the scroll-view on the left side of the example. The blue square on the right will follow the scroll-view's movement. However, you might notice that its movement has an unpredictable delay, especially on devices with lower performance. This delay also increases as the complexity of the page increases.
This is because in Lynx's architecture, events are triggered on the main thread, while regular JS event handlers (Vue's @scroll, etc.) can only be executed on background threads. Therefore, using regular events to trigger animations introduces multiple thread crossings, resulting in untimely responses and animations lagging behind gestures.
The main thread script provides the capability to handle events synchronously on the main thread, ensuring synchronous event responses.
Use Main Thread Functions to Eliminate Event Response Delay
Implementing Animations with Main Thread Script
Synchronizing events using main thread script is very simple. Here we try to modify the previous example.
First, we inform the framework that we want to handle this event on the main thread by using the main-thread-bindscroll attribute instead of @scroll:
Since onScroll is now a main thread event handler, we also need to declare it as a main thread function. This is done by adding a 'main thread' directive as the first line inside the function body:
After declaring it as a main thread function, we can no longer call it from the background thread.
Finally, we can now directly manipulate the element's properties on the main thread, so there's no need to use a reactive ref to change the position. When using a main thread function as an event handler, we can obtain a reference to the target element using useMainThreadRef() and access it via .current inside the main thread function. This object allows you to synchronously get and set node properties, such as using setStyleProperty() in the example:
That's all the changes needed. The example below places the components before and after the modification side by side for comparison. You may notice that the animation delay has disappeared!
Retrieving Data from the Background Thread
You may have noticed that designating a function as a main thread function isolates it from its surrounding context, making it feel like an "island." Its runtime environment is different from other functions, meaning it cannot freely communicate with the background thread.
However, obtaining data from the background thread inside a main thread function is straightforward: just use it directly, as if it were a normal function.
When the main thread function is defined, it automatically captures external variables from the background thread, such as the red variable in the example above. However, you cannot directly modify the values in the background thread.
The values captured by the main thread function are not updated in real time. Instead, they are synchronized from the background thread to the main thread only after the component containing the main thread function re-renders. Additionally, the synchronization requires that the captured values be serializable using JSON.stringify().
To summarize the precautions:
- Main thread functions can and must only run on the main thread. Main thread functions can call each other.
- Captured variables need to be passed between threads using
JSON.stringify(), so they must be serializable to JSON. - Main thread functions do not support nested definitions.
- You cannot modify variables captured from the external scope within a main thread function.
Using main-thread-ref to Obtain Node Objects
In the example above, clicking on the view would change its background color. If we want to change the color of only a specific child element, it is not easy to achieve with just event.target and event.currentTarget. In this case, you can use main-thread-ref to obtain a node object usable on the main thread.
Create a MainThreadRef using the useMainThreadRef() composable, and then assign it to the target node's main-thread-ref attribute:
Note that the current property of MainThreadRef can only be accessed within a main thread function.
Maintaining State in Main Thread Functions
Main thread functions cannot modify captured variables. Therefore, if you need to maintain state between main thread functions, you should use MainThreadRef:
Cross-Thread Function Calls
The examples so far use 'main thread' functions as event handlers. But what if you need to call a main thread function from the background thread, or vice versa? Vue Lynx provides runOnMainThread() and runOnBackground() for bidirectional async communication.
In this example, tapping the box triggers a round trip:
- Main Thread
onTapfires, callsrunOnBackground(incrementCount)()to update reactive state - Background Thread
watch(count)fires, callsrunOnMainThread(applyColor)(nextColor)to change the box color
Asynchronously Invoking Main Thread Functions from the Background Thread
Use runOnMainThread() in the background thread to asynchronously execute a main thread function on the main thread:
Asynchronously Invoking Non-Main Thread Functions from the Main Thread
Use runOnBackground() on the main thread to asynchronously execute a regular function on the background thread:
Cross-Thread Shared Modules
By default, main thread functions cannot directly call plain functions that do not have the 'main thread' directive. This makes code reuse difficult.
To address this limitation, Vue Lynx supports the shared-module mechanism, allowing you to explicitly declare certain modules as shareable between the main thread and the background thread.
Add with { runtime: 'shared' } after the import statement, and the exported variables (including functions, classes, objects, etc.) in that module can be directly called in main thread functions:
Functions in modules imported with with { runtime: 'shared' } can be called by main thread functions, but they are not automatically treated as main thread functions. If they use main-thread-specific capabilities like MainThreadRef, they must still be manually marked with 'main thread'.
The shared module is a regular TypeScript/JavaScript file — it does not need a 'main thread' directive. It must contain only plain functions and constants (no Vue reactivity, no DOM access, no side effects that depend on a specific thread).
Once imported with { runtime: 'shared' }, the exported functions can be called both inside 'main thread' functions and in regular background thread code:
Referencing Third-Party Libraries
Usually, third-party libraries (e.g., motion-dom) contain plain JavaScript functions. If called directly in a main thread function, they will report an error due to the missing 'main thread' directive.
We can use with { runtime: 'shared' } to import them as shared modules:
To facilitate reuse or avoid the limitations below (e.g., losing the shared characteristic after assignment), we recommend wrapping the import in a main thread function:
In this way, animate becomes a standard main thread function that can be freely used in any main thread function, no longer restricted by static analysis:
Limitations
Only identifiers directly imported via import are recognized as "shared". Assigning to a new variable will cause it to lose the shared characteristic:
State Isolation
Variables and state in a shared module are completely isolated between the two threads, each possessing independent instances.
This means that if you modify a variable in a shared module on the main thread, the background thread cannot perceive it, and vice versa. Shared modules solve the "code sharing" problem, not the "state sharing" problem.