検索や推薦をWebWorkerでやる
スライドにした
WebWorkerをproductionで使ってる話
Cosenseのこのへんの機能は
検索
リンク記法の補完
QuickSearch
さすがにページ内テキストの全文検索はサーバーでやる
関連ページリストの推薦
こういう実装になっている
serverは大きめのJSONをドカッと返す
clientのWebWorkerで下処理して辞書を作って
Reactのレンダリングのタイミングで最終計算する
理由
世の中の進歩
スマホでもCPUが4つ以上ついているのが当たり前の時代になった
serverのCPU増やすと金がかかるけど、clientのCPUは無料で増える
回線やcacheが良くなってきている
ファイルサイズの多少の差よりもTCPの接続・往復時間の方が気になる
Scrapbox固有の事情
ドカッと返したJSONに、完全一致する文字列重複が多い
gzipが効いて、わりとファイルサイズが減る
サーバーのCPUを使いたくない
CPUを専有するリクエストがあると、他の人のSocket.IOが詰まってしまう
Reactのrenderで1回あたり100msec固まっちゃう、なんて事は避けたい
テキストエディタなので、キー入力毎にrenderする
IME切ってキー押しっぱなしにしても、なめらかに表示されてほしい
shouldComponentUpdate等も当然使うが、そこではなく
ページ編集毎に検索・推薦用の辞書の再構築が必要な場合がある
他人が同projectの別ページを編集しても、自分の検索・推薦に即反映されてほしい
特にページ間リンクは重要なので、リンク記法の補完は全員の持つ辞書に即時反映させたい
こういう役割分担にした
関連ページリストの場合
server
何を表示するかを返す
[ {title: "タイトル1", links: ["リンク先1", "リンク先2", "リンク先3"] }, {title, links}, {title, links} ]
みたいな配列をドカッと返す
client
WebWorker
どう表示するかを計算する
どのページが何つながりで推薦されているのか、見出しを付ける
スコアを計算してソートする
同じページを何回も重複表示させない
等
UIスレッド
Ajaxでserverから「何を表示するか」を取得
WebWorkerにpostMessage
「どう表示するか」をstateに格納→Reactに渡す→Virtual DOM更新→DOM更新
UIスレッド
1 threadを使いまわす
setTimeout、requsetAnimationFrame
XHRなどの非同期関数
CPUを1つしか使えない
マルチCPU化したスマホの性能を捨ててる
CPU
通電しているだけでCPU100%時の25%ぐらい電力使うらしい(だっけ?)
最近詳解システムパフォーマンスで読んだ
WebWorker
UIスレッドとは別のCPUを複数使える
効能
他の人の編集が即サジェストに反映される機能
https://gyazo.com/d487581f31f748e594a067d758880ef6
projectに1万ページぐらいあって、その各ページに10個ぐらいリンクやhashtagがある場合
サジェスト用の辞書作成を
UIスレッドで、Reactを固めないようにこまめにsleepしながら実行した場合
5秒ぐらいかかる
サーバーで実行した場合
これは最初から試してない
「ページ作った」「リンク変更した」等の情報を差分でやりとりしている
サーバーはその差分情報の中継しかしていない
もしサーバーで計算したら人数分CPU食うのは明らか
WebWorkerで実行した場合
sleepなしでぶん回せるので、500msecもかからない
差分が来たら、毎回サジェスト用の辞書を全再構築している
初期化時と差分時で2種類書くのが面倒だった
計算量的にWebWorker使えば問題ないだろと最初から狙ってた
1project10万ページで速度が足りなくなっても、ここをチューニングすればまだまだ行けそう
という心の余裕として取っておく
関連ページリストの計算
https://gyazo.com/6dc396b96f14603770c7423dbf507810
projectに1万ページぐらいあって、その各ページに10個ぐらいリンクやhashtagがある場合
そこそこ重い計算になる
共通のリンク先を持つページをグループ化する
リンク数などでスコアを計算して並び替える
重複表示をしない
ページ内のリンク記法やhashtagが変更される度に再計算している
サーバーで実行した場合
最悪15秒ぐらいCPUをフルに専有してた
アルゴリズムが良くなかった。O(N^2)だった
でも改善しても2秒ぐらいかかる場合があった
それをUIスレッドで実行した場合
最悪、キー入力して2秒ぐらい固まってしまう
WebWorkerで実行
Reactのレンダリング時間分だけ固まる程度で済む
推薦される関連ページ数が多い場合はまだ重い
ここはforced synchronous layout問題が絡んでいて、今直している
WebWorkerとは
3つあるうちのDedicatedWorkerの事を、この記事では特にWebWorkerと呼びます
window.Worker
WebWorkerが動くブラウザ
だいたい動くのではないか
postMessageでやりとりする
callbackなので、返ってこないと困る
返ってこい
Workerの作り方
Web Worker を使用する - Web API インターフェイス | MDNに書かれてる
3種類ある
HTML内にscriptタグとして埋め込む
コードをbase64 encodeしてnew Worker("data:text/javascript:base64encodeされた文字列")する
普通にconst worker = new Worker('/path/to/worker.js')
これでやってるshokai.icon*3
Workerを使いまわす
事前に作っておく
タスクを投げる直前に作らない
new Worker(url)するとHTTPリクエストがサーバーに飛んで、(おそらく)そこでブロッキングする
Worker複数作る
並行実行したい処理の数だけnew Worker(url)する
それぞれ別のCPUで実行される
new Worker(url)
サーバーが301 not modifyを返せばcacheが使われる
app初期化時に必要な数だけnew Worker(url)すべし
初期レンダリングではWebWorker使わない
SEO、というか主にGoogle bot対策
サーバーサイドレンダリングしないReact SPAのSEO
読むだけならそんなに新しくないブラウザでも見れる
書くにはWebWorker動くブラウザ使ってね、という方針
module bundle
workerも、client jsと同様にBrowserifyで1つのjsにまとめる
1つのworkerに複数の機能を詰め込める
UIスレッドのclient jsとコードが共有できるshokai.icon
ファイルサイズに注意
DOMが無いので、browserify-shim等でCDNにライブラリを任せられない
UIスレッドとWebWorkerで同じ関数を共有できるようにしておく
日頃からなるべく純粋な関数で書くようにしている
UIスレッド・WebWorker・サーバーのどこでも動かせるように作っておく
様子を見て後で変えれる
エラートラッキング
worker.onerror
postMessageはcallbackなので絶対に返ってきてほしい
処理内容がスピードを求める物なだけにtry-catchすると遅い
悩む
try-catch捨てた
WebWorkerの中でもUIスレッドと同様にSentryが使える
workerのバグ見たら即直す
何度もWebWorkerに仕事させる
postMessageをPromiseにする
Promiseの多重実行を防ぐ
async-throttleを使う
#書きかけ