Bufferは、Node.jsでバイナリデータ(バイトの並び)を扱うための仕組みです。画像や音声などのファイル、ネットワークから受け取った生データ、暗号化やエンコード処理などで登場します。fsでファイルを読むと中身はBufferで返り、child_processでコマンドを実行した結果もBufferです。Node.jsを使ううえで避けて通れない基本データ型です。
つまずきやすいのが、Bufferの.lengthは「文字数」ではなく「バイト数」だという点です。日本語はUTF-8で1文字3バイトになるため、"あいう"のBufferのlengthは9になります。また、Bufferを作るBuffer.allocとBuffer.allocUnsafeには中身が初期化されるかどうかの違いがあります。この記事では、実機のNode.jsで確認しながら、Bufferの扱いを整理します。
- 文字列→Bufferは
Buffer.from("文字列", "utf8")、Buffer→文字列はbuf.toString("utf8")。 toString("hex")・toString("base64")でエンコード変換ができます。.lengthはバイト数。日本語はUTF-8で1文字3バイト("あ"=3)。- Bufferの新規作成は
Buffer.alloc(n)(ゼロ埋め・安全)を使います。 Buffer.allocUnsafe(n)は高速ですが中身が初期化されません。- 結合は
Buffer.concat([...])、各バイトはbuf[0]でアクセスできます。
ファイルの読み書きはfsでファイルを読み書きする、コマンド出力の受け取りはchild_process、fetchのバイナリ取得はfetchでAPIを叩くもあわせて参考になります。
Bufferとは(どこで出てくるか)
Bufferは「固定長のバイト列」です。普段は意識しなくても、ファイルやネットワークの生データを扱うと自然に出てきます。たとえば次のような場面です。
const fs = require("node:fs");
// ファイルをエンコード指定なしで読むと Buffer が返る
const data = fs.readFileSync("image.png");
console.log(Buffer.isBuffer(data)); // true
// エンコードを指定すると文字列になる
const text = fs.readFileSync("memo.txt", "utf8"); // これは文字列
// child_process の実行結果も Buffer
// const out = execSync("node --version"); // Buffer → toString() で文字列に
fs.readFileSyncでエンコードを指定せずに読むと、中身はBufferとして返ります(Buffer.isBuffer()で確認できます)。エンコード("utf8"など)を指定すると文字列になります。画像やバイナリファイルはBufferのまま扱い、テキストは文字列に変換する、と使い分けます。
文字列とBufferの相互変換
文字列からBufferを作るにはBuffer.from("文字列", エンコード)、Bufferを文字列に戻すにはbuf.toString(エンコード)です。エンコードを変えれば、同じデータをhexやbase64として取り出すこともできます。
const buf = Buffer.from("Hello", "utf8");
console.log(buf.toString("utf8")); // Hello
console.log(buf.toString("hex")); // 48656c6c6f(16進数)
console.log(buf.toString("base64")); // SGVsbG8=(Base64)
// 逆向き(hex や base64 から文字列に戻す)
console.log(Buffer.from("48656c6c6f", "hex").toString()); // Hello
console.log(Buffer.from("SGVsbG8=", "base64").toString()); // Hello
実機でも、"Hello"のBufferはtoString("hex")で48656c6c6f、toString("base64")でSGVsbG8=になりました。逆に、hexやbase64の文字列をBuffer.from(..., "hex")などで読み込めば、元のデータに戻せます。Bufferは「バイト列」という共通の形を経由することで、文字列・16進数・Base64の間を自由に変換できるのが便利な点です。
.lengthはバイト長(文字数ではない)
もっとも注意したいのがこれです。Bufferの.lengthは文字数ではなく「バイト数」を表します。ASCII(半角英数)は1文字1バイトですが、日本語はUTF-8で1文字3バイトになるため、文字列のlengthとBufferのlengthがずれます。
const str = "あいう";
console.log(str.length); // 3(文字数)
console.log(Buffer.from(str, "utf8").length); // 9(バイト数!)
// バイト数だけ知りたいなら byteLength(Bufferを作らずに済む)
console.log(Buffer.byteLength("あ", "utf8")); // 3
console.log(Buffer.byteLength("a", "utf8")); // 1
実機で確認したところ、"あいう"の文字列のlengthは3(文字数)、Bufferのlengthは9(バイト数)になりました。日本語はUTF-8で1文字あたり3バイトを使うためです。この違いを知らないと、「文字数で確保したバッファに日本語が入りきらない」「ファイルサイズの計算が合わない」といったバグになります。文字数がほしいなら文字列の.length、データのバイト数がほしいならBufferの.lengthかBuffer.byteLength()を使い分けてください。Buffer.byteLength()は、Bufferを実際に作らずにバイト数だけ計算できるので、サイズの確認に便利です。なお、絵文字など一部の文字は4バイトになることもあります。
Bufferを作る alloc と allocUnsafe
空のBufferを新しく作るにはBuffer.alloc(サイズ)を使います。似たものにBuffer.allocUnsafe(サイズ)がありますが、こちらは中身が初期化されないため、扱いに注意が必要です。
// alloc: ゼロで初期化される(安全・推奨)
const safe = Buffer.alloc(4);
console.log(safe.toString("hex")); // 00000000(ゼロ埋め)
// allocUnsafe: 初期化されない(高速だが中身は不定)
const unsafe = Buffer.allocUnsafe(4);
// → 以前メモリにあったデータが残っている可能性がある
// 必ず全体を上書きしてから使うこと
// 文字列から作る場合は Buffer.from
const fromStr = Buffer.from("Hello");
実機で確認したところ、Buffer.alloc(4)は00000000とすべてゼロで初期化されていました。一方、Buffer.allocUnsafe(4)は初期化されず、メモリに以前あったデータがそのまま残っている可能性があります(その分、確保が高速です)。allocUnsafeで作ったBufferを初期化せずに送信・保存すると、意図しない古いデータが混ざる恐れがあり、セキュリティ上の問題にもなり得ます。特別な理由がなければBuffer.allocを使うのが安全です。allocUnsafeを使うのは、直後に必ず全体を書き込むことが分かっている、性能が重要な場面に限ります。古い書き方のnew Buffer()は非推奨なので使わないでください。
結合・バイトアクセス・スライス
複数のBufferをつなぐにはBuffer.concat()、各バイトには配列のようにbuf[0]でアクセスできます。一部を取り出すsubarray()も使えます。
// 複数の Buffer を結合
const joined = Buffer.concat([Buffer.from("AB"), Buffer.from("CD")]);
console.log(joined.toString()); // ABCD
// 各バイトにアクセス(数値で取れる)
const buf = Buffer.from("Hello");
console.log(buf[0]); // 72('H' の文字コード)
// 一部を取り出す(元のメモリを共有する点に注意)
console.log(buf.subarray(0, 3).toString()); // Hel
実機でも、Buffer.concat([...])でABとCDを結合してABCDに、buf[0]で先頭バイト72(Hの文字コード)が取得できました。buf[i]は0〜255の数値を返します。subarray()で一部を切り出せますが、元のBufferとメモリを共有するため、切り出したものを変更すると元にも影響します。独立したコピーがほしいときは、新しいBufferにBuffer.from()でコピーします。
主なメソッドまとめ
Bufferでよく使うものをまとめます。
| 書き方 | 働き |
|---|---|
Buffer.from(str, enc) |
文字列からBufferを作る |
buf.toString(enc) |
Bufferを文字列に(utf8 / hex / base64) |
buf.length |
バイト数(文字数ではない) |
Buffer.byteLength(str) |
文字列のバイト数を計算 |
Buffer.alloc(n) |
ゼロ埋めのBufferを作る(安全) |
Buffer.concat([...]) |
複数のBufferを結合 |
buf[i] / buf.subarray() |
バイトアクセス / 部分取得 |
よくある失敗
.lengthを文字数だと思う
Bufferの.lengthはバイト数です。日本語は1文字3バイトです。文字数は文字列の.lengthを使います。
allocUnsafeを初期化せず使う
古いデータが残っている可能性があります。基本はBuffer.allocを使います。
new Buffer()を使う
非推奨です。Buffer.fromやBuffer.allocを使います。
subarrayの結果を独立コピーだと思う
元とメモリを共有します。独立コピーはBuffer.from()で作ります。
エンコード指定を間違える
作成時と変換時のエンコードをそろえます。違うと文字化けします。
よくある質問
fs.readFileSyncでエンコードを指定せずに読むと、中身はBufferで返ります。文字列にしたいときはtoString()を使います。.lengthは文字数ではなくバイト数だからです。日本語はUTF-8で1文字3バイトのため、"あいう"のBufferのlengthは9になります。文字数がほしいときは文字列の.length、バイト数がほしいときはBufferの.lengthやBuffer.byteLength()を使います。Buffer.alloc(n)は中身をゼロで初期化するため安全です。Buffer.allocUnsafe(n)は初期化しないぶん高速ですが、メモリに以前あったデータが残っている可能性があります。特別な理由がなければBuffer.allocを使ってください。Buffer.from("文字列").toString("base64")でBase64に、toString("hex")で16進数に変換できます。逆にBuffer.from("SGVsbG8=", "base64").toString()で元に戻せます。Bufferを経由することで、各形式の間を自由に変換できます。new Buffer()は非推奨で、使うべきではありません。文字列から作るならBuffer.from()、空のBufferを作るならBuffer.alloc()を使ってください。古いコードでnew Buffer()を見かけたら、これらに置き換えます。まとめ
- Bufferはバイナリデータ用の型。fsやchild_processの結果として自然に登場します。
- 変換は
Buffer.from(str, enc)とbuf.toString(enc)。hex・base64にもできます。 .lengthはバイト数。日本語はUTF-8で1文字3バイトです。- 新規作成は
Buffer.alloc(ゼロ埋め・安全)を基本に。allocUnsafeは上書き前提。 - 結合は
Buffer.concat、バイトはbuf[i]でアクセスします。
Bufferは、ファイルやネットワークの生データを扱うときに欠かせない存在です。「.lengthはバイト数」「基本はBuffer.alloc」という2点を押さえれば、日本語のバイト数のずれや初期化漏れといった定番のミスを避けられます。エンコード変換の便利さも、ぜひ活用してみてください。

