Buffer adalah data mentah dalam bentuk byte, dan stream adalah cara memproses data sedikit demi sedikit (per chunk) tanpa memuat semuanya ke memori. Bayangin memindahkan air dari tandon ke ember. Kamu gak mengangkat tandonnya: kamu pasang selang, dan air mengalir sedikit-sedikit. Kalau ember di ujung lambat (misalnya sambil disaring), kerannya dikecilkan supaya air gak meluap. Selang itu stream, tiap semburan air itu chunk, isinya byte (Buffer), dan mengecilkan keran itu namanya backpressure.
🧪 Tebakan cepat:
Buffer.from("café").lengthhasilnya berapa? Kata itu 4 huruf. Tebak dulu, jawabannya ada di bagian "Buffer: byte mentah".
Buffer: byte mentah
File, request jaringan, dan gambar semuanya datang sebagai byte: angka 0 sampai 255. Node menyimpannya di Buffer dan menampilkannya dalam hex:
const buf = Buffer.from("Halo Dunia", "utf-8");
console.log(buf); // <Buffer 48 61 6c 6f 20 44 75 6e 69 61>
console.log(buf.toString()); // Halo Dunia
console.log(buf.length); // 10
console.log(Buffer.from("Node").length); // 4
console.log(Buffer.from("café")); // <Buffer 63 61 66 c3 a9>
console.log(Buffer.from("café").length); // 5
Buffer.from(teks, "utf-8")mengubah teks jadi byte.48itu hex untuk hurufH,20untuk spasi.buf.lengthmenghitung byte, bukan huruf. Huruf biasa (A-Z, angka, spasi) masing-masing 1 byte, jadi"Halo Dunia"= 10 byte dan"Node"= 4.- UTF-8 memakai lebih dari 1 byte untuk huruf lain:
é= 2 byte (c3 a9),€= 3 byte. JadiBuffer.from("café").lengthhasilnya 5, padahal"café".length= 4. Itu jawaban tebakannya. Buffer.alloc(1024)membuat Buffer kosong 1.024 byte berisi nol.
Stream: membaca sepotong-sepotong
catatan-api punya route GET /catatan/ekspor yang mengirim catatan.csv, 5.000.000 byte. Dengan fs.readFile, seluruh 5 MB masuk memori dulu, baru dikirim. Seratus orang mengunduh bersamaan berarti 500 MB. Dengan stream, file dibaca per chunk:
const fs = require("node:fs");
const stream = fs.createReadStream("catatan.csv");
let jumlah = 0;
stream.on("data", (chunk) => {
jumlah++;
console.log("Chunk " + jumlah + ": " + chunk.length + " byte");
});
stream.on("end", () => console.log("Selesai, " + jumlah + " chunk"));
stream.on("error", (err) => console.error("Gagal: " + err.message));
Chunk 1: 65536 byte
Chunk 2: 65536 byte
...
Chunk 77: 19264 byte
Selesai, 77 chunk
Tiap chunk adalah Buffer 65.536 byte (64 KiB), ukuran baku createReadStream: 76 chunk penuh dan sisa 19.264 byte. Yang ada di memori pada satu waktu cuma satu chunk itu. Node punya empat jenis stream: Readable (sumber, misalnya file yang dibaca), Writable (tujuan, misalnya res), Duplex (dua-duanya, misalnya socket), dan Transform (mengubah data yang lewat, misalnya kompresi gzip).
pipe() dan pipeline(): menyambung selang
pipe() menyambungkan Readable ke Writable, jadi tiap chunk yang dibaca langsung ditulis:
app.get("/catatan/ekspor", (req, res) => {
res.setHeader("Content-Type", "text/csv");
fs.createReadStream("catatan.csv").pipe(res);
});
pipeline() melakukan hal yang sama untuk beberapa stream sekaligus, dan memanggil satu callback saat selesai atau gagal:
const { pipeline } = require("node:stream");
const zlib = require("node:zlib");
pipeline(fs.createReadStream("catatan.csv"), zlib.createGzip(), fs.createWriteStream("catatan.csv.gz"), (err) => {
console.log(err ? "Gagal: " + err.message : "Selesai: catatan.csv.gz");
});
Kalau catatan.csv gak ada, yang tercetak Gagal: ENOENT: no such file or directory, open 'catatan.csv', dan semua stream di dalamnya ditutup. Dengan .pipe() berantai, error seperti itu harus kamu tangkap satu per satu.
Backpressure: kalau konsumen lebih lambat
Membaca dari disk itu cepat, mengirim ke HP pengguna lewat sinyal lemah itu lambat. Kalau pembaca terus mengirim chunk, antrean di tengah membengkak di memori. Makanya writable.write(chunk) mengembalikan false begitu antreannya melewati highWaterMark (64 KiB sejak Node 22, sebelumnya 16 KiB): tanda untuk berhenti dulu sampai event "drain".
readable.on("data", (chunk) => {
if (!writable.write(chunk)) readable.pause(); // antrean penuh: berhenti membaca
});
writable.on("drain", () => readable.resume()); // antrean kosong lagi: lanjut
pipe() dan pipeline() sudah melakukan ini untukmu. Web Streams di browser memakai ide yang sama: highWaterMark menentukan berapa chunk boleh mengantre, dan controller.desiredSize memberi tahu sisa tempatnya. Di Lab, coba konsumen lambat dan lihat antreannya berhenti di highWaterMark.
Huruf yang terpotong di batas chunk
Batas chunk ditentukan jumlah byte, bukan huruf, jadi satu huruf UTF-8 bisa terbelah. "ngopi di café" dibaca per 13 byte:
fs.createReadStream("kopi.txt", { highWaterMark: 13 }).on("data", (chunk) => {
console.log(chunk, chunk.toString());
});
<Buffer 6e 67 6f 70 69 20 64 69 20 63 61 66 c3> ngopi di caf�
<Buffer a9> �
c3 dan a9 terpisah, dan masing-masing diubah jadi teks sendiri-sendiri, jadi muncul � dua kali. Perbaikannya: minta stream-nya yang mengurus teks dengan { encoding: "utf8" }. Byte yang belum lengkap ditahan sampai pasangannya datang, hasilnya "ngopi di caf" lalu "é". Di browser, new TextDecoder().decode(chunk, { stream: true }) melakukan hal yang sama.
Di latihan: Buffer.from dan potongan
Latihannya mengukur "Halo Dunia" dengan Buffer.from(...).length, lalu memotong teks 54 karakter "Ini adalah contoh data yang akan diproses secara chunk" per 10 karakter, seperti stream memotong file. Potongan terakhir boleh lebih pendek:
Buffer: "Halo Dunia" = 10 bytes
Data: "Ini adalah contoh data yang akan diproses secara chunk"
Chunk size: 10
Jumlah chunks: 6
Chunk 1: "Ini adalah"
Chunk 2: " contoh da"
Chunk 3: "ta yang ak"
Chunk 4: "an diprose"
Chunk 5: "s secara c"
Chunk 6: "hunk"
Buffer: "café" = 5 bytes
Baris terakhir mengukur "café" dengan cara yang sama: 5 byte, bukan 4, karena é makan 2 byte.
⚠️ Jebakan yang sering ketemu
readFileuntuk file besar. Seluruh isinya masuk memori per request. Untuk file besar dan unduhan, pakaicreateReadStream(...).pipe(res).- Mengira
lengthitu jumlah huruf.Buffer.lengthmenghitung byte:"café"= 5. chunk.toString()per chunk. Huruf multi-byte di batas chunk jadi�. Pakai{ encoding: "utf8" }atauTextDecoderdengan{ stream: true }.- Mengabaikan
falsedariwrite(). Antrean membengkak sampai memori habis. Pakaipipe/pipeline, atau tunggu"drain".
TL;DR: Buffer menyimpan byte mentah: Buffer.from("Halo Dunia") mencetak <Buffer 48 61 6c 6f 20 44 75 6e 69 61>, length-nya 10 byte, dan "café" jadi 5 byte karena é = c3 a9. Stream memproses data per chunk (64 KiB untuk createReadStream) supaya memori gak penuh. Ada Readable, Writable, Duplex, dan Transform. pipe() menyambungkan Readable ke Writable, pipeline() juga menangani error dan menutup semuanya. Backpressure: write() mengembalikan false saat antrean melewati highWaterMark, lalu tunggu "drain" (pipe melakukannya sendiri). Huruf multi-byte bisa terbelah di batas chunk, jadi baca teks dengan { encoding: "utf8" } atau TextDecoder { stream: true }.