Blog
performance
5 min read

Create, Click, Revoke: Blob Downloads with Object URLs (Next.js Boundaries)

One-line takeaway: For Blob downloads, create an object URL, trigger the download, then immediately revoke it to reduce accumulation risk. In Next.js, the outcome depends on pinning browser-only APIs to Client Components.

One-line takeaway: For Blob downloads, the safest default is to create an object URL with URL.createObjectURL(), trigger the download, then immediately release it with URL.revokeObjectURL(). Downloads aren’t “done once.” Repetition is where things break. UX, performance, and stability split right here. The point is simple: create it, use it, then drop it.

Background / Problem

A common frontend flow for saving generated data (CSV/logs/ad-hoc reports) is: “data → Blob (a browser-native container for file-like binary data) → object URL (a temporary blob: URL) → click an <a download> element.” The question isn’t whether it downloads—it’s whether the cleanup is guaranteed when users repeat it. If you don’t revoke the object URL, resources can accumulate (this may vary by environment/policy).

References: MDN – URL.createObjectURL(), MDN – URL.revokeObjectURL(), MDN – blob: URLs (Memory management)

Core Concepts

Pin the flow (Mermaid)

다이어그램 불러오는 중...

→ What changes / expected result: The trigger and the revoke become one flow, reducing accumulation risk across repeated downloads.

Next.js boundary

document, window, and URL.createObjectURL() are browser-only APIs. In Next.js, you need to pin execution to a Client Component (a component marked with 'use client' so it runs in the browser).

References: Next.js – Server and Client Components, Next.js – use client Directive

Approach

  1. Create a Blob (why: represent the download payload as a standard object) → expected result: type/encoding is explicit and the rest of the flow is simpler.
  2. Create an object URL and click <a download> (why: reuse native browser download UX) → expected result: a natural download without extra libraries.
  3. Revoke with URL.revokeObjectURL() (why: prevent accumulation across repeats) → expected result: repeatable downloads with a clear cleanup point.

Implementation (Code)

1) “Blob → Download” utility

javascript
export function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob);

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

  document.body.appendChild(a);
  a.click();
  a.remove();

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

→ What changes / expected result: Each call triggers a download, then promptly revokes the object URL to reduce repeat-run risk.

2) Trigger from a Next.js Client Component

typescript
'use client';

import { downloadBlob } from './downloadBlob';

export default function ExportButton() {
  const onClick = () => {
    const blob = new Blob(["hello\nworld\n"], { type: "text/plain;charset=utf-8" });
    downloadBlob(blob, "example.txt");
  };

  return (
    <button type="button" onClick={onClick}>
      Download example.txt
    </button>
  );
}

→ What changes / expected result: No SSR collisions—browser-only APIs run on the client, and clicking reliably triggers a download.

3) Download a fetch() response

javascript
import { downloadBlob } from "./downloadBlob";

export async function downloadFromUrl(fileUrl, filename) {
  const res = await fetch(fileUrl);
  if (!res.ok) throw new Error(`Download failed:${res.status}`);
  const blob = await res.blob();
  downloadBlob(blob, filename);
}

→ What changes / expected result: Remote responses funnel into the same utility, keeping implementation consistent.

Verification Checklist

The browser download UI is triggered.
Repeating the action doesn’t cause excessive tab memory growth.
The saved filename matches expectations.
The Client Component boundary is clear (no document is not defined-style errors).
(Remote files) fetch succeeds under required CORS/auth constraints.

Common Mistakes / FAQ

  • Q. What if I don’t call revokeObjectURL()? → A. You can’t assume automatic cleanup is immediate. For repeat-download screens, make revoke the default. Reference: MDN – URL.revokeObjectURL()
  • Q. Why setTimeout(..., 0)? → A. Downloads are browser-internal; it’s hard to hook a reliable “download started” event. Many implementations revoke on the next tick after clicking.
  • Q. Does <a download> work for any URL? → A. Constraints can vary by environment/policy. If you need predictable forced downloads for external URLs, a server Content-Disposition approach is often more reliable. Reference: MDN – <a> download attribute

Alternative / Comparison: Serve an attachment from the server

If you want to avoid loading large Blobs in client memory—or you need server-side control for policy/auth/logging—return an attachment response from a Route Handler using Content-Disposition.

References: Next.js – Route Handlers, MDN – Content-Disposition header

typescript
// app/api/export/route.ts
export async function GET() {
  const body = "id,name\n1,Alice\n2,Bob\n";

  return new Response(body, {
    headers: {
      "Content-Type": "text/csv; charset=utf-8",
      "Content-Disposition": 'attachment; filename="export.csv"',
    },
  });
}

→ What changes / expected result: The browser treats it as a download response, enabling a download UX without object URLs.

Summary (3–5 lines)

Blob downloads are best treated as a loop: create an object URL, trigger <a download>, then revoke immediately. revokeObjectURL is the cleanup point that reduces repeat-run risk. In Next.js, isolate browser-only APIs inside Client Components. If you need stricter control, serve downloads as attachments from Route Handlers.

Conclusion

The goal isn’t “works once.” It’s “stays safe when repeated.” Create, click, revoke. Make that your default and the download flow stays stable in Next.js.

References

Related Posts