ただJSONサイズを80%削減する話
JSONファイルのサイズを 80%削減 できたお話。
実際に実験した数値も踏まえて紹介します。
モチベーション
HowToだけを知りたい方は読み飛ばしてもOKです。
モチベは次の2つです。
- 通信量を削減したい!
- 難読化したい!
1. 通信量を削減したい!
個人開発しているサイトでNetlifyを使っていました。
NetlifyにはFreeのプランがありますが、通信量の制限があります。
個人開発で潤沢にポケットマネーがあるわけではないので、できるだけ通信量のケアをしたい!
2. 難読化したい!
サーバーからブラウザへJSONを配信すると、JSONの中身が見れてしまいます。
苦労してデータ作ってるので、JSONの中身見られてパクられるのがちょっとなぁ...(※)と思ってました。
nginxのようなミドルウェア層でgzipやBrotli圧縮しても良いのですが、ブラウザのディベロッパーツールで見れちゃったり、よく知られた圧縮方式なのでデコードもしやすいです。
独自の方式で難読化したい!
(※2025/01/25)追記
主にスクレイピングの文脈です。例えば curl
コマンド等でパブリックに配信している静的なJSONにアクセスされるケースです。
スクレイピング目的のアクセスを完璧に弾くのは難しいため、保険程度の難読化です。
(アクセス突破してDOM解析すれば終わりじゃん、と言われればまぁそれはそう、という感じです笑。そんなにシビアにはやってません)
あくまでも個人開発&機密情報というデータでもない&通信量削減がメインなので、本記事は「セキュリティ知識総動員してゴリゴリ難読化したい」というわけではないのでそこはご了承ください。
いざ圧縮!
次の2つのステップでJSONを圧縮しました。
- JSONそのものを圧縮
- JSON文字列を圧縮
1. JSONそのものを圧縮
JSONを圧縮するNode.jsのライブラリはいくつかありますが、実験では compressed-json を使わせていただきました。
個人開発しているサイトでいくつか圧縮したいJSONをピックアップします。
手作り感マックスのスプレッドシートで恐縮ですが、「K」と書いてあるのはキロバイトのことです。
JSONは16個、合計804kBのファイルで試してみると次のような結果になりました。
圧縮後は合計で464kBとなり、大体 60%ほど圧縮 できました。
2. JSON文字列を圧縮
JSONそのものを圧縮して終わりません。
最終的にはJSONを文字列化して静的ファイルに落とし込みますが、その 文字列化 の部分にも圧縮の余地があります。
JSON文字列を圧縮するためのライブラリに JSONCrush があるので、これを使ってさらに圧縮をかけてみます。
JSONCrushによる追加の圧縮後は合計ファイルサイズは160kBとなりました。元々のファイルの合計サイズが804kBだったので、 80%ほどのファイルサイズの削減 となりました。
めでたしめでたし!
(2025/01/22追記)
コード例も貼っておきます。
import fs from 'node:fs/promises';
import { compress } from 'compressed-json';
import JSONCrush from 'jsoncrush';
function heavyCompressJsonToString(value) {
return JSONCrush.crush(JSON.stringify(compress(value)));
}
export async function writeDataToLocalFile(exportDir, data) {
const t0 = performance.now();
await fs.writeFile(exportDir, heavyCompressJsonToString(data));
console.log(`Write ${performance.now() - t0} ms`);
}
めでたしめでたし...?
結果だけ見るとめでたしなのですが、いくつか注意事項があるので書いておきます。
圧縮にめちゃくちゃ時間かかる
圧縮したファイルを復元するのにはそんなに気にはならないのですが、 圧縮の方はめちゃくちゃ時間かかります 。
ファイルによるので一概に言えないですが、私のサイトでの経験だと 1ファイル圧縮するのに10分以上かかることもあります(※) 。
圧縮時間の内訳ですが、ほぼ「1. JSONそのものを圧縮」に時間かかります。
なお、この後お話する compress-json にライブラリを置き換えても同様です。
fetch
のような非同期IOとは違い、このJSONの圧縮処理は同期的な処理なので、圧縮が始まると他の処理が止まります。
マルチスレッドで処理するなどの一工夫が必要です。
また、ユーザーからリクエストを受け取ってから動的にJSONを生成して返却するケースも難しいでしょう。私のサイトのようにアプリケーションビルド時などあらかじめ静的にJSONを生成するようなケースで有効です。
(※2025/01/22追記)
Cloud Buildのデフォルトの machineType
で実行した結果なので、かかる時間は実行環境によります。その他詳細な前提はこちらにコメントしてます。
JSON圧縮には別ライブラリを使うのがおすすめ
使わせてもらってる身ですみません、という感じなのですが、今回の実験で使った compressed-json よりも代替のライブラリを使うことをおすすめします。
というのも、この記事の執筆時点ではあまりメンテされていなかったり、TypeScriptに対応していない、他に良いライブラリがある、といったのが理由です。
代替の候補としては compress-json があります。個人的な推奨理由としては次のようなものがあります。
- 記事執筆時点でも直近でメンテされていそう
- TypeScriptもサポート
- ベンチマークをみた限りはcompressed-jsonよりも圧縮率が良い
ただし、圧縮や解凍時間はcompressed-jsonに分がありそう(私は最初これを理由にcompressed-jsonを選んだ)なので、あくまで個人的な意見と捉えていただければと思います。
まとめ
compressed-json と JSONCrush でJSONのサイズを80%削減したぞ!
昔書いた記事ではありますが、もしパフォーマンスチューニングに興味があればこちらの記事も参考にしてみてください。
こちらもちょっと古い記事で恐縮ですが、私のサイトのアーキテクチャなどを紹介した記事があるのでご興味あればこちらもぜひ。
(一部現在のアーキテクチャとは違う部分はあるのであしからず)
ここまでご覧いただきありがとうございました!
Discussion
async functionやPromiseの中で実行をすればいい、という単純な話ではない感じですかね?
前提として
そのため、今回のような CPU バウンドな処理は Promise ベースの提供になっていないことが殆どで、そういった処理は明示的にプロセスやスレッドを起こさないと複数の CPU コアを利用した真の並列化は実現できません。
この記事ではスクレイピングの文脈とのことで、少なくともNode.jsではデフォルトで(必要に応じて)マルチスレッドが使用されるはずと思っています👀
なんとなくわかりやすそうなページ↓
マルチスレッドはOSによって複数のCPUコアが使われる場合がある認識ですが、ここが間違っているのでしょうか?
僕が引用した内容は「JSON圧縮が同期処理である」というもので、多重化・並行並列処理に関しては言及していないつもりです🙋♂️
またPromiseで行う処理が同期処理であるか非同期処理か、については非同期処理である、と考えているのですが、そこが間違っていますか?
話題に上げたらありがたいことにアドバイスをいただいたので、一応こちらにも上げておきます🙆♂️
もし興味がある方は、リプライツリーを見ていただけると。
少々語弊があったみたいですね,すみません。
こちらは語弊があったので無視してください。これが正しいかどうかは内包する処理に依存する,というのが正しい説明でした。
以下,仕切り直して説明します。具体例を挙げて,各ケースにおける「並行性」と「並列性」について説明します。
JSON.stringify
で大きなペイロードを文字列化することを想定してください。A.
JSON.stringify
をラップした JavaScript の同期関数コード例
説明
B.
JSON.stringify
をラップした JavaScript の Promise 関数コード例
説明
concurrentStringify
の実行に入ると全ての処理が終わるまでこの関数を抜けることはない。C.
JSON.stringify
をchild_process
上で実行するようにラップした Node.js の Promise 関数コード例
stringify-worker.js
:説明
child_process
を使用することで別のプロセスで処理され,CPUのコアを活用した並列実行が可能D.
fs.readFile
をラップした Node.js の Promise 関数コード例
説明
fs.readFile
はlibuv
のスレッドプールを利用するため,複数の IO 操作を並列で処理可能もとの話に戻りますが,A から B のように書き換えたところで, 「同期処理として提供されているものを
Promise
でラップしただけでは何も解決しないよ」 が私の一番いいたいところでした。Promise
でラップするだけで OK というのは, D のように元から並列性が実現されているものを取り扱う場合のみであり,そうなっていない処理の場合は基本的には C のような対応が必要ということです。child_process
など(他にも選択肢あり)を使用します。WebWorker
を使用します。単純に気になったのですが、10秒じゃなく10分ですか…?ファイルがすごく大きいのでしょうか?
あー、すみません、10分なのはCloud Buildのデフォルトの
machineType
で実行した結果です!ので実行する環境によってはもっと速くなるかと思います〜ちなみに10分くらいかかってるファイルのサイズは大体100kB~200kBです。
なお、JSONで記述されている各オブジェクトは、オブジェクトや配列がネストされたような複雑なものでもなく、valueが文字列や数値の単純な形式のオブジェクトです。
簡易的ですがコード例も貼っておきます。