Main Thread Script

The Main Thread Script is a JS script that can be executed on the main thread. The most common use cases for the main thread script are smooth animations and gesture handling. It is primarily used to address the response delay issue 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 small square component, we listen to the scroll event of the scroll-view, retrieve the current scroll position from the event parameters, and update its position immediately:

You can try scrolling the scroll-view on the left side of the example. The blue square on the right side of the page 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 will also increase 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 can only be executed on background threads. Therefore, if you use regular touch events to trigger animations, the event trigger -> event handling -> rendering process will involve multiple thread switches, resulting in untimely responses and animations lagging behind gestures.

mts-threads-1.png

The main thread script provides the capability to handle events synchronously on the main thread, ensuring synchronous event responses.

mts-threads-2.png

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 adding a main-thread namespace to the event attribute name:

<view main-thread:global-bindscroll={onScroll} />

Since the onScroll function is now a main thread event handler, we also need to declare the event handler as a main thread function. This is done by adding a main thread directive as the first line inside the function body:

let onScroll = (event) => {
  'main thread';
  // ...
};

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 state to change the position. When using a main thread function as an event handler, the main thread function accepts an event parameter that contains basic information about the event. The event.target and event.currentTarget parameters differ from those in regular event handlers; they are MainThread.Element objects. This object allows you to conveniently synchronize the retrieval and setting of node properties, such as using setStyleProperty() in the example.

let onScroll = (event) => {
  'main thread';
  const detail = event.detail.scrollTop;
  const newPos = {
    x: 0,
    y: 500 - detail,
  };
  event.currentTarget.setStyleProperty(
    'transform',
    `translate(${newPos.x}px, ${newPos.y}px)`,
  );
};

That's all the changes needed. We will place the components before and after the modification in the same example for you to compare the effects. 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 or other main-thread scripts. However, we sometimes still need data from the background thread.

Fortunately, obtaining data from the background thread inside a main thread function is straightforward: just use it directly, as if it were a normal function.

export default function App() {
  const red = 'red';

  function addBackgroundColor(event: MainThread.ITouchEvent) {
    'main thread';
    event.currentTarget.setStyleProperty('background-color', red);
  }

  return (
    <view main-thread:bindtap={addBackgroundColor}>
      <text>Hello World!</text>
      <text>Hello World!</text>
    </view>
  );
}

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 can only execute after TTI (Time to Interactive). This means they cannot execute during the initial screen load.
  • Main thread functions do not support nested definitions.
  • The constructor, getter, and setter of class components do not support being specified as main thread functions.
  • 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 text would change the color of both lines of text. If we want to change the color of only the first line of text when clicking on the text, it is not easy to achieve this 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 (MainThread.Element).

Create a MainThreadRef using the useMainThreadRef() Hook, and then assign it to the target node's main-thread:ref attribute:

import { useMainThreadRef } from '@lynx-js/react';

export default function App() {
  const red = 'red';
  const textRef = useMainThreadRef<MainThread.Element>();

  function addBackgroundColor(event: MainThread.ITouchEvent) {
    'main thread';
    textRef.current?.setStyleProperty('background-color', red);
  }

  return (
    <view main-thread:bindtap={addBackgroundColor}>
      <text main-thread:ref={textRef}>Hello World!</text>
      <text>Hello World!</text>
    </view>
  );
}

Note that the current property of MainThreadRef can only be accessed within a main thread function.

Passing a Main Thread Function to main-thread:ref

Similar to a regular ref, you can also pass a main thread function to main-thread:ref:

import { useMainThreadRef } from '@lynx-js/react';

export function App() {
  let eleRef = useMainThreadRef<MainThread.Element>();

  function handleTapMainThread() {
    'main thread';
    eleRef.current?.setStyleProperty('height', '30px');
  }

  return (
    <view main-thread:bindTap={handleTapMainThread}>
      <view
        main-thread:ref={(ele: MainThread.Element) => {
          'main thread';
          eleRef.current = ele;
        }}
      />
    </view>
  );
}

You can also return a cleanup function in the main thread function passed to main-thread:ref, just like when using a regular ref.

Using main-thread:ref in Class Components

If you are using traditional class components, you cannot use the useMainThreadRef() Hook. Instead, you can directly create a MainThreadRef object:

import { MainThreadRef } from '@lynx-js/react';

class App extends Component {
  eleRef = new MainThreadRef<MainThread.Element>();

  handleTapMainThread(event: MainThread.ITouchEvent) {
    'main thread';
    this.eleRef.current?.setStyleProperty('height', '30px');
  }

  render() {
    // ...
  }
}

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.

For example, changing the background color of a node based on the number of clicks:

import {useMainThreadRef} from '@lynx-js/react';

function App() {
  const countRef = useMainThreadRef(0);

  function handleTapMainThread(event: MainThread.ITouchEvent) {
    'main thread';
    event.currentTarget.setStyleProperty('background-color', ++countRef.current % 2 ? 'blue' : 'green');
  }

  return (
    // ...
  );
}

Cross-Thread Function Calls

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:

import { useMainThreadRef, runOnMainThread } from '@lynx-js/react';

function App() {
  const countRef = useMainThreadRef(0);

  const addCount = (value: number) => {
    'main thread';
    countRef.current += value;
  };

  const doSomething = () => {
    runOnMainThread(addCount)(1);
  };
}

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:

import { useState, runOnBackground } from '@lynx-js/react';

function App() {
  const [count, setCount] = useState(1);

  const onTapMainThread = (event: MainThread.ITouchEvent) => {
    'main thread';
    runOnBackground(() => {
      setCount((count) => count + 1);
    })();
  };
}
Except as otherwise noted, this work is licensed under a Creative Commons Attribution 4.0 International License, and code samples are licensed under the Apache License 2.0.