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
- 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.
- Create an object URL and click
<a download>(why: reuse native browser download UX) → expected result: a natural download without extra libraries. - Revoke with
URL.revokeObjectURL()(why: prevent accumulation across repeats) → expected result: repeatable downloads with a clear cleanup point.
Implementation (Code)
1) “Blob → Download” utility
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
'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
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
document is not defined-style errors).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 serverContent-Dispositionapproach is often more reliable. Reference: MDN –<a>downloadattribute
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
// 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.