React Nativeで手軽に別スレッドでコードを実行する方法
English version available here:
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.stringify
や JSON.parse
すると本末転倒なので気をつける必要がある。
まあ、これはネイティブモジュールを使った場合でも似たような問題かもしれないけど。
That's it! 😄