2019年11月2日 | ブログ記事

traQのmarkdownのパースをWeb Workerでやるようにした話【アドベントカレンダー2019 3日目】

sappi_red

この記事はtraPアドベントカレンダー2019の 3日目(11/2) の記事です。
19のsappi_redです。
今回は部内サービスに行った改善について話したいと思います。

遅い、遅い、遅すぎる

traQという部内SNS[1]ではメッセージ中でmarkdown記法を利用できます。

また、このサークルの性質上プログラムのソースコードが投稿されることが多くあります。GitHubのwebhookによって投稿されるメッセージもかなり多くあります。これらにはソースコードが含まれていることが多いです。

code

markdownのパースが重いチャンネルを開くとタブがフリーズして操作を受け付けなくなるということが発生していました。特にシンタックスハイライトがめちゃくそ重かったので、長いコードが投稿されているチャンネルが遅かったです。

off-the-main-thread

そこで、この問題を解決するためにメインスレッドではないところでmarkdownのパースをするようにしました(off-the-main-thread)。ブラウザでメインスレッドではないスレッドでJavaScriptを実行する方法としては、Worklet[2]などもありますが、今回はWeb Workerを利用しています。

また、タブがフリーズするのを改善する手段としては別スレッドで実行する以外にも方法があります。フリーズが発生する原因は実行するスクリプトの処理が分割されていないためです[3]。したがって、requestIdleCallbackrequestAnimationFramesetTimeoutなどで処理を細切れに分割することによりフリーズすることを回避できます。しかし、シンタックスハイライトはライブラリに依存していてそのようにするのは厳しかったため、今回は採用しませんでした。また、処理が長いならばスレッドを分けてしまったほうがよいということと、処理の強制終了がしやすいということもあります。

Web Worker化

Web Workerの関数はメインスレッドから直接呼び出すことはできず、Workerとメインスレッドの間の通信はpostMessage()に限られています。これでは、非常に使いにくいので今回はworkerize-loaderを利用することにしました。このWebpack loaderは、Workerの関数をメインスレッドから通常の非同期関数のように呼び出せるようにするものです。

WebWorker側にmarkdownをパースする関数があって、メインスレッドでそれを利用したい場合は以下のように利用できます。

import Md from 'workerize-loader!@/worker/markdown'

const md = new Md() // ここでWorkerが生成される

export const parseSplit = async text => {
  const res = await md.parse(text)
  return res.split('\n')
}

Vuexの同期

さて、markdownのパースをWorkerで行うことにしました。しかし、処理をWorkerで動かすだけではうまく行きません。traQのmarkdownでは、メンション機能だったりスタンプ機能だったりのために、メンバーやチャンネルやスタンプの情報がパースの際に必要になります。この情報はVuexで管理されています。しかし、WorkerはメインスレッドのJavaScriptからスコープが分離されているので直接Vuexのデータを参照できません。このため、Workerで利用するデータはWorkerで保持しておかなければなりません。
これを実現するために、Workerの生成時にデータのリクエストを行って、その後はVuexのstore.watch()で変更時に同期されるようにしました。

チャンネル切り替え

これで、タブがフリーズすることから解放されました。しかし、まだ問題があります。一度重いチャンネルを開いてしまうと、別のチャンネルに移動してもそのチャンネルのパースが終了しない限り、今見ているチャンネルのメッセージがパースされません。

これを解決するためにまず試してみたのは、パースのタスクの登録を一斉に行うのではなく、タスクの登録を一つに限ることで直列化した上で、AbortControllerを利用して終了したいタイミングを検知することです。しかし、これだと一つのタスクが長いときにそのタスクを待つ必要が生じました。これは、off-the-main-threadの章で話したようにタスクをさらに細かく分割することで改善することができますが保守性の面で行いませんでした[4]

さて、タスクを無理やり終了させたい。Worker自体を終了させればいいのでは?ここでWorker.terminate()の出番です。チャンネル切り替え時に使っていたWorkerをメインスレッドからWorker.terminate()を利用して処理中だとしても問答無用で終了させ、別のWorkerを利用するようにしました。これによって、チャンネル切り替え時もスムーズなパースができるようになりました。

結果

WebWorker化前
WebWorker化後

上が改善前、下が改善後です。
このようにだいぶもたつきが改善されました。

参照

これが実際のプルリクエストです。
https://github.com/traPtitech/traQ-UI/pull/965
https://github.com/traPtitech/traQ-UI/pull/1102


  1. https://trap.jp/post/309/#1traq ↩︎

  2. Workletの一種のPaint WorkletについてtraP SysAd techbookに記事を書いています。よろしければぜひ。 ↩︎

  3. 参考: JavaScript のスレッド並列実行環境 1. レンダリングエンジンと JavaScript の実行モデル - nhiroki's weblog ↩︎

  4. 自動で細切れにするConcurrent.Threadという大昔の黒魔術ライブラリがあったりする(今でも動くかわからない) ↩︎

この記事を書いた人
sappi_red

Javascriptよく触ってます

この記事をシェア

このエントリーをはてなブックマークに追加

関連する記事

2019年11月21日
DEATH STRANDING【AdC2019 22日目】
Amanogawa
2019年11月20日
AAっぽい動画を作る【Advent Calendar 2019 21日目】
hosshii
2019年11月20日
キラッとプリ☆チャンつくってみた!
d_etteiu8383
2019年11月19日
Advent Calendar 2019 20日目
Adwaver_4157
2019年11月18日
C++スタイルのキャスト演算子を使おう!【アドベントカレンダー2019 19日目】
kegra
2019年11月17日
据え置きの音をお外に持ち出そう
liquid1224

活動の紹介

カテゴリ

タグ