🥴

爆速のアイコン検索サイトを作った

2022/07/14に公開

爆速のアイコン検索サイトを作ったので、遊んでみてください。

(1) まずは自分が使いやすいアイコン検索サイトを作りたかったので作りました。(2) 様々なアイコンを爆速で横断検索し、サクッとアイコンをコピーできるようにしました。単純ながら意外とその部分が面倒なサイトは多い気がします。(3) また応用とメンテがしやすい実装にして、非常にサポート範囲の広い DB を GitHub Pages 上で構築しました。公開時点では 120+ のアイコンセットと、130,000+ のアイコンをサポートしており、サポート数も観測範囲では No.1 です。

https://marmooo.github.io/icon-search/

開発動機

アプリ/サイト開発ではまずお世話になるであろうアイコン。私はこれまで Material Icons と Bootstrap Icons ばかり使っていました。これは検索が面倒だったからです。検索が面倒だと知名度の高いものだけに閉じてしまうので、良くありません。世の中には素晴らしいアイコンがもっとあるはずなので、フリーのアイコンを (ほぼ) すべてまとめて比較したいと思いました。きちんと比較するために、知名度による表示補正は一切行わないようなサイトにもしています。これはデザイナーの方にも良い効果があると思っています。シンプルなアイコンの良さとライセンスだけで比較するほうが、裾野が広がります。

作っている途中で色々と代替案も知ったのですが、同時に自作の必要も感じました。数千行くらいでサックリ作れてメンテも軽めなので、車輪の再発明でもまあ良いかと思いながら作りました。これまで何となく作ってきたライブラリの ttf2svggitn も活用できたので、苦労なく完成しました。他のサイトと比較した利点としては、(1) 真面目にデータ管理しているので、ノイズの少ない検索ができます。あと (2) アーキテクチャ的にたぶん高速に動作します。以降は 2 のアーキテクチャとその高速性について、詳しくまとめていきます。

アーキテクチャ

まず、長期の運用を考えるとバックエンドにコストは掛けたくありません。無料で運用できるアーキテクチャを模索しました。

フロントエンド DB で運用コストゼロ

最近は面白い技術が色々出てきて、フロントエンドに DB を置く技術が出てきています。典型例は 2019年に登場した sql.js とその応用である sql.js-httpvfs です。2022年に大半のブラウザで使えるようになったので、少し前に検証がてら 8つほどアプリを作りました。アイコン検索を作るときにも、最初は同じような実装で実現することを考えていたのですが、2つの問題に気付きました。

(1) まずは、週1回〜月1回くらいの DB 更新です。sql.js-httpvfs は SQLite の DB を 10MB 単位のブロックに分割します。ブロックの中身はバイナリデータで、クエリに対して最適化する必要があるので、差分は取りにくい構造になっています。そのため、DB を更新するとほとんどすべてのブロックが更新されてしまう問題があります。GitHub Pages や Netlify で管理するとすぐに破綻しそうです。Amazon S3 などに DB を置くならまったく問題ありませんが、お金が掛かります。

(2) sql.js-httpvfs には自動圧縮の機能はないので、サーバの gzip 圧縮機能などに影響を受けます。たとえば GitHub Pages に DB を置く想定では、バイナリデータに gzip 圧縮は効きません。そのため転送量が無駄に大きくなってしまう問題があります。同じような条件で動作するサービスは結構多いので、SVG アイコンのようなややサイズの大きなテキストの扱いには弱いのです。今回の開発では、これらの問題を解決する DB を考えることからスタートしました。

Query-oriented JSON DB

考えた結果、クエリに対応するファイル名で JSON を保存し、クライアントサイドではそのリストに対して検索することにしました。最高に雑な DB ですが、私は Query-oriented JSON DB と呼んでいて、昔からたまに使っています。昔から成長したのは Git で管理できるようにしたことです。これは JSON ファイルの中身をこまめに改行して TAB 区切りにするだけです。TAB 区切りなら minify しなくても十分にコンパクトですし、GitHub で十分に DB を管理できます。

この方式はクエリのパターンが 100万とかになると inode 枯渇問題で死ぬ危険性が出てくるので、基本的に真似しないほうが良いですが、クエリパターンが限定的なら活用できるアイディアです。アイコンに付与されているタグ (英単語) はせいぜい 2万パターンしかありません。またアイコンは 1つの単語を検索するだけで十分に絞り込めるので、1単語1ファイルで保存できます。余裕でファイルシステム上に管理できます。sql.js-httpvfs は軽すぎるクエリを バイナリデータとしてまとめるのに最適ですが、Query-oriented JSON DB は数の多い結果をまとめて、diff 管理するのに最適です。

ネットワーク上ではファイル名に対してアクセスするので、O(1) で検索できて効率も良いです。ちなみにファイルシステムは B+ Tree なので、本当は O(1) ではないのですが、その計算を GitHub に任せられるので、擬似的に O(1) になるということです。sql.js-httpvfs ではネットワーク上の検索で O(log N / log m) (m=leaf nodes) のコストが掛かるので大きな改善です。今回のような小さなアプリケーションでは、なんちゃって最速動作するように思えます。

サイト公開時点の JSON ファイルは圧縮前でもほとんど 1MB 以内に収まることがわかっています。ほとんどのサービス上では JSON に gzip 圧縮が掛かるので、実際には 300KB くらいの通信量しか発生しません。つまり最強です。

最速の追求

フロントエンドに DB を置く、というかクエリに対応する JSON をばら撒けば、アイコンのロードは最速にできるとわかりました。ここまで来たら徹底的にやろうと思い、GUI も最速を追求しました。最近の技術を使えば、ただ JSON をロードするだけ以上のことができます。

ReadableStream

特に頑張ったのはデータの fetch 部分です。まずは SVG を一つずつロードすると大量のコネクションが発生して最高に迷惑なので、一つの JSON に大量の SVG を閉じ込めて一気に表示することで高速化しています。アイコンはなるべく一度に全部眺めて比較したいのですが、JSON ファイルが 1MB にもなると低速回線ではアイコンのロードに時間が掛かりそうでした。そこで ReadableStream で逐次レンダリングしてみたところ、いい感じになりました。

Streaming Pagination

とはいえ SVG を 3000個くらい読み込むと、スマホや開発 PC のメモリ (2GB、4GB) が爆発しました。そこでストリームから Pagination を生成しながら、SVG をレンダリングするように改良しました。SVG はレンダリングするとメモリをたくさん食うので、SVG の元データはメモリに String で保存し、Pagination で展開しながら表示するのがポイントそうです。Readable Stream + Pagination は最高に疾くて、なかなかか良い感じです。

XML Parser in Worker

Pagingination を付けて大半のページは不満がなくなりました。しかし一度に 300こくらいアイコンを表示すると、整形処理が重いときに多少時間が掛かりました。この整形処理がまず何をやっているかというと、SVG の ID 重複排除です。SVG を JSON で管理するとコネクション数が減ってハッピーなのですが、SVG の ID が重複する問題があります。最初は JSON から SVG を一斉にロードして、DOMParser でXMLパースした後に、ID を固有化していました。ただ DOMParser は同期処理、その後の整形も DOM 処理なので同期処理で、重いです。そこで Web Worker 内で SVG 文字列をパースして整形し、整形後の String をメインスレッドで SVG にレンダリングするようにしました。レンダリングと DOM 処理が重いので、その回数を減らして非同期化すれば、かなり早くなります。この改善により、体感的な表示速度は爆速になりました。

Accept-Ranges Streaming

1つの JSON ファイルのデータ数やファイルサイズが大きくなり過ぎても問題はありません。現在は、3MB 以上の JSON ファイルの fetch では Accept-Ranges を利用し、1MB を上限にした範囲リクエストを発行しています。そして Pagination で逐次ロードをすることで通信量も大きく抑えています。ただし欠点もあります。Accept-Ranges fetch を使えば理屈上はすべてのファイルが効率的に処理できるのですが、現実はサーバの実装が追いついておらず、gzip 圧縮が利かなくなってしまいます。そこでファイルサイズが 3MB以上 (gzip 圧縮時に約 1MB 以上) の JSON ファイルをリストアップして、そのファイルだけ逐次ロードするようにしています。この方式ならアイコンの数が 100万になっても対応できます。

すこし先端技術を攻めすぎてしまったので、さらなる高速化には技術の進化も必要そうです。Accept-Ranges fetch は 2022年に大半のブラウザでサポートされた機能ですが、この機能追加によってフロントエンド DB の価値が高まっているように感じます。今やフロントエンドだけで、かなりの事はできますし、また今後にも期待できそうです。

まとめ

アイコン検索は何も考えなければ難しい技術は必要なく作ることのできるテーマですが、極めようとすると技術的にも割と面白いところがありました。公開前から自分に不満のないレベルまでは作り込んだので、気持ち良く利用できると思います。遊んでみてください。

https://marmooo.github.io/icon-search/

Discussion