react-native-multithreading

repository·master·Indexed 22 days ago

https://github.com/mrousavy/react-native-multithreading

A proof-of-concept library providing multithreading capabilities for React Native using JSI. It allows developers to offload expensive or blocking JavaScript calculations to separate parallel runtimes via the spawnThread function to prevent the main React-JS thread from freezing. Requires react-native-reanimated version 2.1.0 or higher and supports JSC, Hermes, and V8 (iOS only) engines.

Tokens
2.7K
Snippets
7
Records
10
Agent score
78%

What's inside react-native-multithreading

  1. What is possible with react-native-multithreading?

    master

    When using spawnThread, you have the following capabilities:

    • Run arbitrary JS code: Execute any JavaScript logic in the isolated thread.
    • Access external variables: You can use variables from the outer scope (e.g., state), but these values are captured and immutable/frozen in the new thread.
    • Call external functions:
      • Worklets: Functions marked with the 'worklet' directive can be called directly.
      • Native JSI functions: Host functions (e.g., from react-native-mmkv) can be called synchronously.
      • Normal JS functions: Functions like setState can be called back on the React-JS thread using Reanimated's runOnJS.
    • Reanimated Shared Values: You can assign and interact with Reanimated Shared Values.
  2. Install react-native-multithreading

    master

    Install the package using npm and run pod install for iOS.

    Important Requirements:

    • Requires react-native-reanimated version 2.1.0 or higher.
    • Because JSI is not officially released, you must manually edit native files during setup. Refer to the SETUP.md file in the repository for specific instructions.

    Warning: This library is a proof of concept and should not be used in production.

    npm install react-native-multithreading
    npx pod-install
  3. Install react-native-multithreading on Android (without other JSI libs)

    master

    Because pure JSI Modules cannot be autolinked, you must manually initialize them in MainApplication.java. Use this method if you are NOT using react-native-mmkv or other JSI libraries that require custom JSI module packaging.

    1. Open MainApplication.java.
    2. Import MultithreadingJSIModulePackage and JSIModulePackage.
    3. In your ReactNativeHost implementation, add MultithreadingPackage to the getPackages() list.
    4. Override the getJSIModulePackage() method to return a new instance of MultithreadingJSIModulePackage.
    // In MainApplication.java
    import com.reactnativemultithreading.MultithreadingJSIModulePackage;
    import com.facebook.react.bridge.JSIModulePackage;
    
    // ... inside ReactNativeHost implementation
    @Override
    protected List<ReactPackage> getPackages() {
      List<ReactPackage> packages = new PackageList(this).getPackages();
      packages.add(new MultithreadingPackage());
      return packages;
    }
    
    @Override
    protected JSIModulePackage getJSIModulePackage() {
      return new MultithreadingJSIModulePackage();
    }
  4. Install react-native-multithreading on Android (with react-native-mmkv or other JSI libs)

    master

    If your project already uses JSI libraries like react-native-mmkv or react-native-reanimated, you must create a custom JSI package to ensure MultithreadingModule is installed alongside them.

    1. Create a new Java class (e.g., ExampleJSIPackage) in your src/main/java/... folder.
    2. Extend ReanimatedJSIModulePackage.
    3. Override getJSIModules to call MultithreadingModule.install(reactApplicationContext, jsContext) after calling super.getJSIModules(...).
    4. In MainApplication.java, override getJSIModulePackage() to return your new custom package class.

    Note: Ensure you replace com.example with your actual package name and ExampleJSIPackage with the name of the class you created.

    // 1. Create your custom JSI package class
    package com.example;
    
    import com.facebook.react.bridge.JSIModuleSpec;
    import com.facebook.react.bridge.JavaScriptContextHolder;
    import com.facebook.react.bridge.ReactApplicationContext;
    import com.swmansion.reanimated.ReanimatedJSIModulePackage;
    import com.reactnativemmkv.MultithreadingModule;
    import java.util.Collections;
    import java.util.List;
    
    public class ExampleJSIPackage extends ReanimatedJSIModulePackage {
        @Override
        public List<JSIModuleSpec> getJSIModules(ReactApplicationContext reactApplicationContext, JavaScriptContextHolder jsContext) {
            super.getJSIModules(reactApplicationContext, jsContext);
            MultithreadingModule.install(reactApplicationContext, jsContext);
            return Collections.emptyList();
        }
    }
    
    // 2. In MainApplication.java, override getJSIModulePackage
    import com.example.ExampleJSIPackage;
    import com.facebook.react.bridge.JSIModulePackage;
    
    // ... inside ReactNativeHost implementation
    @Override
    protected JSIModulePackage getJSIModulePackage() {
      return new ExampleJSIPackage();
    }
  5. Limitations and Troubleshooting

    master

    Remote Debugging

    Because this library uses JSI for synchronous native method access, remote debugging (e.g., via Chrome) is not possible. You must use Flipper for debugging.

    Workletization

    To ensure code actually runs on the separate thread, all functions invoked within the thread must be 'workletized'. You must add the 'worklet' directive to the top of every function, including the thread callback itself. Ensure the Reanimated Babel plugin is installed.

    Performance Overhead

    There is a small overhead when calling spawnThread because all external variables used within the function must be copied into the new thread. For example, passing large arrays will incur a copy cost. Always benchmark your specific use case.

  6. Use spawnThread to offload work to a separate thread

    master

    The spawnThread function allows you to run expensive calculations or blocking calls on a separate parallel runtime without freezing the main React-JS thread.

    Shoot and Forget

    If you do not need the result of the calculation, call spawnThread without awaiting it. The React-JS thread will continue execution immediately.

    Await Results

    Since spawnThread returns a Promise, you can await the result. The main React-JS thread remains unblocked (timers, callbacks, and rendering continue to function) while the custom thread processes the work.

    Note: Every function called inside the thread, including the callback itself, must include the 'worklet' directive to ensure it runs on the separate thread.

    // Shoot and Forget
    spawnThread(() => {
      'worklet'
      // expensive calculation
    })
    
    // Await
    const result = await spawnThread(() => {
      'worklet'
      // expensive calculation
      return result
    })
  7. Configure Metro for react-native-multithreading example

    master

    When setting up the react-native-multithreading-example project, the metro.config.js must be configured to handle peer dependencies correctly. This prevents multiple versions of the same dependency from being loaded by blacklisting the versions located in the monorepo root and aliasing them to the versions inside the example's node_modules via extraNodeModules.

    const path = require('path');
    const blacklist = require('metro-config/src/defaults/exclusionList');
    const escape = require('escape-string-regexp');
    const pak = require('../package.json');
    
    const root = path.resolve(__dirname, '..');
    const modules = Object.keys({
      ...pak.peerDependencies,
    });
    
    module.exports = {
      projectRoot: __dirname,
      watchFolders: [root],
    
      resolver: {
        // Blacklist peer dependencies at the root to avoid duplicate loading
        blacklistRE: blacklist(
          modules.map(
            (m) =>
              new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`)
          ),
        ),
    
        // Alias peer dependencies to the versions in the example's node_modules
        extraNodeModules: modules.reduce((acc, name) => {
          acc[name] = path.join(__dirname, 'node_modules', name);
          return acc;
        }, {}),
      },
    
      transformer: {
        getTransformOptions: async () => ({
          transform: {
            experimentalImportSupport: false,
            inlineRequires: true,
          },
        }),
      },
    };
  8. Run functions on a separate thread with spawnThread

    master

    Use spawnThread to execute a function in a custom thread within a parallel runtime.

    Important Note: Despite the name, threads are not spawned on demand; they are managed via a thread-pool and will be re-used for efficiency.

    To use it, pass a function to spawnThread. If you need to return a value from the thread to the main JS thread, you can await the promise returned by spawnThread.

    const result = await spawnThread(() => {
      const someValue = doExpensiveCalculation()
      return someValue
    })