こんにちは、kamyknです。
今回はWebAssemblyの力を試してみたかったので、履歴、ブックマーク、タブ検索ができるChrome Extensionを作ってみました。今回は記事中コードはほぼ出てきませんが、開発の際に得たWebAssemblyの知見などをメインに書いていってみたいと思います。
なお、今回作成したChrome Extensionの特徴は下記のような感じです。
技術的な概要
- 検索ロジック部分はfzf風に作っていてWebAssemblyによるものです。
- (まだ自分用の段階ですが、WebAssemblyをnpm package化したりしてます)
- 処理はWebWorkerで別スレッドに逃がしています
- 表示部分などの実装はJS、特にVue.jsによるものです
- 検索対象のデータはすべてChromeのAPIで提供されるデータを利用します
これらについては記事内で順に説明していきたいと思います。
作ったもの (Lightning)
Lightning (History, bookmark and tab search)
Lightningっていう名前の由来ですが、検索したいときにパッと出してゴリゴリ検索してスッと消えてなくなるイメージが、なんとなく個人的には稲光のイメージだったので⚡️⚡️⚡️
既存の同等のJSの履歴検索Chrome Extensionを置き換えたい心意気です。
↓GIFアニメーションによるデモ(ちなみにキーボードだけでも操作が完結します)
検索ロジック部分につかったWebAssemblyについて
WebAssemblyとはざっくりいうと、ブラウザで実行するアセンブリでJavaScriptより高速に実行できることを目指したものです。
現在はC, C++, Rust, GoなどがWebAssemblyにコンパイルできます。
今回は、RustでWebAssembly部分を作成しました。
Rustを選択した理由ですが、ライフタイムという概念でガベージコレクション無しでメモリ安全に書けて、かつ速いということで興味があったのと、WebAssembly対応が早かったので比較的情報があったことなどが理由になります。(あと、Goは僕が学び始めた時期(2018年5月頃)はまだWebAssemblyに対応してなかった気がします。)
実装はfzf風あいまい検索
検索ロジックですが、個人的にfzfが好きなこともあって、今回はfzf風なあいまい検索で履歴を検索できることを目指しました。
→ fzf: https://github.com/junegunn/fzf
→ fzfの使い方記事: fzfを活用してTerminalの作業効率を高める
fzfのざっくりとした説明をすると、入力した文字とイコールで一致しなくても、使われている文字が一致してたら結果に表示してくれるあいまい検索です(と僕はfzfをそう認識している)。処理の中で一致度をスコアリングして、一番スコアが高いものを上に出してくれます。
(例えばQiitaなら入力が『qt』でもマッチしてくれます。)
今回は独自の実装としてJSからオブジェクトを渡して、いくつか持っているvalueの中から複合的に検索するようになっています。例えば今回の実装では『URL』と『ページタイトル』(とブックマークは『ブックマーク名』)を1つのオブジェクトにまとめて渡し、それらのvalueを1つずつ検索してくれるようにしています。たとえば、こんな感じです↓↓
{
_protocol: 'http',
url: 'example.com/abc',
title: 'サンプルのページ',
}
(ちなみにアンダースコア(_)から始まるものは検索対象にしないようにしており、結果で同じオブジェクトを受け取れるので、結果側で使いたいだけのデータを入れるようにしています。)
検索ワードをスペースで分けると、keyがまたがっていてもマッチするようにしています。
例えば、マッチ対応表にすると下記のような感じです(マッチするものに○)
検索ワード | URL: qiita.com | title: rustのページ | 補足 |
---|---|---|---|
rust | × | ○ | |
qiita | ○ | × | |
qt | ○ | × | |
qiitarust | × | × | (URLがqiita.com/rustならマッチする) |
qt rust | ○ | ○ |
WebAssemblyはマルチスレッディングはまだできない
さて、WebAssemblyをRustで書くとして、今回のような検索ロジックを高速化したいときにまず思い浮かぶのが並列処理ですが、マルチスレッディングはWebAssembly側がまだ対応していません。(入る噂もあるんですが、今は対応してないです。)
Goで書こうがRustで書こうが残念ながら…というのが現状です。
というわけで、今回はシングルスレッドで検索しています。
WebAssemblyの実行速度について
RustでWebAssemblyで書けばめちゃくちゃ速くなるはず…と思いたいところですが、期待し過ぎは禁物かもしれません。というのもそもそもJavaScrpiptは優秀ですので、スクリプト言語としては処理速度は速い部類です。
大体Web上のJSとWebAssemblyの比較記事を見てみても、処理速度が数十パーセントほど早くなったという記事が多いかなというのと、自分が今回のChrome Extensionとは別で比較した処理もだいたいそんな感じ…というのが自分の所感です。なので、処理の最適化がされていないWebAssemblyと、処理の最適化をしたJSでは処理速度は十分に逆転し得ると思います。
今回の検索ロジックでは検索結果を一部キャッシュして使い回すような処理を入れたり、入力中かつ検索処理中は続く処理をpendingして、最後の文字の入力を判定して最後のみ実行するなど実行頻度を調節するような処理をRxJSっぽく書いてたりなどの配慮はしました。
WebAssemblyはnpmで配れる!
WebAssemblyはnodejs/npmのエコシステムに乗れます!
が、Webpackなどのモジュールバンドルを通してもWebAssemblyのファイル(.wasm)は独立した1つのファイルのままだったりするので注意してください。
つまり、2つWebAssemblyのファイルを使っていたら2つのファイルのままなのですが、今はまだそこまでWebAssemblyが増えすぎるような状況は少ないと思いますので…現状はまぁ、いいかという感じ。
↓ 今回のChrome Extension開発のある時点でのdistの様子
(npmで配れる!ということで余裕が出てきたら、次は今回の検索ロジックのWebssemblyのnpm package化の記事でも書こうかなと思っています。)
ちなみに、初めてnpmパッケージ化しながらWebAssemblyでHello World!する感じのチュートリアルは、素晴らしい記事をざっくり翻訳させていただいた記事を以前書きましたので、こちらももしよかったら見てみてください。
https://qiita.com/kamykn/items/371cba5487d3c7cea8aa
WebAssemblyはどんなときに使えば良い?
まだまだDOM操作などの領域はJSのライブラリに敵わないので、私としては計算量が多くなる処理をWebAssemblyで置き換えるというのがおすすめです。今回のChrome Extensionも表示部分の処理はVue.js、検索のロジックは計算量が多くなるのでWebAssemblyという感じです。ガツッと処理したいところをWebAssemblyで置き換えていくのがWebAssembly導入の第一歩だと思っています。
Web Worker
さて、今度はWeb Workerですが今回はWebAssemblyの処理をメインとは別スレッドで実行するために利用しています。WebAssemblyでマルチスレッディングはできませんが、JS側でWeb Workerを使ってスレッドを別で立てた上で実行することはできるのです。
さてスレッドを分ける理由なのですが、一番の理由は実は処理高速化のためではありません。メインスレッドでWebAssemblyでゴリゴリ検索すると、↓こちらの記事でも書かれているように画面が一瞬固まったようになります。
Comlink + Rust で言語とスレッドの垣根を越えた WebAssembly 開発
スレッドの専有が発生してその間操作など受け付けないようです。CSS3によるアニメーションもカックカクです。というわけでWeb Workerを使って処理を別スレッドに移して実行してあげることによってこの問題を回避することができます。
なので、**『重い処理でWebAssemblyするならばWeb Workerもセットでね』**という感じです。
ちなみに、私もこの記事の通りに実装してまして、Web Workerでつかうworker-pluginを使っているのですが、この記事をかかれた方(@3846masa さん)のworker-pluginをwasm対応するPRがつい先日マージされたようです🎉🎉🎉
https://github.com/GoogleChromeLabs/worker-plugin/pull/15
表示部分などの実装(Vue.js)
表示部分はVue.jsを使っています。Vue.jsは単体導入でもかなり使い勝手が良いのでいいですよね。さすがはProgressive Web Frameworkです。ちなみにChrome ExtensionでもVue.jsは安定して使えました。
今回Vue.jsを使いたかった理由ですが
- テンプレートとロジックの分離
- Observableなデータバインディング
- キーボードのショートカット用のイベント管理
- 小規模開発なのでシンプルなライブラリ
あたりが理由です。
(実務でSPAしてたりするので、使い慣れているというのもあります😆)
今回はChrome Extensionということでほぼ1ページしかないので、Vuexやvue-routerも使いませんでした。が、1ページしかないからといって今回に限ってはコンポーネント切り出しをサボったため、一つのJSファイルにまとまってます😇
(メソッドとかは処理単位で切ってあるのでそのうちコンポーネントに切り出したい…😅)
なお、WebAssemblyはコード上では普通のJSのオブジェクトのインスタンスのような感じで使えるので、JS側のライブラリはこのライブラリじゃないとダメってことはないです。どのライブラリでもWebAssemblyを組み合わせて使うことができます。
ちなみにRustでwasm-bindgenを使った場合はこんなイメージです
const wasm = import("./crate/pkg/wasm.js")
wasm.search()
※ jsファイルは自動生成されます。
検索対象のデータはすべてChromeのAPIから
一応、Chrome Extensionの紹介記事なので、最後にこのChrome Extensionがブラウザから利用するデータについても記載しておきます。
今回は、上記の履歴APIで提供されているような閲覧履歴の他に、ブックマーク、開いているタブをブラウザのAPIから取得し、『タイトル』と『URL』(と『ブックマーク名』)で複合的に検索をします。なので、新しくデータを別の所に溜め込んだりしませんし、Chrome Extensionを入れた日より前の履歴を検索することももちろんできます。
JSベースの履歴を検索するChrome Extensionだと、速度優先のために件数を絞ったりしているのか、上手くマッチしないことがある気が…ということで、今回のChrome Extension開発では極力たくさんの履歴を検索対象にしたかったので、履歴に関しては直近10000件(ただし1年以内)をとってきて対象としています。
ChromeのAPIドキュメント:
https://developer.chrome.com/extensions/history
https://developer.chrome.com/extensions/bookmarks
https://developer.chrome.com/extensions/tabs
まとめ
今回はWebAssemblyを使ってChrome Extensionを作ってみました(ちなみにデザインも頑張りました)。
現段階ではJSとWebAssemblyが共存して得意な分野をそれぞれにやらせてあげるのが自然かなと思います。JSの豊富なライブラリ群がWebAssemblyに置き換わるような日はまだまだ遠いかなと個人的には感じました。
というわけでWebAssemblyはまだまだJSを置き換えるようなレベルではないのですが、ポイントに絞ってガッツリ処理するなど、使い方しだいでは実用レベルにまで来ていると思いました。
こういったWebAssemblyにしか解決できない部分もきっとあると思います。そんなときに引き出しとしてのWebAssemblyをいかがでしょうか?