◆ Blob のデータは Blob を作ったページではなくブラウザ全体部分のメモリに保持される
◆ Blob から Blob を作るとコピーされない

Blob

TypedArray (Uint8Array など) と ArrayBuffer は実体が共有されています
どれかを書き換えると全部の値が変わります

const u8 = new Uint8Array(4)
const buf = u8.buffer
const u16 = new Uint16Array(buf)

console.log(u8, u16)
u16[0] = 5000
console.log(u8, u16)
Uint8Array(4) [0, 0, 0, 0]
Uint16Array(2) [0, 0]
Uint8Array(4) [136, 19, 0, 0]
Uint16Array(2) [5000, 0]

コピーされないのでメモリ使用量は増えません

ですが Blob はコピーです

const u8 = new Uint8Array(4)
const blob1 = new Blob([u8])
u8[0] = 1
const blob2 = new Blob([u8])
u8[0] = 2
console.log(new Uint8Array(await blob1.arrayBuffer()))
console.log(new Uint8Array(await blob2.arrayBuffer()))
Uint8Array(4) [0, 0, 0, 0]
Uint8Array(4) [1, 0, 0, 0]

Uint8Array を更新しても Blob を作成した時点の値になってます
ということはムダにいっぱい作るとメモリ使用量が増えます
特に大きめなデータの場合は気軽にコピーしないほうが良さそうです

ただ 中身が同じなら内部的に最適化されて二重に確保されないとかあるのでしょうか
devtools でメモリ使用量を見てみることにしました

メモリ使用量

同じデータ

試すためのページを作りました
50MiB の Uint8Array を用意して ボタンが押されたら 5 つのグローバル変数に Blob を作ります
中身が空のままだとメモリ上では圧縮して 「0 が 50MiB 分続いてる」 みたいに保持されてるかもしれないので一応ランダムで埋めておきます
ランダムの作成は長さ制限があったのでループして埋めてます

<!doctype html>

<button id="button">button</button>

<script type="module">
const wait = async (ms) => {
return new Promise(r => setTimeout(r, ms))
}

const u8 = new Uint8Array(1024 * 1024 * 50)

for (let i = 0; i < u8.length; i += 1024) {
const random = crypto.getRandomValues(new Uint8Array(1024))
u8.set(random, i)
}

const fn = async () => {
globalThis.value1 = new Blob([u8])
globalThis.value2 = new Blob([u8])
globalThis.value3 = new Blob([u8])
globalThis.value4 = new Blob([u8])
globalThis.value5 = new Blob([u8])
}

button.onclick = fn
</script>

devtools の機能を使って ページを開いた状態で GC を呼び出してからメモリのスナップショットを取ります
ボタンを押してからもう一度スナップショットを取って 比較します
今回の場合は大きめの Blob にしてるので個別の使用量は見なくても全体の使用量を見るだけでも十分です

どっちも 55MB くらいでほぼ変わりませんでした
ということはいい感じに最適化されてそうです

違うデータ

では最初の 1 バイトを変えてみます

<!doctype html>

<button id="button">button</button>

<script type="module">
const wait = async (ms) => {
return new Promise(r => setTimeout(r, ms))
}

const u8 = new Uint8Array(1024 * 1024 * 50)

for (let i = 0; i < u8.length; i += 1024) {
const random = crypto.getRandomValues(new Uint8Array(1024))
u8.set(random, i)
}

const fn = async () => {
u8[0] = 0
globalThis.value1 = new Blob([u8])
u8[0]++
globalThis.value2 = new Blob([u8])
u8[0]++
globalThis.value3 = new Blob([u8])
u8[0]++
globalThis.value4 = new Blob([u8])
u8[0]++
globalThis.value5 = new Blob([u8])
}

button.onclick = fn
</script>

上の方で書いたようにこれだと全部別の Blob データになるはずです

しかし 前後でどちらも 55MB ほどで変わりません
どういうことなのでしょうか……
最初の 1 バイトだけの違いですし いい感じに差分保持までしてるのでしょうか?

最初の 1 バイトだけではなく全体を書き換えてみます

<!doctype html>

<button id="button">button</button>

<script type="module">
const wait = async (ms) => {
return new Promise(r => setTimeout(r, ms))
}

const u8 = new Uint8Array(1024 * 1024 * 50)

const fillRandom = (u8) => {
for (let i = 0; i < u8.length; i += 1024) {
const random = crypto.getRandomValues(new Uint8Array(1024))
u8.set(random, i)
}
}

fillRandom(u8)

const fn = async () => {
globalThis.value1 = new Blob([u8])
fillRandom(u8)
globalThis.value2 = new Blob([u8])
fillRandom(u8)
globalThis.value3 = new Blob([u8])
fillRandom(u8)
globalThis.value4 = new Blob([u8])
fillRandom(u8)
globalThis.value5 = new Blob([u8])
}

button.onclick = fn
</script>

これなら変わるでしょ と思っていたのにこれでも変わっていません
いったい Chrome はどんな超技術を使ってるんでしょうか……

以前 文字列の repeat みたいなところで計測したときは 作るのは高速なのに初回アクセスが遅くて 内部的に最初に使うタイミングで実行してそうなことがありました
ここでもそんな遅延評価がされているのでしょうか

一応一部をコンソールに出力してみます

const read = (blob) => blob.arrayBuffer().then(buf => new Uint8Array(buf).slice(0, 10))

await read(value1)
// Uint8Array(10) [236, 212, 31, 195, 77, 176, 197, 180, 120, 42]

await read(value2)
// Uint8Array(10) [93, 244, 22, 163, 82, 229, 145, 211, 31, 179]

await read(value3)
// Uint8Array(10) [3, 228, 217, 221, 251, 227, 50, 157, 90, 32]

await read(value4)
// Uint8Array(10) [160, 100, 161, 123, 51, 5, 91, 113, 122, 210]

await read(value5)
// Uint8Array(10) [136, 28, 62, 63, 17, 239, 47, 206, 9, 113]

ちゃんとランダムです
その後確認しても同じメモリ使用量です
すぐ GC されたのか読み取り用の Uint8Array 分も増えてないようでした

ページ外メモリ

あと考えられるのは devtools で見れるヒープメモリの外に保持されてるくらいでしょうか
Chrome のタスクマネージャを見てみます

chrome-memory-blob

増えてました
devtools で見れるメモリの外ですが ページ内ではなくページ外のブラウザ全体のメモリ領域を使ってるようです

182M → 439M に増えています
だいたい 250M (50M x 5) くらいの増加なのでちょうどですね

ページの方も 70M ほど増えていますが こっちは GC するともとに戻ったので一時的なもののようです

こういう持ち方をしてるのは予想外でした

Chrome のタスクマネージャを見て なぜか「ブラウザ」でメモリが多く使われてるとなったら どこかのページが大きめな Blob を使ってるのかもしれませんね

最適化

「ブラウザ」で確保されていたメモリは 5 つ分でした
全部データが別になるようしていたからですが 同じならどうなるのでしょうか
fn をこう変更してみました

const fn = async () => {
globalThis.value1 = new Blob([u8])
globalThis.value2 = new Blob([u8])
globalThis.value3 = new Blob([u8])
globalThis.value4 = new Blob([u8])
globalThis.value5 = new Blob([u8])
}

それでも変わらず 5 つ分増えています
最適化は行われないようです

と思っていたら blob を渡した場合は 1 つ分しか増えませんでした

const fn = async () => {
globalThis.value1 = new Blob([u8])
globalThis.value2 = new Blob([globalThis.value1])
globalThis.value3 = new Blob([globalThis.value2])
globalThis.value4 = new Blob([globalThis.value3])
globalThis.value5 = new Blob([globalThis.value4])
}

Uint8Array から Blob を作ると以前作ったものと同じであっても新規にコピーが作られるようです
Blob から Blob を作ると再利用されて新規にコピーは作られないようです

File は Blob を継承しているので 混ざっても同じ動作です

const fn = async () => {
globalThis.value1 = new Blob([u8])
globalThis.value2 = new Blob([globalThis.value1])
globalThis.value3 = new File([globalThis.value2], "name3")
globalThis.value4 = new File([globalThis.value3], "name4")
globalThis.value5 = new Blob([globalThis.value4])
}

また繰り返しの場合でもコピーされないようです

const fn = async () => {
globalThis.value1 = new Blob([u8])
globalThis.value2 = new Blob([globalThis.value1])
globalThis.value3 = new Blob([globalThis.value1, globalThis.value2])
}

value3 は 100MiB の長さですが value1 の Blob ひとつ分しかメモリ使用量が増えていません
繰り返しで大きなファイルになる場合はこうしたほうがメモリは節約できるようですね