Modern frontend applications deal with files all the time.

Users upload avatars, drop videos into dashboards, export CSV reports, preview PDFs, download generated configuration files, and work with media directly in the browser. At first, these features look simple. A file input, a preview element, maybe a download button, and the job seems done.

Then the real problems appear.

Large files freeze the tab. Image uploads are too slow. Data URLs become huge. Browser memory keeps growing. Previews work for one file type but not another. A dashboard that looked fine during testing becomes unstable after a user processes dozens of images in a single session.

This is where the Blob API becomes important.

A Blob is not just a small browser feature for downloads. It is one of the core building blocks behind file handling in modern JavaScript. Once you understand how Blob objects work, you can build cleaner upload flows, safer previews, better export tools, and more memory-efficient interfaces.

This article walks through practical Blob usage in real frontend work: creating Blob objects, processing large files in chunks, compressing images, building previews, generating downloads, and avoiding memory leaks caused by forgotten object URLs.

What Is a Blob?

A Blob is an immutable object that represents raw data.

That data can be text, JSON, CSV, an image, a video, a PDF, or any other binary content the browser can hold. The word “Blob” stands for Binary Large Object, but in frontend development you can think of it as a file-like container created or handled inside the browser.

A minimal Blob looks like this:

ts
const textBlob = new Blob(['Hello from the Blob API'], {
  type: 'text/plain',
});

The first argument is an array of data parts. The second argument describes the Blob. Most of the time, the most important option is type, which stores the MIME type.

Examples of common MIME types:

txt
text/plain
application/json
text/csv
image/png
image/jpeg
application/pdf

The MIME type helps the browser understand how to treat the data. A JSON Blob, an image Blob, and a PDF Blob may all be Blob objects, but the browser can preview, download, or send them differently depending on the type.

Why Blob Is Better Than Large Data URLs

A common beginner approach for generating files is to create a data: URL manually:

ts
const hugeText = 'Some large content...'.repeat(100_000);

const url =
  'data:text/plain;charset=utf-8,' +
  encodeURIComponent(hugeText);

This works for small examples, but it scales badly. Large data URLs can consume more memory than expected, become difficult for the browser to handle, and fail when the generated URL becomes too long.

Blob is usually the better option:

ts
const hugeText = 'Some large content...'.repeat(100_000);

const blob = new Blob([hugeText], {
  type: 'text/plain',
});

const url = URL.createObjectURL(blob);

Instead of encoding everything into a giant string URL, the browser creates an object URL that points to the Blob data internally. That is cleaner, more memory-friendly, and more reliable for larger content.

Creating Blob Objects Properly

Blob creation is simple, but in real projects it is worth wrapping it in small utilities so your code stays consistent.

ts
type BlobOptionsInput = {
  mimeType?: string;
};

function createFileBlob(
  parts: BlobPart[],
  options: BlobOptionsInput = {}
) {
  return new Blob(parts, {
    type: options.mimeType ?? 'text/plain',
  });
}

Now you can create different file-like objects with explicit MIME types.

Text Blob

ts
const readmeBlob = createFileBlob(
  ['This file was generated in the browser.'],
  { mimeType: 'text/plain' }
);

JSON Blob

ts
const userSettings = {
  theme: 'dark',
  compactMode: true,
  language: 'en',
};

const settingsBlob = createFileBlob(
  [JSON.stringify(userSettings, null, 2)],
  { mimeType: 'application/json' }
);

HTML Blob

ts
const htmlDocument = `
  <!doctype html>
  <html>
    <body>
      <h1>Generated HTML</h1>
    </body>
  </html>
`;

const htmlBlob = createFileBlob(
  [htmlDocument],
  { mimeType: 'text/html' }
);

The main rule is simple: always set the correct MIME type when you know what kind of data you are creating.

Downloading Generated Files in the Browser

One of the most common Blob use cases is generating downloadable files without asking the server to create them.

For example, a settings page may allow users to export their configuration as JSON:

ts
function downloadJsonFile(
  data: unknown,
  filename = 'settings.json'
) {
  const json = JSON.stringify(data, null, 2);

  const blob = new Blob([json], {
    type: 'application/json',
  });

  const url = URL.createObjectURL(blob);

  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  link.style.display = 'none';

  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);

  URL.revokeObjectURL(url);
}

Usage:

ts
downloadJsonFile(
  {
    editor: 'VS Code',
    theme: 'dark',
    autosave: true,
  },
  'developer-settings.json'
);

This pattern is useful for:

  • exporting user settings
  • downloading generated reports
  • creating local backups
  • exporting analytics data
  • generating JSON, CSV, or text files from client-side state

The important part is cleanup:

ts
URL.revokeObjectURL(url);

Whenever you create an object URL, you should also decide when to revoke it.

Processing Large Files in Chunks

Reading a large file all at once can hurt performance.

This looks harmless:

ts
const reader = new FileReader();

reader.onload = () => {
  const content = reader.result;
  console.log(content);
};

reader.readAsText(file);

For small files, this is fine. For large files, it can freeze the page or consume too much memory.

The safer pattern is chunked processing.

A File is a kind of Blob, so it supports the slice() method. This allows you to read a file piece by piece.

ts
async function processFileInChunks(
  file: File,
  chunkSize = 1024 * 1024
) {
  const totalChunks = Math.ceil(file.size / chunkSize);

  for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
    const start = chunkIndex * chunkSize;
    const end = Math.min(start + chunkSize, file.size);

    const chunk = file.slice(start, end);

    await processChunk(chunk, chunkIndex, totalChunks);
  }
}

A simple chunk reader can look like this:

ts
function readBlobAsText(blob: Blob): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload = () => {
      resolve(String(reader.result ?? ''));
    };

    reader.onerror = () => {
      reject(reader.error);
    };

    reader.readAsText(blob);
  });
}

async function processChunk(
  chunk: Blob,
  index: number,
  total: number
) {
  const text = await readBlobAsText(chunk);

  console.log({
    index,
    total,
    size: chunk.size,
    preview: text.slice(0, 80),
  });
}

This approach gives you more control over memory usage and makes it easier to show progress.

ts
async function processFileWithProgress(
  file: File,
  onProgress: (percentage: number) => void
) {
  const chunkSize = 1024 * 1024;
  const totalChunks = Math.ceil(file.size / chunkSize);

  for (let index = 0; index < totalChunks; index++) {
    const start = index * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);

    await processChunk(chunk, index, totalChunks);

    const percentage = Math.round(((index + 1) / totalChunks) * 100);
    onProgress(percentage);
  }
}

Chunking is especially helpful for:

  • large text files
  • logs
  • CSV imports
  • video uploads
  • browser-based file analysis tools

Building a Chunked Upload Flow

Chunked uploads are a natural extension of Blob slicing.

Instead of sending a full file in one request, you split it into parts and upload each part separately.

ts
type ChunkUploadOptions = {
  endpoint: string;
  chunkSize?: number;
  onProgress?: (progress: {
    uploadedChunks: number;
    totalChunks: number;
    percentage: number;
  }) => void;
};

class ChunkedFileUploader {
  private file: File;
  private endpoint: string;
  private chunkSize: number;
  private onProgress?: ChunkUploadOptions['onProgress'];

  constructor(file: File, options: ChunkUploadOptions) {
    this.file = file;
    this.endpoint = options.endpoint;
    this.chunkSize = options.chunkSize ?? 2 * 1024 * 1024;
    this.onProgress = options.onProgress;
  }

  async upload() {
    const uploadId = this.createUploadId();
    const totalChunks = Math.ceil(this.file.size / this.chunkSize);

    for (let index = 0; index < totalChunks; index++) {
      const start = index * this.chunkSize;
      const end = Math.min(start + this.chunkSize, this.file.size);
      const chunk = this.file.slice(start, end);

      await this.uploadChunk(chunk, index, uploadId);

      this.onProgress?.({
        uploadedChunks: index + 1,
        totalChunks,
        percentage: Math.round(((index + 1) / totalChunks) * 100),
      });
    }

    return this.completeUpload(uploadId, totalChunks);
  }

  private async uploadChunk(
    chunk: Blob,
    index: number,
    uploadId: string
  ) {
    const formData = new FormData();

    formData.append('chunk', chunk);
    formData.append('index', String(index));
    formData.append('uploadId', uploadId);
    formData.append('filename', this.file.name);

    const response = await fetch(this.endpoint, {
      method: 'POST',
      body: formData,
    });

    if (!response.ok) {
      throw new Error(`Failed to upload chunk ${index}`);
    }

    return response.json();
  }

  private async completeUpload(uploadId: string, totalChunks: number) {
    const response = await fetch(`${this.endpoint}/complete`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        uploadId,
        totalChunks,
        filename: this.file.name,
      }),
    });

    if (!response.ok) {
      throw new Error('Failed to complete upload');
    }

    return response.json();
  }

  private createUploadId() {
    return `${Date.now()}-${crypto.randomUUID()}`;
  }
}

Usage:

ts
const uploader = new ChunkedFileUploader(file, {
  endpoint: '/api/uploads',
  onProgress(progress) {
    console.log(`Uploaded ${progress.percentage}%`);
  },
});

await uploader.upload();

This does require backend support. The server must receive chunks, store them temporarily, and merge them after the final chunk arrives. But on the frontend side, Blob slicing is the foundation.

Compressing Images with Canvas and Blob

Image uploads are another place where Blob is extremely useful.

Uploading raw images directly is often wasteful. A phone photo can easily be several megabytes, even when the UI only needs a small avatar or preview.

A common frontend approach is:

  1. Load the image.
  2. Draw it to a canvas.
  3. Resize it.
  4. Export the canvas as a Blob.
  5. Upload the compressed Blob.

First, create a helper for loading images:

ts
function loadImageFromFile(file: File): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    const url = URL.createObjectURL(file);
    const image = new Image();

    image.onload = () => {
      URL.revokeObjectURL(url);
      resolve(image);
    };

    image.onerror = () => {
      URL.revokeObjectURL(url);
      reject(new Error('Failed to load image'));
    };

    image.src = url;
  });
}

Then calculate dimensions while preserving aspect ratio:

ts
function fitInsideBox(
  originalWidth: number,
  originalHeight: number,
  maxWidth: number,
  maxHeight: number
) {
  let width = originalWidth;
  let height = originalHeight;

  if (width > maxWidth) {
    height = Math.round((height * maxWidth) / width);
    width = maxWidth;
  }

  if (height > maxHeight) {
    width = Math.round((width * maxHeight) / height);
    height = maxHeight;
  }

  return { width, height };
}

Now compress the image:

ts
type ImageCompressionOptions = {
  maxWidth?: number;
  maxHeight?: number;
  quality?: number;
  outputType?: string;
};

async function compressImageFile(
  file: File,
  options: ImageCompressionOptions = {}
): Promise<Blob> {
  const {
    maxWidth = 1600,
    maxHeight = 1000,
    quality = 0.82,
    outputType = 'image/jpeg',
  } = options;

  const image = await loadImageFromFile(file);

  const size = fitInsideBox(
    image.width,
    image.height,
    maxWidth,
    maxHeight
  );

  const canvas = document.createElement('canvas');
  canvas.width = size.width;
  canvas.height = size.height;

  const context = canvas.getContext('2d');

  if (!context) {
    throw new Error('Canvas 2D context is not available');
  }

  context.drawImage(image, 0, 0, size.width, size.height);

  return new Promise((resolve, reject) => {
    canvas.toBlob(
      blob => {
        if (!blob) {
          reject(new Error('Image compression failed'));
          return;
        }

        resolve(blob);
      },
      outputType,
      quality
    );
  });
}

Usage:

ts
const compressedAvatar = await compressImageFile(file, {
  maxWidth: 256,
  maxHeight: 256,
  quality: 0.9,
  outputType: 'image/jpeg',
});

const formData = new FormData();
formData.append('avatar', compressedAvatar, 'avatar.jpg');

await fetch('/api/avatar', {
  method: 'POST',
  body: formData,
});

This improves upload speed, reduces storage usage, and keeps the UI more responsive.

Creating File Previews with Blob URLs

Blob URLs make local previews simple.

You do not need to upload a file to preview it. The browser can create a temporary object URL and use that as a source for an image, video, audio element, or link.

Image preview:

ts
function previewImage(file: File, image: HTMLImageElement) {
  const url = URL.createObjectURL(file);

  image.onload = () => {
    URL.revokeObjectURL(url);
  };

  image.src = url;
}

Video preview:

ts
function previewVideo(file: File, video: HTMLVideoElement) {
  const url = URL.createObjectURL(file);

  video.onloadedmetadata = () => {
    URL.revokeObjectURL(url);
  };

  video.src = url;
}

Audio preview:

ts
function previewAudio(file: File, audio: HTMLAudioElement) {
  const url = URL.createObjectURL(file);

  audio.onloadedmetadata = () => {
    URL.revokeObjectURL(url);
  };

  audio.src = url;
}

Text preview needs a different approach because text must be read:

ts
async function previewText(file: File, container: HTMLElement) {
  const text = await file.text();

  const pre = document.createElement('pre');
  pre.textContent =
    text.length > 10_000
      ? `${text.slice(0, 10_000)}\n\n...preview truncated`
      : text;

  container.replaceChildren(pre);
}

For a real app, it helps to wrap preview logic in a class.

ts
class FilePreviewer {
  constructor(private container: HTMLElement) {}

  async preview(file: File) {
    this.container.replaceChildren();

    if (file.type.startsWith('image/')) {
      return this.renderImage(file);
    }

    if (file.type.startsWith('video/')) {
      return this.renderVideo(file);
    }

    if (file.type.startsWith('audio/')) {
      return this.renderAudio(file);
    }

    if (file.type.startsWith('text/') || file.type === 'application/json') {
      return this.renderText(file);
    }

    return this.renderUnsupported(file);
  }

  private renderImage(file: File) {
    const image = document.createElement('img');
    image.style.maxWidth = '100%';

    const url = URL.createObjectURL(file);

    image.onload = () => {
      URL.revokeObjectURL(url);
    };

    image.src = url;
    this.container.appendChild(image);
  }

  private renderVideo(file: File) {
    const video = document.createElement('video');
    video.controls = true;
    video.style.maxWidth = '100%';

    const url = URL.createObjectURL(file);

    video.onloadedmetadata = () => {
      URL.revokeObjectURL(url);
    };

    video.src = url;
    this.container.appendChild(video);
  }

  private renderAudio(file: File) {
    const audio = document.createElement('audio');
    audio.controls = true;

    const url = URL.createObjectURL(file);

    audio.onloadedmetadata = () => {
      URL.revokeObjectURL(url);
    };

    audio.src = url;
    this.container.appendChild(audio);
  }

  private async renderText(file: File) {
    const text = await file.text();

    const pre = document.createElement('pre');
    pre.textContent =
      text.length > 10_000
        ? `${text.slice(0, 10_000)}\n\n...preview truncated`
        : text;

    this.container.appendChild(pre);
  }

  private renderUnsupported(file: File) {
    const message = document.createElement('p');
    message.textContent = `Preview is not available for ${file.name}.`;
    this.container.appendChild(message);
  }
}

This keeps preview logic centralized and easier to maintain.

Exporting CSV, JSON, and Text Files

Blob is also ideal for export features.

A basic download helper:

ts
function triggerBlobDownload(blob: Blob, filename: string) {
  const url = URL.createObjectURL(blob);

  const link = document.createElement('a');
  link.href = url;
  link.download = filename;
  link.style.display = 'none';

  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);

  setTimeout(() => {
    URL.revokeObjectURL(url);
  }, 0);
}

Export JSON

ts
function exportJson(data: unknown, filename = 'data.json') {
  const blob = new Blob(
    [JSON.stringify(data, null, 2)],
    { type: 'application/json' }
  );

  triggerBlobDownload(blob, filename);
}

Export CSV

CSV generation needs escaping. Commas, quotes, and new lines must be handled correctly.

ts
function escapeCsvCell(value: unknown) {
  const text = String(value ?? '');

  if (
    text.includes(',') ||
    text.includes('"') ||
    text.includes('\n')
  ) {
    return `"${text.replace(/"/g, '""')}"`;
  }

  return text;
}

function exportCsv(
  rows: Record<string, unknown>[],
  filename = 'report.csv'
) {
  if (rows.length === 0) {
    triggerBlobDownload(
      new Blob([''], { type: 'text/csv;charset=utf-8' }),
      filename
    );
    return;
  }

  const headers = Object.keys(rows[0]);

  const csv = [
    headers.map(escapeCsvCell).join(','),
    ...rows.map(row =>
      headers
        .map(header => escapeCsvCell(row[header]))
        .join(',')
    ),
  ].join('\n');

  const bom = '\uFEFF';

  const blob = new Blob([bom + csv], {
    type: 'text/csv;charset=utf-8',
  });

  triggerBlobDownload(blob, filename);
}

Usage:

ts
exportCsv(
  [
    { date: '2026-05-19', product: 'Pro Plan', revenue: 120 },
    { date: '2026-05-20', product: 'Team Plan', revenue: 340 },
  ],
  'sales-report.csv'
);

Adding the BOM helps spreadsheet applications detect UTF-8 correctly, especially when the file contains non-English text.

Blob URL Memory Leaks

Blob URLs are powerful, but they are not free.

Every call to URL.createObjectURL() creates a browser-managed reference.

If you never call URL.revokeObjectURL(), those references can stay alive longer than necessary.

Bad example:

ts
function renderPreview(file: File) {
  const url = URL.createObjectURL(file);

  const image = document.createElement('img');
  image.src = url;

  document.body.appendChild(image);

  // URL is never revoked
}

This may not matter for one image. It matters a lot in a long-running app where users preview dozens or hundreds of files.

A Simple Blob URL Registry

For Blob-heavy applications, centralized memory management helps.

ts
class BlobUrlRegistry {
  private urls = new Set<string>();

  create(blob: Blob) {
    const url = URL.createObjectURL(blob);
    this.urls.add(url);
    return url;
  }

  revoke(url: string) {
    URL.revokeObjectURL(url);
    this.urls.delete(url);
  }

  revokeAll() {
    for (const url of this.urls) {
      URL.revokeObjectURL(url);
    }

    this.urls.clear();
  }

  get size() {
    return this.urls.size;
  }
}

Usage:

ts
const registry = new BlobUrlRegistry();

const previewUrl = registry.create(file);

image.src = previewUrl;

image.onload = () => {
  registry.revoke(previewUrl);
};

Before leaving a page or destroying a component:

ts
registry.revokeAll();

This pattern is especially useful in:

  • image editors
  • upload managers
  • file dashboards
  • design tools
  • long-running single-page applications

Component-Level Cleanup Example

Here is a small preview component pattern:

ts
class SafeImagePreview {
  private currentUrl: string | null = null;

  constructor(
    private container: HTMLElement,
    private registry: BlobUrlRegistry
  ) {}

  show(file: File) {
    this.cleanup();

    const image = document.createElement('img');
    image.style.maxWidth = '100%';

    const url = this.registry.create(file);
    this.currentUrl = url;

    image.src = url;

    image.onerror = () => {
      this.cleanup();
    };

    this.container.replaceChildren(image);
  }

  cleanup() {
    if (this.currentUrl) {
      this.registry.revoke(this.currentUrl);
      this.currentUrl = null;
    }

    this.container.replaceChildren();
  }
}

The key idea is that the component owns its Blob URL and releases it when it no longer needs it.

Practical Best Practices

Blob is easy to use, but a few rules make a big difference.

Always Set the MIME Type

Avoid this:

ts
new Blob([data]);

Prefer this:

ts
new Blob([data], {
  type: 'application/json',
});

The browser, server, and user all benefit from correct type information.

Avoid Data URLs for Large Content

Data URLs are fine for tiny examples. For generated files and large exports, Blob URLs are usually safer.

Use Chunking for Large Files

Do not read huge files all at once unless you are sure the file size is controlled.

Use:

ts
file.slice(start, end);

to process files incrementally.

Compress Images Before Upload

Most users do not need to upload a 6MB avatar. Compressing images on the client can improve upload speed and reduce server costs.

Revoke Object URLs

Every createObjectURL() should have a cleanup plan.

Centralize Cleanup in Complex Apps

If your app creates many previews, generated downloads, or editing history states, use a registry or manager to track URLs.

When Blob Is the Right Tool

Blob is a good fit when you need to:

  • generate files in the browser
  • preview local files before upload
  • upload binary content
  • process large files in smaller pieces
  • compress images
  • export CSV, JSON, or text
  • manage binary data without server round trips

It is not a replacement for all file processing. Very heavy video editing, huge dataset transformations, or complex document processing may still require server-side infrastructure or Web Workers.

But for many frontend workflows, Blob is the right foundation.

Final Thoughts

The Blob API looks small, but it powers a large part of modern browser file handling.

It helps with uploads, previews, downloads, exports, media processing, binary data, and memory management.

The most important lesson is not just how to create a Blob. The real skill is knowing how to manage the lifecycle around it.

Create data intentionally.

Use the right MIME type.

Process large files in chunks.

Prefer Blob URLs over giant data URLs.

Clean up object URLs when you are done.

Once those habits become natural, frontend file handling becomes faster, safer, and much easier to maintain.