丁寧に手を抜く

頑張らない努力

React Nativeで手軽に別スレッドでコードを実行する方法

English version available here:

dev.to

React NativeアプリはJavaScriptで組まれるから、1つのスレッドしかない。 だからCPUヘビーなタスクを実行するとUIをブロックしてしまう。例えばインデクシングとか。 バックグラウンドでJSを実行したければ、react-native-workersみたいなネイティブモジュールを使う必要がある。これはWeb WorkerみたいなAPIを提供してくれるモジュール。でも、単純なコードを実行したいだけならオーバースペック感がある。あと、むやみやたらにネイティブモジュールを増やしたくない。アプリが複雑になるから。expoアプリを作っている人はそもそもネイティブモジュールが使えない。

で、気付いたんだけど、WebViewを使えばいいじゃないか、と。 WebViewには injectJavaScript というメソッドがあって、これを使えば任意のコードをwebview内で実行できる。 しかもアプリのUIスレッドをブロックしない。 なぜならwebviewの中身はSafari(iOS)/Chrome(Android)の別インスタンスだから。 それを確認するために以下のコードを両方のプラットフォームで実行してみた。仮説は当たった:

for (;;) { Math.random() * 9999 / 7 }

そうと気づけばWebViewはとても便利。ネイティブモジュールいらずでJSをバックグラウンド実行できる。 例を以下に示す:

import React, { Component } from 'react'
import { WebView } from 'react-native'

export default class BackgroundTaskRunner extends Component {
  render() {
    return (
      <WebView
        ref={el => this.webView = el}
        source={{html: '<html><body></body></html>'}}
        onMessage={this.handleMessage}
      />
    )
  }
  runJSInBackground (code) {
    this.webView.injectJavaScript(code)
  }
  handleMessage = (e) => {
    const message = e.nativeEvent.data
    console.log('message from webview:', message)
  }
}

コードの実行結果を受け取りたい時は onMessage propを上記のようにwebviewに追加すればいい。 webview側では window.postMessage 関数があって、これを呼び出すと渡したデータがアプリ側に送信される。

以下のようにwebview側で呼び出す:

const message = { ok: 1 }
window.postMessage(message)

するとアプリ側で以下のように結果が得られる:

message from webview:, { ok:1 }

ただし、データの受け渡しにオーバーヘッドが見込まれるので注意する必要がある。巨大なデータを何も考えずに JSON.stringifyJSON.parse すると本末転倒なので気をつける必要がある。 まあ、これはネイティブモジュールを使った場合でも似たような問題かもしれないけど。

That's it! 😄