😽
ブラウザ内でバイナリを圧縮してコードやlocalStorageに埋め込む
JS で wasm のダウンロードや TypedArry を通じた操作をやってると、コード内や localStorage にバイナリを埋め込みたいときがあります。
考え方
- JS の内部エンコーディングは UTF16 と決められているので、UTF16で表現可能な範囲を1文字として、バイナリをインライン化すればサイズが小さくて済むはず
- Chrome は CompressionStearm でブラウザ内で deflate できるので、あれば圧縮する
- https://chromestatus.com/feature/5855937971617792
- Chrome ではない場合、deflate 処理は飛ばしてそのまま。localStorage の読み書きなら途中でブラウザ自体のサポート増える/消えるなどしない限り一貫性は取れる
- 今回はやってないが、インラインJSに埋め込む場合、50kb を超えたあたりで pako を dynamic import して使ってフォールバックしてもよい
やってみた
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const compress_supported = !!globalThis.DecompressionStream;
async function deflate(str: string): Promise<ArrayBuffer> {
const buf = encoder.encode(str);
if (compress_supported) {
const stream = new Response(buf).body!.pipeThrough(new CompressionStream("deflate"));
const buf2 = await new Response(stream as any).arrayBuffer();
return buf2;
} else {
return buf.buffer;
}
}
async function inflate(buffer: ArrayBuffer): Promise<string> {
if (compress_supported) {
const decompressedStream = new Blob([buffer]).stream().pipeThrough(new DecompressionStream("deflate"));
const buf = await new Response(decompressedStream as any).arrayBuffer();
return decoder.decode(buf);
} else {
return decoder.decode(buffer);
}
}
const encode = async (raw: string) => {
const buf = await deflate(raw);
const u8 = new Uint8Array(buf.byteLength % 2 ? buf.byteLength + 1 : buf.byteLength);
u8.set(new Uint8Array(buf));
const u16 = new Uint16Array(u8.buffer);
const u16str = Array.from(u16).map(s => String.fromCharCode(s))));
return u16str.join('');
));
const decode = async (encoded: string) => {
const u16c = Array.from(encoded).map((c) => c.charCodeAt(0));
const u8c = new Uint8Array(new Uint16Array(u16c).buffer);
const last = u8c.at(-1);
try {
return await inflate(
last === 0 ? u8c.slice(0,-1) : u8c
);
} catch (err) {
return await inflate(u8c);
}
}
使う
// usage
const input = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
`;
(async () => {
console.time('all');
console.time('encode');
const encoded = await encode(input);
console.timeEnd('encode');
console.time('decode');
const decoded = await decode(encoded);
console.timeEnd('decode');
console.timeEnd('all');
})();
結果
encode 結果は 1340byte => 143byte。143byte と小さいのは、lolem ipsum を3回コピペしてるので、同じ部分が符号化されて deflate が効いているのがわかります。
encode された内容はこんな感じ。
鱸凭燋봈誻聗ꭧ湈ꚾ∀ັ鈳▐룰ꃼ剬⹃፻�鮏긎㺐⊽텫⦁ꨆ坬洤鎓鈴攀㋩뒓렻㑈X쾋ᦪ뗆닇⒴뉙茷ਛٽ�栎ꕆ⍻酐펇྆㜃膩⪍銌ᗪ霏ꚉ蛓䞋⌒洓勰⚨逽郗奌㽌튐ᡣ⅌蚼㴦Ќ涕學郆ꇡപﮃ饮ᡇ辏ᚧ䇯⇇鰧⑤ꄯ搮䭨㨐⢍汼罸뻆豢㐌猔믉뉤ᆵ郞쥐囜⮊₩幍ⴺ탟靽萤鏌ⵖً肭뾘窹⻝�빹糳裏�ퟦ嫯
URLに使うのは安全ではありませんが、JS内で取り回す文字列としては問題ないです。
ただ、青空文庫から引っこ抜いてきたデータを突っ込んでみたら、charCode が 65536 を超えて動きませんでした。バイナリなら問題ないですが日本語はもうちょっと工夫が必要そうです。codePoint を使うとなんとかなるかも。
Discussion
よく分からなかったのですが、任意のバイナリに対応しているわけではない、ということでしょうか?
書くのサボったんですが、コードポイントが u16 を超えてると、UInt16Array への変換処理で溢れて壊れました
これは・・・
Uint8Array
⇔Uint16Array
のところがエンディアン依存・・・ですよね?今回の用途は
localStorage
とのことなので、同一環境での enc/dec だと存じますが、encode された文字列を異なる環境間で同等に扱うには、上位/下位 bit の扱いを統一する等の改変が必要そうですね。
(Little Endian 方式に統一すれば、
slice(0, -1)
等の条件分岐は不要になる?かも・・・)