ArrayBuffer: Foundation of Binary Data in Browsers
When you upload a photo to Instagram, edit an image in Photoshop online, or play music in Spotify's web player, the browser needs to work with binary data—raw bytes representing images, audio, and files. Unlike Node.js which has the Buffer
class for this purpose, browsers use ArrayBuffer as the foundation for handling binary data.
Understanding ArrayBuffer is your first step toward working with files, processing images, handling audio, implementing custom file formats, and communicating over WebSockets. Let's explore what ArrayBuffer is, why it exists, and how it fits into the bigger picture of binary data in browsers.
What Problem Does ArrayBuffer Solve?
Before diving into what ArrayBuffer is, let's understand the problem it solves.
JavaScript Was Designed for Text
When JavaScript was created in 1995, it was designed to make web pages interactive—handle button clicks, validate forms, and manipulate HTML. Everything was text-based: strings, HTML, and simple numbers.
What JavaScript could do easily:
// Working with text - JavaScript's original purpose
const greeting = "Hello, World!";
const htmlContent = "<div>Welcome</div>";
const jsonData = '{"name": "John", "age": 30}';
// All of this is straightforward in JavaScript
Modern Web Apps Need Binary Data
But today's web applications need to do much more than manipulate text. They need to handle:
Real-world scenarios requiring binary data:
- Photo editing apps like Photopea or Pixlr need to read and modify image files
- Music players like Spotify Web need to decode and play audio files
- Video conferencing like Zoom or Google Meet need to process video streams
- File uploaders like Dropbox or Google Drive need to upload any file type
- Games need to load textures, sounds, and game assets
- PDF viewers need to parse and display PDF files
All of these involve binary data—raw bytes that don't represent text.
Why Strings Don't Work for Binary Data
You might wonder: "Can't we just use strings for everything?" Let's see why that doesn't work:
// ❌ Problem: Trying to handle binary data as text
async function readImageAsText(file: File): Promise<string> {
// Read image file as text
const text = await file.text();
// Result: Garbled characters like "ÿØÿàJFIF"
// Problems:
// 1. Data is corrupted (some bytes become invalid characters)
// 2. File size increases (some bytes need multi-byte encoding)
// 3. Can't manipulate individual bytes
// 4. Can't interpret the data correctly
return text;
}
// What the image bytes actually look like:
// [0xFF, 0xD8, 0xFF, 0xE0, ...] - JPEG header
// When forced into text: "ÿØÿà..." - meaningless garbage
Why this fails:
- Data corruption: Not all bytes are valid text characters. When JavaScript tries to interpret bytes 0-31 or 128-255 as text, it creates invalid or unexpected characters
- Loss of information: Binary files use every possible byte value (0-255). Text encoding systems like UTF-8 use multi-byte sequences, changing the data
- Cannot manipulate: You can't extract pixel data, modify audio samples, or parse file headers when everything is treated as text
- Performance: Converting binary to text and back is slow and wasteful
The Real-World Impact
Here's a concrete example of what happens without proper binary support:
// Imagine trying to build a photo editor without binary data support
// User uploads a photo
const photoFile: File = /* user's uploaded image */;
// ❌ Without binary support:
// - Can't read the image format (JPEG, PNG, GIF)
// - Can't extract width and height
// - Can't access individual pixels
// - Can't apply filters or effects
// - Can't save modified image
// ✅ With binary support (using ArrayBuffer):
const arrayBuffer = await photoFile.arrayBuffer();
// - Can read file headers to detect format
// - Can extract metadata (dimensions, color space)
// - Can access and modify every pixel
// - Can apply complex effects
// - Can save in any format
This is exactly why ArrayBuffer was created—to give JavaScript proper binary data handling capabilities.
What Is ArrayBuffer?
Now that you understand the problem, let's define the solution.
ArrayBuffer is a fixed-size container that holds raw binary data in memory. It's a continuous block of bytes that JavaScript can allocate and manage.
Simple definition: An ArrayBuffer is like a box that can hold a specific number of bytes. Once created, the box size cannot change, but you can read and write the bytes inside it.
Real-World Analogy
Think of ArrayBuffer like a row of post office boxes:
Post Office Boxes (ArrayBuffer)
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ Box │ Box │ Box │ Box │ Box │ Box │ Box │ Box │
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
The analogy breakdown:
- Each box = One byte in the ArrayBuffer
- Box number = Position (0, 1, 2, etc.)
- Box contents = The byte value (0-255)
- The entire row = The complete ArrayBuffer
- Fixed size = You can't add or remove boxes, only change what's inside
- Empty at creation = All boxes start with 0
Just like you can't open a post office box directly without a key, you can't access ArrayBuffer bytes directly—you need a "view" (which we'll learn about in the next article).
Creating Your First ArrayBuffer
Let's create an ArrayBuffer and explore its properties:
// Create an ArrayBuffer that holds 8 bytes
const buffer = new ArrayBuffer(8);
// Check its size
console.log(buffer.byteLength); // 8 (bytes)
// Log the ArrayBuffer
console.log(buffer);
// Output: ArrayBuffer { [Uint8Contents]: <00 00 00 00 00 00 00 00>, byteLength: 8 }
// The bytes are initialized to 0
// Visual representation:
// [0, 0, 0, 0, 0, 0, 0, 0]
// ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
// 0 1 2 3 4 5 6 7 ← byte positions
What this code does:
new ArrayBuffer(8)
- Allocates 8 bytes of memorybyteLength
- Property that tells you the size- All bytes start at 0 - Default initialization
- Memory is continuous - All 8 bytes are next to each other
ArrayBuffer Properties and Characteristics
Let's explore what makes ArrayBuffer unique:
// Property 1: Fixed size
const buffer = new ArrayBuffer(10);
console.log(buffer.byteLength); // 10
// You CANNOT resize an ArrayBuffer
// If you need more space, create a new one
// buffer.byteLength = 20; // ❌ This doesn't work!
// Property 2: All bytes initialized to zero
// When created, every byte is 0
const newBuffer = new ArrayBuffer(5);
// Memory: [0, 0, 0, 0, 0]
// Property 3: Cannot be accessed directly
// console.log(buffer[0]); // ❌ undefined - this doesn't work!
// You need a "view" to access the bytes (covered in next article)
// Property 4: You can check the type
console.log(buffer instanceof ArrayBuffer); // true
// Property 5: You can get a copy using slice
const original = new ArrayBuffer(10);
const copy = original.slice(2, 6); // Copy bytes 2-5
console.log(copy.byteLength); // 4 (bytes 2, 3, 4, 5)
Why ArrayBuffer Cannot Be Accessed Directly
This might seem frustrating—why create a container you can't access? There's an important reason for this design.
The problem with direct access:
// Imagine if we could access ArrayBuffer directly
const buffer = new ArrayBuffer(4);
// What would this mean?
buffer[0] = 255;
// Questions that arise:
// - Is 255 a single byte? (uint8)
// - Is it part of a 16-bit number? (uint16)
// - Is it part of a 32-bit number? (uint32)
// - Is it a signed or unsigned number?
// - What about decimal numbers?
// The answer: It's ambiguous!
The solution: Separate data from interpretation
ArrayBuffer holds the raw data, but you choose how to interpret it using "views":
const buffer = new ArrayBuffer(4);
// Interpret as four 8-bit unsigned integers
const view8 = new Uint8Array(buffer);
view8[0] = 255; // One byte
// Interpret the SAME buffer as one 32-bit unsigned integer
const view32 = new Uint32Array(buffer);
console.log(view32[0]); // All 4 bytes as one number
// Same data, different interpretations!
// This flexibility is powerful and necessary
Visual representation:
ArrayBuffer (4 bytes):
┌─────┬─────┬─────┬─────┐
│ 255 │ 0 │ 0 │ 0 │
└─────┴─────┴─────┴─────┘
Interpreted as Uint8Array (4 elements):
[255, 0, 0, 0]
Interpreted as Uint32Array (1 element):
[255]
Same bytes, different meanings!
This separation of storage (ArrayBuffer) from interpretation (views) is a key design principle that makes binary data handling flexible and powerful.
Common ArrayBuffer Operations
Let's explore the basic operations you can perform with ArrayBuffer.
Creating ArrayBuffers
// Method 1: Create empty buffer of specific size
const buffer1 = new ArrayBuffer(100); // 100 bytes of zeros
console.log(buffer1.byteLength); // 100
// Method 2: Create from existing buffer (copy)
const buffer2 = buffer1.slice(0, 50); // Copy first 50 bytes
console.log(buffer2.byteLength); // 50
// Method 3: From File API
const fileInput = document.querySelector<HTMLInputElement>("#fileInput");
fileInput?.addEventListener("change", async (event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
// Read file as ArrayBuffer
const buffer = await file.arrayBuffer();
console.log(`File size: ${buffer.byteLength} bytes`);
}
});
// Method 4: From Fetch API
async function downloadBinary(url: string): Promise<ArrayBuffer> {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
return buffer;
}
Copying and Slicing
// Create original buffer
const original = new ArrayBuffer(10);
// slice(start, end) - creates a COPY
const copy1 = original.slice(0, 5); // Copy bytes 0-4
const copy2 = original.slice(5, 10); // Copy bytes 5-9
const copy3 = original.slice(2); // Copy from byte 2 to end
console.log(copy1.byteLength); // 5
console.log(copy2.byteLength); // 5
console.log(copy3.byteLength); // 8
// Important: slice() creates a NEW ArrayBuffer
// Modifying the copy doesn't affect the original
// (We'll see this in action when we learn about views)
Checking Buffer Size
const buffer = new ArrayBuffer(1024); // 1 KB
// Get size in bytes
console.log(buffer.byteLength); // 1024
// Calculate size in different units
const kilobytes = buffer.byteLength / 1024;
const megabytes = buffer.byteLength / (1024 * 1024);
console.log(`Size: ${kilobytes} KB`); // 1 KB
console.log(`Size: ${megabytes.toFixed(2)} MB`); // 0.00 MB
// Check if it's actually an ArrayBuffer
if (buffer instanceof ArrayBuffer) {
console.log("Yes, this is an ArrayBuffer");
}
Transferring ArrayBuffers (Advanced)
ArrayBuffers can be "transferred" between contexts (like Web Workers) for efficient data sharing:
// Main thread
const buffer = new ArrayBuffer(1000000); // 1 MB
// Transfer to Web Worker (zero-copy operation)
const worker = new Worker("worker.js");
worker.postMessage({ data: buffer }, [buffer]);
// Important: After transfer, buffer is "detached"
console.log(buffer.byteLength); // 0 - buffer is now empty!
// The Web Worker now owns the buffer
// This is much faster than copying 1 MB of data
// The buffer ownership is transferred, not copied
When Do You Use ArrayBuffer?
Understanding when to use ArrayBuffer helps you recognize the right tool for the job.
Use ArrayBuffer When Working With:
1. File Operations
// Reading uploaded files
async function processUploadedFile(file: File): Promise<void> {
const arrayBuffer = await file.arrayBuffer();
console.log(`Received ${arrayBuffer.byteLength} bytes`);
// Now you can:
// - Check file format by reading header bytes
// - Extract metadata
// - Process the file content
// - Modify and save
}
2. Network Communication
// Receiving binary data over WebSocket
const ws = new WebSocket("wss://example.com");
ws.binaryType = "arraybuffer"; // Important!
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
console.log(`Received ${event.data.byteLength} bytes`);
// Process binary message
}
};
3. Image Processing
// Loading and processing images
async function processImage(imageUrl: string): Promise<void> {
const response = await fetch(imageUrl);
const arrayBuffer = await response.arrayBuffer();
// Now you can:
// - Read image format (JPEG, PNG, etc.)
// - Extract pixel data
// - Apply filters
// - Convert formats
}
4. Audio/Video Processing
// Loading audio data
async function loadAudio(audioUrl: string): Promise<AudioBuffer> {
const response = await fetch(audioUrl);
const arrayBuffer = await response.arrayBuffer();
// Decode audio data
const audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
return audioBuffer;
}
5. Custom Binary Formats
// Reading custom file formats
async function readCustomFormat(file: File): Promise<void> {
const arrayBuffer = await file.arrayBuffer();
// Parse your custom format:
// - Read header (first N bytes)
// - Parse data sections
// - Extract structured information
}
ArrayBuffer vs Other Data Types
Let's compare ArrayBuffer with other ways JavaScript handles data:
ArrayBuffer vs String
// String: For text data
const text: string = "Hello, World!";
console.log(typeof text); // "string"
console.log(text.length); // 13 characters
// ArrayBuffer: For binary data
const buffer: ArrayBuffer = new ArrayBuffer(13);
console.log(typeof buffer); // "object"
console.log(buffer.byteLength); // 13 bytes
// Key differences:
// 1. String stores characters, ArrayBuffer stores bytes
// 2. String has .length (characters), ArrayBuffer has .byteLength (bytes)
// 3. String is for text, ArrayBuffer is for any binary data
// 4. String can be directly accessed, ArrayBuffer needs a view
ArrayBuffer vs Array
// Regular Array: For any JavaScript values
const array: number[] = [1, 2, 3, 4, 5];
console.log(array[0]); // 1 - direct access
console.log(array.length); // 5 elements
array.push(6); // Can resize
// ArrayBuffer: For raw bytes only
const buffer: ArrayBuffer = new ArrayBuffer(5);
// console.log(buffer[0]); // ❌ undefined - no direct access
console.log(buffer.byteLength); // 5 bytes (fixed size)
// buffer cannot be resized
// Key differences:
// 1. Array holds any values, ArrayBuffer holds bytes only
// 2. Array is resizable, ArrayBuffer is fixed-size
// 3. Array can be accessed directly, ArrayBuffer needs a view
// 4. Array is slower/uses more memory, ArrayBuffer is faster/compact
ArrayBuffer vs Blob
// Blob: For file-like data
const blob = new Blob(["Hello"], { type: "text/plain" });
console.log(blob.size); // 5 bytes
console.log(blob.type); // "text/plain"
// ArrayBuffer: For binary data in memory
const buffer = new ArrayBuffer(5);
console.log(buffer.byteLength); // 5 bytes
// No type information
// Key differences:
// 1. Blob can have a MIME type, ArrayBuffer doesn't
// 2. Blob is for files/downloads, ArrayBuffer is for processing
// 3. Blob can be larger (disk-backed), ArrayBuffer is memory-only
// 4. Convert between them:
// - Blob → ArrayBuffer: await blob.arrayBuffer()
// - ArrayBuffer → Blob: new Blob([arrayBuffer])
Quick Reference Table
Feature | String | Array | ArrayBuffer | Blob |
---|---|---|---|---|
Stores | Text | Any values | Bytes | File-like data |
Size | Variable | Variable | Fixed | Variable |
Direct access | Yes | Yes | No | No |
Best for | Text | Collections | Binary processing | Files |
Memory | More | Most | Least | Disk-backed |
Common Pitfalls and Solutions
Let's address common mistakes when working with ArrayBuffer.
Pitfall 1: Trying to Access Bytes Directly
// ❌ Wrong: Trying to read ArrayBuffer directly
const buffer = new ArrayBuffer(10);
console.log(buffer[0]); // undefined - doesn't work!
// ✅ Correct: Use a view (covered in next article)
const view = new Uint8Array(buffer);
console.log(view[0]); // 0 - works!
Pitfall 2: Expecting ArrayBuffer to Resize
// ❌ Wrong: Trying to resize ArrayBuffer
const buffer = new ArrayBuffer(10);
// buffer.byteLength = 20; // ❌ Cannot resize!
// ✅ Correct: Create a new ArrayBuffer
const smallBuffer = new ArrayBuffer(10);
const largeBuffer = new ArrayBuffer(20);
// Copy data if needed
const view1 = new Uint8Array(smallBuffer);
const view2 = new Uint8Array(largeBuffer);
view2.set(view1); // Copy all bytes from small to large
Pitfall 3: Confusing byteLength with length
// ArrayBuffer uses byteLength, not length
const buffer = new ArrayBuffer(100);
console.log(buffer.byteLength); // ✅ 100
console.log(buffer.length); // ❌ undefined
// Remember: ArrayBuffer measures in BYTES
// Later: Typed Arrays use .length (elements) AND .byteLength (bytes)
Pitfall 4: Not Handling Detached Buffers
// After transferring, buffer becomes detached
const buffer = new ArrayBuffer(100);
const worker = new Worker("worker.js");
// Transfer buffer to worker
worker.postMessage({ data: buffer }, [buffer]);
// Buffer is now detached (empty)
console.log(buffer.byteLength); // 0
// ❌ Wrong: Trying to use detached buffer
// const view = new Uint8Array(buffer); // ❌ Error!
// ✅ Correct: Create new buffer or don't transfer
const buffer2 = new ArrayBuffer(100);
worker.postMessage({ data: buffer2.slice(0) }); // Send copy, keep original
Summary: Key Takeaways
What is ArrayBuffer?
- A fixed-size container for raw binary data
- Holds a specific number of bytes in continuous memory
- Created with
new ArrayBuffer(size)
- All bytes initialized to 0
Why ArrayBuffer exists:
- JavaScript was designed for text, not binary data
- Modern web apps need to handle images, audio, files, and network data
- Strings corrupt binary data and waste memory
- ArrayBuffer provides proper binary data support
Key characteristics:
- Fixed size: Cannot be resized after creation
- Raw data: Just bytes, no type information
- Indirect access: Cannot access bytes directly, need a view
- Transferable: Can be transferred to Web Workers efficiently
When to use ArrayBuffer:
- Reading/writing files (images, audio, documents)
- Network communication (WebSocket, binary protocols)
- Image processing (pixels, filters, formats)
- Audio/video processing (samples, frames, codecs)
- Custom binary formats (parsers, serializers)
What's next:
- Learn about Typed Arrays to actually read/write bytes
- Understand different view types (Uint8Array, Int16Array, etc.)
- Work with real files and images
- Build practical binary data applications
ArrayBuffer is your foundation for working with binary data in the browser—master it, and you'll unlock powerful capabilities for your web applications!