-
@ ocknamo
2024-12-02 16:46:10
# nostterに画像最適化機能つけたよ(サーバー編)
この記事は[Nostr Advent Calendar 2024](https://adventar.org/calendars/10004) の3日目の記事です。
あまり技術的な話はないので暇つぶし程度に読んでみてください。
## どうしてこうなった?
そもそも認証の改善以前にNostrあんまり人が増えてないのが一番課題だよねと思ったのが始まりでした。
Nostrに人が増えないのはオーガニック検索流入が少ないからに違いないと特に根拠もなく思ったのでNostrのまとめサイト(Toggeter)のようなものを作ろうと考えました。
ところでNostrのまとめを行うアプリ自体は鎌倉さんという方が[ノスリ](https://nosli.vercel.app/)というのを作成されていました。ブログ形式のイベントにTwitterライクな短文投稿をまとめるというものでまさしくToggeterですが、そこまで活用されていません。
そこでその仕組みをありがたくそのまま転用してまとめ記事を何らかの方法で量産しつつ、一つのドメインのサイトにまとめてSEOもなんかうまいことやれば検索流入増えるんじゃねという目論みです。
ところでそのまとめサイトは静的サイトジェネレータで(SSG:Static Site Generator)でつくりたいなと思いました。いや普通に考えてSSRにしてサーバ側でNostrからデータとってきたらSEO的にもよくねって感じなんですが、SSRでサーバーでウェブソケット呼ぶのだる…レイテンシを考慮すると結構不利じゃないかなと思ったわけです。うそです。そこまで考えてなくてほとんど個人的な好みです。(多分ちゃんとキャッシュきかせればそんなにレイテンシ問題にならなさそうな気がする)。ただキャッシュとかも考慮すると結構複雑な気はする。
ということで今年の6月頃にNostrのブログ記事を[SSGするサイト](https://nostr-ssg-blog-template.pages.dev/)をためしに作りました。
去年のアドベントカレンダー記事とか乗っけてます。
全然画像最適化の話になりませんね。もうちょっとです。
SSGのブログといえば[Gatsby](https://www.gatsbyjs.com/docs/glossary/static-site-generator/)(今は下火かも)とかで作成した経験があります。Gatsbyでは[画像用のプラグイン](https://www.gatsbyjs.com/docs/how-to/images-and-media/using-gatsby-plugin-image/)が公式に用意されていて生成されたブログに表示される画像はもとの画像データから変換され、表示時のサイズに最適化されたサイズにリサイズすることができます。
それだけではなくサイトの初期表示時には画像の画質を落としてぼやけた画像を表示してその後元画像が読み込まれたらきれいな画像を表示する、みたいなことができます。インラインでデータを持っているのでHTMLの表示と同時に画像の初期表示ができたりします。(その頃はよくわかってなかったがNostrの[NIP-92](https://github.com/nostr-jp/nips-ja/blob/main/92.md)で定義されているblurhashとかを使っていたんでしょう多分)
とにかくSSGは初期表示の速さが良い。というのが自分の頭の中にあるわけです。SSGのブログをNostrで作るとなると当然同じことがやりたい。
しかし、Nostrの画像はただのURLです。(オプショナルで画像のメタデータを定義できる[imeta](https://github.com/nostr-jp/nips-ja/blob/main/92.md)タグはあるが…)
基本的にSSGの画像最適化というのはローカルに存在する画像データをビルド時に変換するという流れで行われます。もしくはリモートURLに画像が置かれる場合は画像ストレージサービスやCDN上で配信されるときに最適化されることになります。
参考: [Svelteの画像ベストプラクティス](https://kit.svelte.jp/docs/images#loading-images-dynamically-from-a-cdn)
Nostrの画像はただの外部のURLですから(2回目)、ローカルの画像を変換する方法は採用できないわけです。無理やり実行するならビルド時にURLからすべての画像を一旦ダウンロードして変換するとかも考えましたが、ただの外部URLというのはつまりは一体どういうファイルなのかダウンロードしてみるまでわからない、要するに全然信頼できないということですから、よくわからないURLをかたっぱしからダウンロードするようなことは避けたいわけですね。(極端な話100GBの画像URLとかつくって攻撃されたら破綻しますよね)
ここが結構他の中央集権的なSNSと違って辛いところなのかなと思います。X(Twitter)とかであれば画像ストレージサーバは一つなので表示される画像の形式も一つに限定されるのですが、画像のアップロードが外部頼みでただのURLなので(3回目)何が来るかわからないということですね。
サーバ側で画像形式などを制限できない(imetaも信頼できるかわからない)となるとクライアント側でなんとか対応する必要があり、Nostrはクライアント側が大変なプロトコルだなあとということに思いを馳せていましたが、いろいろ考えたり調べた結果として作るべきものは画像最適化のためのプロキシサーバであるということがわかりました。
プロキシサーバというのはここでどういう働きをするのかというと、もとの画像がおいてあるサーバとクライアントのあいだに入って、もとの画像を一旦ダウンロードして圧縮したり縮小したりしてそのあとクライアントに送ってあげるという働きをします。
> プロキシがないとき
`[画像置き場] -> [クライアント]`
> プロキシがあるとき
`[画像置き場] -> [プロキシ] -> [クライアント]`
利点としてはクライアントが実際にダウンロードするのはサイズの小さな画像になるので通信量や画像の表示スピードは改善します。が、一方でサーバを経由する分通信にかかる時間(レイテンシ)が長くなります。というわけなので、当初の目的である表示速度の改善を実現するには、画像をエッジキャッシュするなどの対応が必須となります。平たく言うと一度送った画像はプロキシで保存しておいて次に要求されたらそれをすぐ返すということです。
画像をどれくらいのサイズにするか、画質はどうするかなどはプロキシに指示してあげれば良いのでクライアント側は想定通りの画像を受け取れてハッピーということになります。嘘です。ある程度はハッピーですが、プロキシ自体がエラーを返す可能性もあるし、処理できなくてもとの画像をそのまま返す場合もあります。そんなあれこれの結果、クライアントはフォールバック処理を実装する必要がありました。(それはまた別の記事で)
一方で自分はNostrのモバイルクライアントのヘビーユーザですので、Nostrの通信量の多さも解決したい課題でした。Nostrやってるとすぐギガがなくなります。
通信量の多さのボトルネックはウェブソケットで送られるNostrのイベントの通信量というよりは投稿に添付される画像のダウンロードの方ではないかという話もTLで耳にしたため(特に裏もとらず)、いっちょ画像最適化のプロキシサーバを作ってクライアントに実装してプルリクエスト投げるかと考えました。
そこで私はまずクライアントをAmethystからnostterに乗り換えることから始めました。なぜならばAmethystよりもTypeScriptとSvelteで書かれているnostterのほうがコントリビュートしやすかったという私的な事情です。
## 解決したい課題
遠回りしてきましたが課題はこれ。
- Nostrの画像の表示改善
- Nostrの通信量の改善
## インターフェイス(設計)
画像最適化のプロキシは実装よりもインターフェースを考えるのが大変です。ここはとても苦労しそうな気がしたので、**何も考えず** cloudflare Imagesのインターフェースをそのまま借用することにしました。
https://developers.cloudflare.com/images/transform-images/transform-via-url/
また何かあってもcloudflare Imageに課金して乗り換えることが可能という利点もあります。
## 実装
とにかく時間がないので手グセでかけるTypeScriptかJS。かつ課金しないで可用性を維持できる無料枠のサーバレス環境が使いたかったので [cloudflare workers](https://www.cloudflare.com/ja-jp/developer-platform/products/workers/) に目星をつけて調査したところ、[非常に素晴らしい記事](https://zenn.dev/sora_kumo/articles/wasm-image-optimization)を見つけましたのでこのwasmのライブラリをTSから呼び出して使わせていただくことにしました。
自分の実装したところはURLのパースと、キャッシュAPIの呼び出しを行って、最後にこのライブラリを叩くだけです。
ほかはクライアントキャッシュ用のヘッダーを書き換えたりとか細かいことはやっていますが全く大したことはやっておらず、サーバは非常にリーズナブルにやりたいことを実装した形になりました。
大変助かりました。
実装したものがこちらです。
https://github.com/ocknamo/nostr-image-optimizer
## 実装後の課題
このプロキシただのパブリックAPIなのでDoS攻撃に弱いです。何回もAPIをたたかれたら無料枠をすぐに超えてしまうことが容易に想像できました。
無料枠超えても使えなくなるだけで破産はしないので安心ですが、しかし対策は考える必要があります。
### WAF
WAFです。APIに対するリクエスト回数などに対する制限をIPごとにかけています。また攻撃された場合検知できれば特定のIPや地域をブラックリストに入れることも可能です。
細かいことを説明してギリギリを攻めて攻撃されても困るので細かく説明しませんが、当初は全力でDoS攻撃されても無料枠超えないくらいの感じで制限をかけていました。
しかしNostrのTLをちょっと早めに表示するとすぐにWAFに引っかかってエラーになるという状態になることもあり、結局アイコンの表示は画像最適化の対象にしない方針にするなどの影響がありました。
いろいろあって、前提が変わり、もう少し制限をゆるくしたのでアイコンの対応は今後は検討できるかもしれません。
## まとめ
割と手抜きで作ったことがわかるかと思います。実装しててNostrへの貢献ではあるはずですが、Nostrのプロトコル(NIPsとか)全然関係ないことやってるなと思いました。
フロントについては来週書きます。
次回のアドベントカレンダーの記事は eyemono.moe さんによる[”クライアント自作を通して得られた知見まとめ”](https://adventar.org/calendars/10004)ですね!楽しみです!