- 技術 💻
Blobとは|JavaScriptのBlob URL・DBのBLOB型の違いと使い方
結論
Blobとは「中身の形式を決めつけない、ひとかたまりのバイナリデータ」のことです。JavaScript の Blob オブジェクト、blob: で始まる Blob URL、データベースの BLOB 型、Azure Blob Storage は、どれも「画像でも PDF でも何でも、バイトの列として丸ごと扱う」という同じ考え方です。
この記事でわかること
- Blob の4つの意味(JavaScript・Blob URL・DB の型・クラウド)の使い分け
- Chromium で動かした new Blob → createObjectURL → 画像表示・ダウンロード・解放の実物と、解放し忘れで 455MB 増えた実測
- SQLite・PostgreSQL(bytea)・MariaDB の BLOB 型に画像を入れて取り出した実物と、「ファイルは DB に入れるか」の判断表
「blob」は英語で「かたまり・しずく」を指す言葉で、IT では Binary Large OBject(大きなバイナリの対象物)の略としても使われます。どの文脈でも共通するのは、「中身が何かは問わず、バイトの列をひとかたまりとして保存・受け渡しする」という役割です。JavaScript では、その塊に blob: で始まる仮の URL を付けて、画像の表示やダウンロードに使います。
flowchart TD
S["文字列・ArrayBuffer・File<br>fetch の応答・canvas"] --> B["Blob<br>size と type を持つ塊"]
B --> T["text / arrayBuffer<br>中身を読む"]
B --> U["URL.createObjectURL<br>blob: の仮の URL"]
U --> I["img.src・a[download]"]
I --> R["URL.revokeObjectURL<br>使い終わったら解放"]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| blobとは | 形式を決めつけないバイナリの塊。JS・URL・DB・クラウドで同じ考え方 | Blobとは(意味の使い分け) |
| JavaScript の Blob とは | new Blob([中身], {type}) で作る、size と type を持つ読み取り専用の塊 |
JavaScriptのBlobとは |
| blob url とは | URL.createObjectURL(blob) が返す blob:http://…/UUID。ページを閉じると無効 |
Blob URLの寿命 |
| Blob と File の違い | File は Blob に name と lastModified を足したもの | BlobとFile・ArrayBuffer・Base64の違い |
| Blob を Base64 にする | FileReader.readAsDataURL。大きさは 1.33 倍になる |
BlobとBase64の大きさ・速さ |
| revokeObjectURL は必要か | 必要。4MB×100 個で 455MB 増え、解放すると 56MB まで戻った | 解放しないとメモリが増える |
| blob 型とは(DB) | MySQL は BLOB 4種、PostgreSQL は bytea、SQLite は BLOB | データベースのBLOB型 |
| 画像を DB に入れてよいか | 小さく・少なく・整合性が要るなら可。大きく・多いならストレージ | ファイルはDBに入れるか |
| Azure の Blob とは | Azure のオブジェクトストレージ。ブロック・追加・ページの3種 | Azure Blob Storageとは |
確認環境:Ubuntu 24.04.5 LTS の上で、JavaScript の Blob・File・Blob URL・FileReader・fetch・FormData・IndexedDB・Web Worker・canvas の挙動は、Chromium 153.0.8010.12(Playwright 1.63.0、ヘッドレス)で 127.0.0.1 に置いた自分のページ(Python 3.13.18 の http.server)を開いて確かめました。メモリの実測は、Chromium の全プロセスの RSS(/proc の VmRSS の合計)の差で測っています。Node.js 22.23.4 の Blob も実行しました。データベースは、SQLite 3.53.1(Python の sqlite3)、PostgreSQL 16.15(作業用ディレクトリに initdb して 127.0.0.1 の別ポートで起動)、MariaDB 10.11.14(Ubuntu のパッケージを展開して 127.0.0.1 の別ポートで起動。MySQL 互換)に同じ PNG(1,065 バイト)を入れて取り出し、cmp で元のファイルと一致することを確かめました。Blob・URL の仕様は MDN、BLOB 型の仕様は MySQL 8.4・PostgreSQL 16・SQLite の公式ドキュメント、Azure Blob Storage は Microsoft Learn で確かめています。図は mermaid 11.16.0 で描画を確かめました(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Blobとは|JavaScriptのBlob URL・DBのBLOB型の違いと使い方」
Blobとは(意味の使い分け)
この節では、「Blob」という言葉が指す4つのものと、どの文脈でどれを指しているのかが分かります。
Blob は「Binary Large OBject」の略で、画像・音声・PDF・暗号化されたデータなど、中身の構造をデータベースやプログラムが解釈しない「バイトの塊」を指します。 文字列や数値のように「中身の形式が決まっているデータ」と対比される言葉です。同じ言葉が、次の4つの場面で使われます。
| 文脈 | 指しているもの | 見分け方 |
|---|---|---|
| JavaScript(ブラウザ・Node.js) | Blob オブジェクト。size と type を持つ読み取り専用のバイト列。File はその拡張 |
new Blob・response.blob()・files[0] が出てくる |
| Blob URL(オブジェクト URL) | blob:http://example.com/UUID の形の仮の URL。Blob を img や a タグから参照するためのもの |
URL が blob: で始まる。DevTools で見かける |
| データベースの型 | MySQL の BLOB・MEDIUMBLOB など、PostgreSQL の bytea、SQLite の BLOB、Oracle・SQL Server の BLOB/VARBINARY(MAX) | CREATE TABLE の列の型に出てくる |
| クラウドのストレージ | Azure Blob Storage(Microsoft のオブジェクトストレージ)。AWS の S3・Google Cloud Storage にあたる | 「コンテナー」「ストレージアカウント」と一緒に出てくる |
このほか、Git の内部ではファイルの中身を保存するオブジェクトを「blob」と呼び(git cat-file -t で blob と表示されます)、画像処理では「連結した画素の塊」を blob と呼びます。どれも「中身を解釈せずに塊として扱う」という意味で共通しています。
JavaScript の Blob
ブラウザの中で、ファイルのようなデータを持ち運ぶ入れ物。アップロード・ダウンロード・画像の表示に使う。
この記事の主役
Blob URL
Blob に付ける仮の URL。サーバーに送らずに、メモリの中のデータを img・a・iframe から参照できる。
createObjectURL
DB の BLOB 型
テーブルの列にバイナリをそのまま入れる型。文字コードや照合順序を持たない。
bytea・MEDIUMBLOB
Azure Blob Storage
ファイルを「BLOB」として URL 付きで保存するクラウドのサービス。S3 と同じ位置づけ。
公式ドキュメントの範囲
JavaScriptのBlobとは(new Blobの作り方とsize・type)
この節では、JavaScript の Blob がどういうオブジェクトで、どう作り、何が入っているのかが、実行した結果で分かります。
JavaScript の Blob は、「バイト列」と「MIME タイプ」の2つを持つ、変更できない(immutable)オブジェクトです。 MDN は「Blob インターフェイスは blob、すなわち不変の生データであるファイルのようなオブジェクトを表します」と説明しています。作り方は new Blob(部品の配列, { type: MIMEタイプ }) で、部品には文字列・ArrayBuffer・TypedArray・別の Blob を混ぜて渡せます。
const b = new Blob(["こんにちは, Blob"], { type: "text/plain" });
console.log(b.size, b.type, String(b));
console.log(await b.text());
const ab = await b.arrayBuffer();
console.log(ab.byteLength, [...new Uint8Array(ab).slice(0, 8)]);
// 部品は混ぜられる(Uint8Array・文字列・Blob)
const mixed = new Blob([new Uint8Array([0x41, 0x42]), "CD", new Blob(["EF"])]);
console.log(await mixed.text());
Chromium 153 で実行した結果です。
21 "text/plain" "[object Blob]"
"こんにちは, Blob"
21 [227, 129, 147, 227, 130, 147, 227, 129] ← e3 81 93 … = 「こ」「ん」の UTF-8
"ABCDEF"
「こんにちは, Blob」は 11 文字ですが、size は 21 です。size は文字数ではなくバイト数で、文字列は UTF-8 に変換されて入ります(ひらがな1文字=3バイト、ASCII 1文字=1バイト)。
| 書いたコード | 結果 | 読み取れること |
|---|---|---|
new Blob() |
size 0・type "" |
空の Blob が作れる |
new Blob([], {type:"Image/PNG"}) |
type "image/png" |
type は小文字に直される |
new Blob([], {type:"text/plain\n"}) |
type "" |
印字できない文字が入ると空になる(エラーにはならない) |
new Blob([{a:1}]) |
size 15 | オブジェクトは文字列化され [object Object] が入る。JSON は自分で stringify する |
b.slice(0, 15) |
「こんにちは」・type "" |
slice はバイト単位。type は引き継がれず、第3引数で指定する |
JSON.stringify(b) |
{} |
JSON には入らない。送るなら FormData か Base64 |
structuredClone(b).size |
21 | postMessage・IndexedDB にはそのまま渡せる |
b instanceof File |
false | Blob は File ではない(逆は true) |
new Blob("abc") のように配列で囲まずに渡すと、TypeError: Failed to construct 'Blob': The provided value cannot be converted to a sequence. になります。第1引数は必ず配列です。
Blob の型(type)は「自己申告」
type は、中身を検査して決まるものではなく、作った側が付けるラベルです。PNG のバイト列を text/plain で作ってもエラーにはなりません。fetch の response.blob() では応答の Content-Type が、<input type="file"> の File では拡張子から推定した値が入ります。中身を確かめたいときは、先頭のバイト(PNG なら 89 50 4E 47)を見ます。先頭バイトの読み方は、関連記事「バイナリエディタとは|使い方・おすすめとxxdで読む実例・戻し方」で扱っています。
BlobとFile・ArrayBuffer・Base64(data URL)の違い
この節では、バイナリを扱うときに一緒に出てくる File・ArrayBuffer・TypedArray・Base64 と Blob の関係と、相互の変換方法が分かります。
File は「名前と更新日時を持つ Blob」、ArrayBuffer は「中身を直接読み書きできる生のメモリ」、Base64 は「バイナリを文字だけで表した文字列」です。 Blob は中身を直接いじれない代わりに、巨大でも JS のヒープを圧迫せず、URL を付けたりストリームで読んだりできます。
| Blob / File | ArrayBuffer / Uint8Array | Base64(data URL) | |
|---|---|---|---|
| 何か | 読み取り専用のバイトの塊+type | 読み書きできる固定長のメモリ | バイナリを A〜Z a〜z 0〜9 + / の文字で表した文字列 |
| 中身の変更 | できない(slice で切り出す) | できる(u8[0] = 0xff) |
文字列なので作り直す |
| 大きさ | 元のバイト数 | 元のバイト数 | 1.33 倍(実測 1,048,576 → 1,398,141 文字) |
| img・a タグで使う | URL.createObjectURL(64 文字の URL) |
いったん Blob にする | そのまま src・href に書ける |
| JSON・localStorage に入る | 入らない({}・[object Blob] になる) |
入らない | 入る |
| 向いている場面 | ファイルの受け渡し・表示・ダウンロード | バイトの加工・パース・WebGL・暗号 | 小さな画像の埋め込み・JSON で送る |
相互の変換は次のとおりです(すべて Chromium 153 で実行)。
// Blob → ArrayBuffer / Uint8Array
const ab = await blob.arrayBuffer();
const u8 = await blob.bytes(); // Uint8Array を直接返す(新しめの API)
// ArrayBuffer → Blob
const blob2 = new Blob([u8], { type: "application/octet-stream" });
// Blob → Base64(data URL)
const dataUrl = await new Promise(ok => {
const fr = new FileReader();
fr.onload = () => ok(fr.result); // "data:text/plain;base64,SGk="
fr.readAsDataURL(blob);
});
// Base64(data URL)→ Blob:fetch に食わせるのが短い
const blob3 = await (await fetch(dataUrl)).blob();
// Blob → File(名前を付ける)
const file = new File([blob], "memo.txt", { type: blob.type, lastModified: Date.now() });
File のコンストラクターは第2引数のファイル名が必須で、省くと TypeError: Failed to construct 'File': 2 arguments required, but only 1 present. になります。<input type="file"> で選んだファイルは、次のような File として届きました。
{ name: "upload_sample.csv", size: 17, type: "text/csv",
lastModified: 1793358752883, isFile: true, isBlob: true,
text: "id,name\n1,田中\n", webkitRelativePath: "" }
type は拡張子 .csv から text/csv と推定され、lastModified はファイルの更新日時のミリ秒です。Base64 の仕組みと各言語での変換は、関連記事「Base64エンコード各言語コード一覧|sha256ハッシュのコマンド早見表」で扱っています。
画像を画面に出すとき
Base64 が向くのは、数 KB のアイコンを CSS や JSON に埋め込むときです
Blobの中身を読む(text・arrayBuffer・stream・FileReader)
この節では、Blob から文字列やバイト列を取り出す方法と、以前からある FileReader との使い分け、文字コードのつまずきが分かります。
今は await blob.text()・await blob.arrayBuffer()・await blob.bytes() の Promise 版が短く書けます。 FileReader はイベント(onload)で結果を受け取る以前からの API で、Base64 にしたいとき(readAsDataURL)と、文字コードを指定して読みたいとき(readAsText(blob, "Shift_JIS"))に今も使います。
| やりたいこと | Promise 版(新しい書き方) | FileReader 版 |
|---|---|---|
| 文字列として読む(UTF-8) | await blob.text() |
fr.readAsText(blob) |
| Shift_JIS など別の文字コードで読む | new TextDecoder("shift_jis").decode(await blob.arrayBuffer()) |
fr.readAsText(blob, "Shift_JIS") |
| バイト列として読む | await blob.arrayBuffer()・await blob.bytes() |
fr.readAsArrayBuffer(blob) |
| Base64(data URL)にする | (Uint8Array.prototype.toBase64 は Chromium 153 で使えた) |
fr.readAsDataURL(blob) |
| 大きなものを少しずつ読む | blob.stream().getReader() |
blob.slice() を順に読む |
Shift_JIS の「こんにちは」(10 バイト)を入れた Blob を、4通りで読んだ結果です。
blob.text() → "����ɂ���" (UTF-8 として読んで化けた)
fr.readAsText(blob) → "����ɂ���" (既定は UTF-8)
fr.readAsText(blob, "Shift_JIS") → "こんにちは"
new TextDecoder("shift_jis").decode(await blob.arrayBuffer()) → "こんにちは"
Windows で作った CSV を blob.text() で読んで化けるのは、この既定が UTF-8 だからです。 type に charset=shift_jis を付けても text() は UTF-8 で読みます(type は読み方に影響しません)。
readAsDataURL の結果は type によって変わりました。type: "text/plain" なら data:text/plain;base64,SGk=、type を付けないと data:application/octet-stream;base64,SGk= です。Blob の type が空だと、data URL の MIME タイプも「中身不明」になります。
Blob URL(createObjectURL)で画像を表示・ダウンロードする
この節では、Blob に仮の URL を付けて img で表示し、a タグでダウンロードさせる手順と、そのとき裏で何が起きているかが分かります。
Blob URL(オブジェクト URL)は、URL.createObjectURL(blob) が返す blob:http://127.0.0.1:46110/fc19362a-c5d3-4d49-90fa-187cfea396d9 のような文字列です。 「blob:」の後ろに作ったページのオリジン、その後ろに UUID が付き、長さは 64 文字でした。この URL を img・a・iframe・fetch に渡すと、ブラウザがメモリの中の Blob を読みに行きます。サーバーには何も送りません。
Blob を用意する
fetch の response.blob()、input[type=file] の File、canvas.toBlob、new Blob のどれでもよい。手元では fetch("/photo.png") の応答(image/png・1,065 バイト)を使った。
URL を作る
const url = URL.createObjectURL(blob)。呼ぶたびに別の URL ができる(同じ Blob でも)。
img.src か a.href に入れる
img なら img.src = url で表示される(naturalWidth 400・naturalHeight 300 で読めた)。ダウンロードなら a.href = url; a.download = "hello.txt"; a.click()。
使い終わったら解放する
URL.revokeObjectURL(url)。img の onload の中、ダウンロードなら click() の直後で呼ぶ。
// 画像の表示
const res = await fetch("/photo.png");
const blob = await res.blob(); // { size: 1065, type: "image/png" }
const url = URL.createObjectURL(blob); // blob:http://127.0.0.1:46110/fc19362a-…
img.onload = () => URL.revokeObjectURL(url); // 表示が終わったら解放
img.src = url;
// ダウンロード
function download(blob, filename) {
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
}
download(new Blob(["hello, blob\n"], { type: "text/plain" }), "hello.txt");
Chromium で実行した結果です。ダウンロードは Playwright の download イベントで受け取り、保存したファイルの中身まで確かめました。
createObjectURL → blob:http://127.0.0.1:46110/fc19362a-c5d3-4d49-90fa-187cfea396d9 (64 文字)
img.onload → naturalWidth 400, naturalHeight 300, complete true
fetch(url) → status 200, content-type image/png, 1065 bytes
download event → suggestedFilename "hello.txt", url blob:http://127.0.0.1:46110/4ddcc0cc-…
saved file → "hello, blob\n"
fetch(url) で Blob URL を読むと、ステータス 200 と Blob の type が Content-Type として返りました。Blob URL は「ブラウザの中だけで通用する、ミニ HTTP サーバーの URL」と考えると挙動が読めます。
canvas の画像を Blob にする
canvas の描画結果は canvas.toBlob(callback, type, quality) で Blob になります。200×100 の青い矩形を3つの形式で出した結果です。
toBlob() → 884 bytes, image/png
toBlob(cb, "image/jpeg", 0.8) → 897 bytes, image/jpeg
toBlob(cb, "image/webp", 0.8) → 610 bytes, image/webp
toDataURL() → 1,202 文字(PNG の Base64。Blob の 1.36 倍)
Blob URLの寿命(ページを閉じると無効・別オリジンから読めない)
この節では、Blob URL がいつまで使えて、どこから使えるのかが、実際に試した結果で分かります。
Blob URL は「作ったページ(document)が生きている間だけ」有効です。 MDN は「URL の寿命は、それを作成したウィンドウ内の document と結び付けられています」と書いています。ページを閉じる・再読み込みする・revokeObjectURL を呼ぶ、のどれかで無効になります。Chromium 153 で試した結果を表にしました。
| 試したこと | 結果 | 原文・表示 |
|---|---|---|
作ったページから fetch(url) |
読める | 200・image/png・1,065 バイト |
revokeObjectURL の後に fetch(url) |
読めない | TypeError: Failed to fetch・コンソールに net::ERR_FILE_NOT_FOUND |
| revoke の後、表示済みの img | 表示は残る | naturalWidth 400 のまま。読み込みが終わっていれば消えない |
| revoke の後、別の img に同じ URL | 読めない | onerror が呼ばれた |
| 同じブラウザの別タブで URL を開く(作ったページは開いたまま) | 開ける | 200・「from blob」が表示 |
| 作ったページを閉じてから別タブで開く | 開けない | net::ERR_FILE_NOT_FOUND。画面は「Your file couldn't be accessed」 |
| ページを再読み込みしてから元の URL を fetch | 読めない | TypeError: Failed to fetch |
別オリジン(localhost:46110)のページから fetch(url) |
読めない | TypeError: Failed to fetch |
別オリジンのページから location.href = url |
移動しない | コンソールに Not allowed to load local resource: blob:http://127.0.0.1:46110/fa89… |
同じオリジンのページから location.href = url |
移動する | 200・Blob の HTML が表示 |
| iframe の src に Blob URL(text/html) | 表示される | 中の location.origin は親と同じ http://127.0.0.1:46110 |
1保存して後で使えない
Blob URL を localStorage や DB に保存しても、次に開いたときには ERR_FILE_NOT_FOUND になる。保存するなら Blob そのもの(IndexedDB)か、サーバーの URL。
2オリジンが付いている
URL に作ったページのオリジンが含まれ、別オリジンからは読めない。127.0.0.1 と localhost も別オリジン扱いだった。
3revoke しても表示済みは消えない
img の読み込みが終わってから revoke しても画像は残った。onload の中で revoke するのが安全で、画像を差し替えるときに前の URL を解放する。
iframe に Blob URL を入れたときのオリジンは、Blob を作ったページのオリジンを引き継ぎました(MDN の説明どおりです)。text/html の Blob を iframe に入れると、中のスクリプトは親と同じオリジンで動きます。他人が作った HTML をそのまま Blob にして表示する使い方は避け、必要なら sandbox 属性を付けます。
revokeObjectURLしないとメモリが増える(実測)
この節では、Blob URL を解放し忘れたときに、どれだけメモリが増え、何をすれば戻るのかが、実測した数字で分かります。
4MB の Blob を 100 個作って createObjectURL し、JS の変数を捨てて GC しても、Chromium 全体のメモリは 455MB 増えたままでした。 revokeObjectURL を全部呼ぶと 56MB まで戻りました。URL が付いた Blob は、JS から参照されなくなっても GC の対象にならないためです(URL の文字列からいつでも読める必要があるので、ブラウザが保持し続けます)。
revoke せずにページを再読み込みすると、2〜20 秒後の測定で +117〜114MB まで下がりました。MDN の「ブラウザーは、文書がアンロードされた際にこれらのオブジェクト URL をメモリーから解放します」という記述のとおり、document が消えれば解放されます。ただし revoke のようにすぐ元の値まで戻るわけではなく、解放のタイミングはブラウザ任せになります。
測り方は、Chromium の全プロセス(ブラウザ・レンダラー・GPU など9プロセス)の VmRSS を /proc から合計し、起動直後との差を取りました。増えたのは主にブラウザプロセス(182MB → 599MB)で、Blob の実体はレンダラーではなくブラウザプロセス側に置かれていることも分かります。
単一ページアプリ(SPA)では、ページの再読み込みが起きないので、画像のプレビューを差し替えるたびに URL を作って捨てていると、使った分だけ増え続けます。 前の URL を revoke してから次を作る、という一行で防げます。
let currentUrl = null;
function showPreview(file) {
if (currentUrl) URL.revokeObjectURL(currentUrl); // 前のを解放
currentUrl = URL.createObjectURL(file);
preview.src = currentUrl;
}
BlobとBase64(data URL)の大きさ・速さの実測
この節では、同じデータを Blob URL と Base64(data URL)で扱ったときの大きさと、変換にかかる時間が分かります。
Base64 は 3 バイトを 4 文字で表すので、大きさは必ず 1.33 倍になります。 1MB と 10MB のバイナリで、Blob URL を作る時間・Base64 にする時間・Base64 から Blob に戻す時間を測りました(Chromium 153、各1回)。
| 測ったこと | 1MB(1,048,576 バイト) | 10MB(10,485,760 バイト) |
|---|---|---|
| Blob URL の長さ | 64 文字 | 64 文字 |
| data URL の長さ | 1,398,141 文字(1.333 倍) | 13,981,053 文字(1.333 倍) |
URL.createObjectURL |
0.40ms | 8.0ms |
FileReader.readAsDataURL |
7.9ms | 38.5ms |
Uint8Array.toBase64() |
1.3ms | 7.7ms |
data URL → fetch().blob() で戻す |
32.0ms | 274.8ms |
blob.arrayBuffer() |
1.9ms | 17.5ms |
blob.text() |
3.1ms | 122.5ms |
- Blob URL を作る時間は、データの大きさにほとんど比例しません。 10MB でも 8ms で、コピーではなく「参照に名前を付けている」だけだと分かります。
- Base64 は作るより戻すほうが遅く、10MB で 275ms かかりました。画面に出す目的で Base64 を経由する理由はありません。
blob.text()は UTF-8 のデコードが入るため、バイナリを文字列として読むと遅く、しかも壊れます。バイナリはarrayBuffer()かbytes()で読みます。
- 1画面に出す・ダウンロードさせる → Blob URLcreateObjectURL。使い終わったら revoke
- 2サーバーに送る → FormData に Blob/File をそのままmultipart/form-data。Base64 にしない
- 3バイトを加工する → arrayBuffer / bytesUint8Array にしてから new Blob で戻す
- 4ブラウザに保存する → IndexedDB に Blob のままFile を入れて取り出すと File のまま戻った
- 5JSON や CSS に埋め込む → Base64(data URL)数 KB まで。1.33 倍になる
fetchのresponse.blob()・FormData・Worker・Node.jsのBlob
この節では、Blob がブラウザの他の API とどうつながるか、Node.js ではどう使えるかが、実行した結果で分かります。
fetch の応答は response.blob() で Blob になり、type には応答の Content-Type が入ります。 本文は一度しか読めないので、blob() を2回呼ぶと TypeError: Failed to execute 'blob' on 'Response': body stream already read になります。404 でも本文(エラーページの HTML)が Blob になるので、response.ok を先に見ます。
fetch("/nothing") → ok false, status 404, blob: size 335, type text/html ← エラーページが Blob になる
fetch("/dot.png") → blob() 2回目: TypeError: Failed to execute 'blob' on 'Response': body stream already read
FormData に Blob を append すると、multipart/form-data の1パートになります。第3引数でファイル名を付けないと、ファイル名は blob、type の無い Blob は application/octet-stream で送られました。
Content-Disposition: form-data; name="f"; filename="x.csv"
Content-Type: text/csv
csv,data
Content-Disposition: form-data; name="g"; filename="blob"
Content-Type: application/octet-stream
noname
大きなファイルを分割して送るときは blob.slice(start, end) です。10MB+5 バイトを 4MB ずつ切ると、4,194,304・4,194,304・2,097,157 バイトの3つになりました。fetch と FormData の送り方は、関連記事「JavaScript fetchの使い方|用途別コピペ関数と実行結果」と「FormDataの使い方|fetchで送信・ファイルアップロードをコピペ解説」で扱っています。
| つなぎ先 | やったこと | 結果 |
|---|---|---|
| Web Worker | postMessage(blob) で渡す |
Worker 側で instanceof Blob が true、text() で読めた。Worker のコード自体も Blob URL で作れた |
| IndexedDB | File を put して get | コンストラクターは File、name は idb.txt、text は abc。Blob のまま保存できる |
| localStorage | setItem("b", blob) |
"[object Blob]" という文字列になる。入らない |
| JSON.stringify | Blob を渡す | {}。中身は消える |
| Node.js 22 | new Blob(["hello"," ","node"]) |
size 10・text "hello node"。URL.createObjectURL も使え、fetch(blobUrl) で 200 が返った。fs.openAsBlob(path) でファイルを Blob にできる |
Node.js の Blob はグローバルにあり(node:buffer の Blob と同じもの)、ブラウザと同じ text()・arrayBuffer()・slice() が使えます。ブラウザ向けのコードを Node.js でテストするときに、ファイルを Blob として渡すのに fs.openAsBlob が便利でした(type は空で返るので、必要なら new Blob([b], {type}) で付け直します)。
データベースのBLOB型(SQLite・PostgreSQL bytea・MySQL)に画像を入れる
この節では、データベースの BLOB 型がどういう型で、各 DB で何という名前なのか、同じ PNG を入れて取り出すとどうなるかが分かります。
データベースの BLOB 型は、文字コードも照合順序も持たない「バイトの列」をそのまま保存する列の型です。 MySQL の公式ドキュメントは、BLOB は「binary strings (byte strings)」として扱われ、比較と並べ替えはバイトの数値で行われる、TEXT は文字セットと照合順序で比較される、と区別しています。DB ごとの名前と上限を表にしました。
| DB | 型の名前 | 1つの値の上限(公式) | 手元で確かめた |
|---|---|---|---|
| MySQL/MariaDB | TINYBLOB・BLOB・MEDIUMBLOB・LONGBLOB(TEXT 4種と対応) | 255B・64KB・16MB・4GB。送れる大きさは別途 max_allowed_packet | MariaDB 10.11.14 |
| PostgreSQL | bytea(別に「ラージオブジェクト」もある) | 1フィールド 1GB(Limits の表) | PostgreSQL 16.15 |
| SQLite | BLOB(型の宣言はゆるく、入れた値ごとに型が決まる) | 既定 10 億バイト(SQLITE_MAX_LENGTH)、実装上の上限 2,147,483,645 | SQLite 3.53.1 |
| Oracle・SQL Server | SQL Server は varbinary(max)(Oracle は BLOB) | varbinary(max) は 2^31-1 バイト(約 2GB、Microsoft Learn) | 公式の範囲 |
SQLite:Python の bytes をそのまま入れる
import sqlite3
data = open("photo.png", "rb").read() # 1065 バイト
con = sqlite3.connect("t110db.sqlite")
con.execute("CREATE TABLE images (id INTEGER PRIMARY KEY, name TEXT, mime TEXT, data BLOB)")
con.execute("INSERT INTO images (name, mime, data) VALUES (?, ?, ?)", ("photo.png", "image/png", data))
con.commit()
row = con.execute("SELECT id, name, mime, typeof(data), length(data), substr(hex(data),1,16) FROM images").fetchone()
print(row)
blob = con.execute("SELECT data FROM images WHERE id=1").fetchone()[0]
print(type(blob).__name__, blob == data)
open("out_sqlite.png", "wb").write(blob)
(1, 'photo.png', 'image/png', 'blob', 1065, '89504E470D0A1A0A')
bytes True
$ cmp photo.png out_sqlite.png && echo identical
identical
SQLite は列の型宣言に関わらず入れた値の型で保存するので、同じ列に文字列 'abc' を入れると typeof(data) は text になりました。SQL で直接バイナリを書くときは X'89504E47' の16進リテラルで、これは blob になります。Python 側は bytes を渡せば BLOB、str を渡せば TEXT です。大きな BLOB を少しずつ読むには con.blobopen("images", "data", 1) があり、全体をメモリに載せずに先頭 8 バイトだけ読めました(89504e470d0a1a0a)。
PostgreSQL:bytea と \x の16進表記
CREATE TABLE images (id serial PRIMARY KEY, name text, mime text, data bytea);
-- psql で、ファイルを Base64 にして decode() で入れる
\set content `base64 -w0 photo.png`
INSERT INTO images (name, mime, data) VALUES ('photo.png', 'image/png', decode(:'content', 'base64'));
INSERT INTO images (name, mime, data) VALUES ('hex', 'application/octet-stream', '\x89504e47');
SELECT id, name, pg_typeof(data), length(data), substring(data from 1 for 8) AS head FROM images;
id | name | pg_typeof | length | head
----+-----------+-----------+--------+--------------------
1 | photo.png | bytea | 1065 | \x89504e470d0a1a0a
2 | hex | bytea | 4 | \x89504e47
postgres=# SHOW bytea_output; → hex
postgres=# SELECT pg_column_size(data) FROM images WHERE id=1; → 1069(長さ4バイト+本体)
$ psql -At -c "SELECT encode(data,'base64') FROM images WHERE id=1" | base64 -d > out_pg.png
$ cmp photo.png out_pg.png && echo identical
identical
PostgreSQL の bytea は、入出力が \x で始まる16進の文字列です(公式ドキュメント:hex 形式が既定で、以前の escape 形式より速く、新しいアプリケーションでは hex を使う)。形式が合わない文字列を入れると ERROR: invalid input syntax for type bytea になりました。アプリケーションからは、ドライバーが bytes(Python)や byte[](Java)をそのまま bytea にしてくれるので、16進の文字列を自分で組み立てなくて済みます。
bytea とは別に「ラージオブジェクト」(lo_from_bytea などで扱い、OID で参照する)もあります。bytea は行の中に入り 1GB まで、ラージオブジェクトは別テーブルに分割して置かれ、途中から読み書きできる、という違いです。
MariaDB(MySQL 互換):LOAD_FILE と max_allowed_packet
CREATE TABLE images (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), mime VARCHAR(50), data MEDIUMBLOB);
INSERT INTO images (name, mime, data) VALUES ('photo.png', 'image/png', LOAD_FILE('/path/photo.png'));
INSERT INTO images (name, mime, data) VALUES ('hex', 'application/octet-stream', X'89504E47');
SELECT id, name, mime, LENGTH(data) AS bytes, HEX(LEFT(data, 8)) AS head FROM images;
SELECT data INTO DUMPFILE '/path/out_mdb.png' FROM images WHERE id = 1;
id name mime bytes head
1 photo.png image/png 1065 89504E470D0A1A0A
2 hex application/octet-stream 4 89504E47
$ cmp photo.png out_mdb.png && echo identical
identical
-- BLOB と TEXT の比較の違い(BLOB はバイトで比較するので大文字小文字を区別する)
CREATE TABLE cmp (b BLOB, t TEXT); INSERT INTO cmp VALUES ('abc', 'abc');
SELECT b = 'ABC' AS blob_eq, t = 'ABC' AS text_eq FROM cmp; → blob_eq 0, text_eq 1
MySQL 系で BLOB を扱うときに最初に当たる壁が max_allowed_packet(既定 16MB)です。18MB の INSERT 文を送ると、クライアントには ERROR 2006 (HY000): Server has gone away、サーバーのエラーログには Aborted connection … (Got a packet bigger than 'max_allowed_packet' bytes) と出ました。サーバー側で SET GLOBAL max_allowed_packet = 67108864、クライアント側でも --max-allowed-packet=64M を指定すると、9MB の値が入りました(公式ドキュメントも「サーバーとクライアントの両方で変える」としています)。TINYBLOB(255 バイト)に 300 バイト入れると、strict モードでは ERROR 1406 (22001): Data too long for column 'd' at row 1 です。
BLOB 型が向くもの
数十 KB までの小さなもの(サムネイル・アイコン・署名画像)
行と一緒にトランザクションで消えてほしいもの
暗号化済みのデータ・鍵・証明書
件数が少なく、バックアップを DB 1つで済ませたいもの
ストレージ(S3・Azure Blob)が向くもの
数 MB 以上の画像・動画・PDF
件数が増え続けるもの(ユーザーの投稿)
CDN から直接配信したいもの
DB のバックアップ・レプリケーションを軽くしたいとき
ファイルはDBに入れるか(判断表と実測)
この節では、「画像を DB の BLOB 列に入れるか、ファイル/ストレージに置いてパスだけ DB に入れるか」を決める基準と、その根拠になる実測が分かります。
小さく・少なく・行と一緒に消えてほしいなら BLOB 列、大きく・増え続け・直接配信したいならストレージにパス(URL)だけ、が基本です。 50KB のデータ 2,000 件(約 100MB)を SQLite の BLOB 列と個別ファイルの両方に入れて測りました(tmpfs 上、Python 3.13、各1回)。
| 測ったこと | 個別ファイル | SQLite の BLOB 列 |
|---|---|---|
| 2,000 件の書き込み | 111ms | 493ms(1トランザクション・WAL) |
| ランダムに 5,000 回読む | 253ms | 207ms |
| 占める大きさ | 102.4MB | 106.5MB |
count(*)(BLOB 列を触らない) |
― | 2.7ms |
sum(length(data))(BLOB 列を全部なめる) |
― | 6.3ms |
| SQL のダンプ(バックアップ)の大きさ | ― | 204.9MB(16進の文字列になるので 2 倍) |
- 小さなファイルを「読む」速さは、BLOB 列のほうがわずかに速いか同じでした。SQLite の公式ページ「35% Faster Than The Filesystem」も、10KB 前後の小さな BLOB なら個別ファイルの fread/fwrite より約 35% 速く、領域も約 20% 少なくて済む一方、大きくなるとファイルシステムが有利になる、と説明しています。
- 一方で、SQL のダンプは2倍の大きさになり、BLOB 列を含む行を SELECT するたびに転送量が増えます。 一覧画面で
SELECT *を書くと、不要な画像まで毎回読むことになります。 - ストレージに置く場合は、DB には「パス(キー)・MIME タイプ・大きさ・ハッシュ」だけを入れ、ファイルの削除と行の削除がずれないように設計します。
- 11件の大きさは 100KB 未満か超えるならストレージ。MySQL なら max_allowed_packet にも当たる
- 2件数は増え続けないかユーザー投稿のように増えるものは、DB のバックアップ時間が伸び続ける
- 3行と一緒にトランザクションで消したいかはい、なら BLOB 列の利点が大きい。ストレージだと削除漏れの対策が要る
- 4ブラウザや CDN から直接配信したいかはい、ならストレージ。DB の BLOB はアプリを経由しないと配信できない
- 5一覧の SELECT で BLOB 列を除いているかBLOB 列を持つテーブルで SELECT * を書かない。列名を書く
アップロードされたファイルの保存先の設計と命名は、関連記事「ファイルアップロードの保存先設計|DBかストレージかの判断表と命名規則」で扱っています。
Azure Blob Storageとは(公式ドキュメントの範囲)
この節では、クラウドの文脈で出てくる「Blob」、Microsoft Azure の Blob Storage が何で、どういう構造と種類を持つかが分かります。
Azure Blob Storage は、Microsoft Azure のオブジェクトストレージで、AWS の S3・Google Cloud Storage にあたるサービスです。 ここでの「BLOB」は「コンテナーの中に置く1つのファイル(オブジェクト)」のことで、URL(https://アカウント名.blob.core.windows.net/コンテナー名/BLOB名)で参照します。この記事の確認では Azure に接続していないため、Microsoft Learn の記述の範囲で整理します。
| 項目 | 内容(Microsoft Learn、2026年10月29日時点) |
|---|---|
| 向いている用途 | 画像やドキュメントをブラウザーに直接配信、分散アクセス用のファイル、動画・音声のストリーミング、ログの書き込み、バックアップ・アーカイブ、分析用データの格納 |
| 構造 | ストレージアカウント → コンテナー(ディレクトリのように BLOB を整理)→ BLOB(データ本体) |
| ブロック BLOB | テキストとバイナリを格納。個別に管理できるブロックで構成。最大約 190.7TiB。ふつうのファイルはこれ |
| 追加 BLOB | 追加操作向けに最適化。仮想マシンからのログ記録などに |
| ページ BLOB | 最大 8TiB のランダムアクセスファイル。VHD を置いて仮想マシンのディスクにする |
| アクセス層 | ホット(頻繁に使う)・クール(30 日以上保持)・コールド(90 日以上)・アーカイブ(180 日以上、オフラインで読む前にリハイドレートが必要、最大 15 時間) |
JavaScript の Blob と直接の関係はありませんが、ブラウザで File を Azure の SDK に渡してアップロードする、という形で両方が同時に出てくることはあります。Azure の画面と料金の全体像は、関連記事「Azure Portalとは|Azureの使い方・画面・料金を初心者向けに解説」で扱っています。
Blobのよくあるエラー・つまずき(原文と直し方)
この節では、Blob まわりで出るエラーの原文と、原因・直し方が分かります。すべて手元で再現した文言です。
| エラーの原文(Chromium 153・各 DB) | 原因 | 直し方 |
|---|---|---|
TypeError: Failed to construct 'Blob': The provided value cannot be converted to a sequence. |
new Blob("abc") のように配列で囲んでいない |
new Blob(["abc"]) |
TypeError: Failed to execute 'createObjectURL' on 'URL': Overload resolution failed. |
Blob/File 以外(文字列・ArrayBuffer・undefined)を渡した | ArrayBuffer は new Blob([buf]) にしてから渡す。await 忘れで undefined になっていないか見る |
TypeError: Failed to construct 'File': 2 arguments required, but only 1 present. |
File のファイル名を省いた | new File([blob], "name.ext", {type}) |
TypeError: Failed to execute 'readAsText' on 'FileReader': parameter 1 is not of type 'Blob'. |
FileReader に文字列や files(FileList)を渡した |
files[0] のように1つの File を渡す |
net::ERR_FILE_NOT_FOUND(blob: の URL)・TypeError: Failed to fetch |
revoke 済み・作ったページを閉じた・再読み込みした・別オリジン | URL を保存して使い回さない。必要な間は revoke しない |
Not allowed to load local resource: blob:http://… |
別オリジンのページから Blob URL へ移動しようとした | 同じオリジンで作るか、サーバーの URL を使う |
TypeError: Failed to execute 'blob' on 'Response': body stream already read |
response.blob() と text() など本文を2回読んだ |
1回だけ読む。2回要るなら response.clone() |
InvalidCharacterError: Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range. |
日本語などを btoa に直接渡した |
TextEncoder でバイト列にしてから Base64 にする。Blob なら readAsDataURL |
文字化け(����ɂ���) |
Shift_JIS のファイルを text() で読んだ(既定は UTF-8) |
readAsText(blob, "Shift_JIS") か TextDecoder("shift_jis") |
ERROR 2006 (HY000): Server has gone away(MySQL/MariaDB) |
INSERT が max_allowed_packet(既定 16MB)を超えた。エラーログに Got a packet bigger than 'max_allowed_packet' bytes |
サーバーとクライアント両方の max_allowed_packet を上げるか、ストレージに置く |
ERROR 1406 (22001): Data too long for column 'd' at row 1 |
TINYBLOB(255B)などの上限を超えた | MEDIUMBLOB/LONGBLOB にする |
ERROR: invalid input syntax for type bytea(PostgreSQL) |
bytea の形式(\x… か escape)に合わない文字列を入れた |
ドライバーに bytes を渡す。SQL で書くなら '\x89504e47' か decode(…, 'base64') |
よくある質問
Q. Blob とは何の略ですか? A. Binary Large OBject の略です。もともと「かたまり」を意味する英単語で、IT では「中身の形式を解釈しないバイトの塊」を指します。JavaScript の Blob、データベースの BLOB 型、Azure Blob Storage は、どれもこの意味です。
Q. JavaScript の Blob と File の違いは何ですか?
A. File は Blob を継承したもので、name(ファイル名)と lastModified(更新日時)が加わっています。input[type=file] で選んだものは File、fetch の response.blob() や new Blob で作ったものは Blob です。File は Blob として扱える(instanceof Blob が true)ので、Blob を受け取る API には File も渡せます。
Q. blob: で始まる URL は何ですか?
A. URL.createObjectURL(blob) が返す、ブラウザの中だけで通用する仮の URL です。作ったページが開いている間だけ有効で、ページを閉じる・再読み込みする・revokeObjectURL を呼ぶと使えなくなります。別のオリジンのページからは読めません。
Q. Blob URL をコピーして別の場所で開けますか?
A. 開けません。手元では、作ったページを閉じてから別タブで開くと net::ERR_FILE_NOT_FOUND になりました。後で使うなら、Blob そのものを IndexedDB に保存するか、サーバーにアップロードして普通の URL をもらいます。
Q. revokeObjectURL は本当に必要ですか? A. 必要です。URL を付けた Blob は、JS の変数を捨てても解放されません。4MB×100 個で 455MB 増え、revoke すると 56MB まで戻りました。ページの再読み込みでも解放されますが、SPA では再読み込みが起きないので、差し替えるたびに前の URL を revoke します。
Q. Blob を Base64 に変換するには?
A. FileReader.readAsDataURL(blob) で data:image/png;base64,… の形になります。Base64 の部分だけ要るならカンマの後ろを取り出します。戻すときは fetch(dataUrl).then(r => r.blob()) が短いです。大きさは 1.33 倍になり、10MB で戻すのに 275ms かかったので、画面に出すだけなら Blob URL を使います。
Q. Blob の中身を文字列として読むには?
A. await blob.text() です。UTF-8 として読むので、Shift_JIS のファイルは new FileReader().readAsText(blob, "Shift_JIS") か new TextDecoder("shift_jis").decode(await blob.arrayBuffer()) を使います。
Q. Blob を JSON で送れますか?
A. そのままでは送れません。JSON.stringify(blob) は {} になります。ファイルを送るなら FormData に append して multipart/form-data で送るのが基本で、どうしても JSON に入れるなら Base64 にします。
Q. データベースの BLOB 型と TEXT 型の違いは?
A. BLOB は文字コードを持たないバイトの列で、比較・並べ替えはバイトの値で行われます('abc' = 'ABC' が偽)。TEXT は文字セットと照合順序を持つ文字列です(既定の照合順序では真)。画像・PDF・暗号化データは BLOB、人が読む文章は TEXT です。
Q. 画像を DB の BLOB 列に入れてよいですか? A. 数十 KB までの小さなもので、件数が増え続けず、行と一緒に消えてほしいなら入れてよいです。数 MB 以上・増え続ける・CDN から直接配信したい、のどれかに当たるならストレージに置き、DB にはパスとメタデータだけ入れます。
Q. PostgreSQL に BLOB 型はありますか?
A. 「BLOB」という名前の型はなく、bytea を使います。1フィールド 1GB までで、入出力は \x89504e47 のような16進の文字列です。別の仕組みとして OID で参照する「ラージオブジェクト」もあります。
Q. Azure Blob Storage の Blob は JavaScript の Blob と同じですか? A. 言葉の由来は同じ(バイナリの塊)ですが、別のものです。Azure の BLOB はクラウド上に置かれる1つのファイル(オブジェクト)で、ブロック・追加・ページの3種類があります。JavaScript の Blob はブラウザのメモリの中のデータです。ブラウザの File を Azure にアップロードする、という形で両方が同時に登場します。
まとめ
- Blob は「形式を決めつけないバイトの塊」。JavaScript の Blob・Blob URL・DB の BLOB 型・Azure Blob Storage のどれも同じ考え方で、文脈で指すものが変わる。
- JavaScript の Blob は
new Blob([部品], {type})で作る読み取り専用のバイト列。size はバイト数(文字列は UTF-8)、type は自己申告。File は name と lastModified を足した Blob。 - Blob URL は
URL.createObjectURLで作る 64 文字の仮の URL。作ったページが生きている間・同じオリジンからだけ使える。保存して使い回せない。 - 解放し忘れると消えない。4MB×100 個で 455MB 増え、
revokeObjectURLで 56MB に戻った。SPA では前の URL を revoke してから次を作る。 - Base64 は 1.33 倍で、戻すのに 10MB で 275ms。画面に出す・ダウンロードさせる・サーバーに送るのは Blob のままで、Base64 は数 KB の埋め込みに限る。
- DB の BLOB 型は MySQL が BLOB 4種(max_allowed_packet に注意)、PostgreSQL が bytea(1GB・
\x16進)、SQLite が BLOB。小さく・少なく・行と一緒に消すなら BLOB 列、大きく・増え続け・直接配信ならストレージ。
参考・出典
確認日はいずれも 2026年10月29日です。
- MDN「Blob」:https://developer.mozilla.org/ja/docs/Web/API/Blob
- MDN「File」:https://developer.mozilla.org/ja/docs/Web/API/File
- MDN「URL.createObjectURL()」:https://developer.mozilla.org/ja/docs/Web/API/URL/createObjectURL_static
- MDN「URL.revokeObjectURL()」:https://developer.mozilla.org/ja/docs/Web/API/URL/revokeObjectURL_static
- MDN「FileReader」:https://developer.mozilla.org/ja/docs/Web/API/FileReader
- MDN「Response: blob() メソッド」:https://developer.mozilla.org/ja/docs/Web/API/Response/blob
- W3C「File API」(Blob・Blob URL の仕様):https://w3c.github.io/FileAPI/
- Node.js「Buffer」の Blob:https://nodejs.org/docs/latest-v22.x/api/buffer.html#class-blob
- MySQL 8.4 Reference Manual「13.3.4 The BLOB and TEXT Types」:https://dev.mysql.com/doc/refman/8.4/en/blob.html
- PostgreSQL 16「8.4. Binary Data Types」:https://www.postgresql.org/docs/16/datatype-binary.html
- PostgreSQL 16「Appendix K. PostgreSQL Limits」:https://www.postgresql.org/docs/16/limits.html
- SQLite「Implementation Limits For SQLite」:https://www.sqlite.org/limits.html
- SQLite「35% Faster Than The Filesystem」:https://www.sqlite.org/fasterthanfs.html
- Microsoft Learn「binary および varbinary (Transact-SQL)」:https://learn.microsoft.com/ja-jp/sql/t-sql/data-types/binary-and-varbinary-transact-sql
- Microsoft Learn「Azure Blob Storage の概要」:https://learn.microsoft.com/ja-jp/azure/storage/blobs/storage-blobs-introduction
- Microsoft Learn「BLOB データのアクセス層」:https://learn.microsoft.com/ja-jp/azure/storage/blobs/access-tiers-overview
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Blobとは|JavaScriptのBlob URL・DBのBLOB型の違いと使い方」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。