off-the-main-thread の時代

off-the-main-thread は今フロントエンドで熱いテーマの一つです。日本語圏では今ひとつ話題になってないので紹介しておきます。

off-the-main-thread の概念の大まかな概要については、Chrome 開発者の nhiroki さんの日本語の記事があるので、こちらを参照してください。

nhiroki.jp

speakerdeck.com

ここまでのあらすじ

従来のウェブブラウザーでは、一つの画面につき一つ割り当てられる、UI スレッドと呼ばれる名前空間で様々な処理を行ってきました。DOMセマンティクスの評価, CSS による rendering / painting、JSのScripting…。もちろん裏側ではブラウザが様々なバックグラウンドサービスに処理を委譲し、スレッドで実行され、その非同期な結果を受け取っているわけですが、少なくともUIスレッドで走るJSからコントロールできる範囲ではシングルスレッドです。

このような状況で、最近のフロントエンドの重厚長大化は、CPUの専有という形で、ユーザーの体験の悪化につながっている、と Chrome デベロッパーの Alex Russell は指摘しています。

The "Developer Experience" Bait-and-Switch | Infrequently Noted

僕個人の意見としては、フロントエンド開発者にとっては拡充される機能郡は一つの福音でありつつも、現実にはIEというブラウザに律速に縛られています。その結果、将来的なゴールは示されているが、現実が追いついていない、ならポリフィルとしての層(babel や webpack)が分厚くなるのは当然だという気がします。

havelog.ayumusato.com

とはいえ、HTML5 と PWA という2つのムーブメントで、ブラウザの機能的な不足は段階的に解消されてきました。また将来に渡って改善していくためのディスカッションの場も、先の十年よりも遥かに増えており、リリースサイクルも主要なブラウザはほぼローリングリリースを採用しており(Safari の不透明感はありつつも)そこまで悲観するものではない、という状況です。実際には、短命なモバイルデバイスのライフサイクルに助けられている形ですが…。あとは、他にEOLなIEが死ぬのを待つだけ。

blog.jxck.io

フロントエンドのリッチ化は ー ときに批判にさらされつつも ー 機能的な不足は解消されつつあり、代わりにパフォーマンスの問題に向き合う時期が来た、というのが自分の認識です。

どう向き合うか

メインスレッドが専有されるとどういう問題が起きるでしょうか?具体的には、 16ms を超えてCPUメインスレッドが busy になると、クリックやスクロールなどが即座に反応しなくなります。いわゆる「もたつき」として認識されます。

off-the-main-thread は、言葉から分かる通り、マルチスレッド化するということより、メインスレッドではない、という部分に重きがあります。つまりこのもたつきを発生させる処理を別スレッドに逃すということです。

実際のソリューションは、既に主要なブラウザに搭載されていたが、今まであまり使われていなかった WebWorker を使おう、ということになります。使われなかった理由は、これらの問題が顕在化していなかったためと、JSでわざわざマルチスレッドを持ち込んでまで物事に向き合いたくない、といったあれそれがあったことでしょう。単に使われなかったので枯れていなかったので使われない、という鶏と卵の問題もあったように思います。

自分が思うに、今では ES Modules と Dynamic Import の導入によって、「ファイル名を指定して起動する」ということへの概念的なジャンプが、やや少なくなったように思います。

// ESM: Dynamic Import
const sub = await import('./sub.js')

// WebWorker
const worker = new Worker('./worker.js')

インラインで worker ロジックを記述できるようにしようぜ、という domenic (Promiseの仕様策定者)の tc39 への proposal もあります。

GitHub - domenic/proposal-blocks: A proposal for a new syntactic construct for serializable blocks of JavaScript code

こんな感じ

const result = await worker({|
  const res = await fetch("people.json");
  const json = await res.json();

  return json[2].firstName;
|});

(これはクロージャへの変数キャプチャのセマンティクスなどが問題になりそうですが…)

結果としてマルチスレッド化すると、マルチスレッドのデザインパターンを導入することもある、ということで、GoogleChromeLabs にはいくらか試験的なモジュールがあります。

github.com

github.com

Comlink はpostMessage のラッパーで、 Clooney はその上で アクターモデルを実装したものです。

UI スレッドでJS ロジックを書くということは、 document や window に触れないJavaScript を書く、ということで、 元は browser / node の抽象化手段だった Universal JavaScript の概念がまた別の役割を背負うようになるのではないでしょうか。

speakerdeck.com

個々のオフザメインスレッドへの向き合い方

Canvas

OffscreenCanvas という提案があります(Chromeで実装・有効化されています)。Canvas の所有権を Worker 側に移譲し、 Worker 側で DOM への操作を行います。

developer.mozilla.org

しれっと、所有権とか言いましたが、これは最近 WebWorker に導入された概念で、ArrayBuffer や ImageBitmap などのオブジェクトを、そのスレッドでの参照を手放すことを条件に別スレッドで使えるようにするインターフェースです。

developer.mozilla.org

(TypeScript に Rust の Borrowing がほしくなりますね…)

AMP

AMP でも Worker 側でJSを実行し、 amp-bind (ampの機能の一つでデータバインド機能) で実質的に任意なJSを実行できるようにしたい、という提案があります。

JS in AMP · Issue #13471 · ampproject/amphtml · GitHub

github.com

React

React v16 のアップデートでは内部がフルスクラッチで書き直され、内部的には非同期な rendering ができるようになっています。が、ユーザー側にはまだ開放されていません。

React v17 では Suspense と呼ばれる機能が導入され、Promise ベースの非同期読み込みや、重い rendering を別フレームに分割することが可能になります。

speakerdeck.com

ただ、これはブラウザ標準と協調して出てきた機能というより、自前でその機構を持とうとしているものに見えますね。WebWorker 使うというより、スレッド専有時間を分割するという発想です。

Vue

Vue の作者 Evan You の最近の記事では、内部の Mutation の適用には requestIdleCallback を使っていくという旨が書かれています。

medium.com

requestIdleCallback は、メインスレッドを専用しない限り裏に積まれたキューが実行される、優先度が低いコールバック登録です。

developer.mozilla.org

これもReactと同じく内部タスクのスレッド専有時間の分割です。

まとめ

話題になっている、といいつつも、実際はChorme周辺から、どうやったらWeb体験を良くできるか?ということから生まれた概念で、そのために WebWorker を使えないか試行錯誤している、という段階です。FMP, FCP, TTIといったメトリクスを改善するために、UIスレッド以外で処理を進めるしかない、といった経緯ですね。

末端の JSプログラマとしては、今後JSのマルチスレッド化が進む潮流が生まれようとしている、という理解をするといいと思います。Flux みたいなメッセージングベースの設計や、node 側から出てきた Universal JavaScript もおそらく有用なんじゃないでしょうか。