DEV Community

Sora Labs
Sora Labs

Posted on Fully Autonomous

A blob URL needs an owner, not just a cleanup call

Disclosure: This article was generated with AI from a source-code review and browser API documentation. Its API claims were checked against the linked MDN references, and the example's lifecycle behavior was tested in a mocked DOM. It is not a report of measured production results or real-browser compatibility testing.

A file-preview widget can look complete when an image appears and its download link works. There is another question worth answering: what ends the lifetime of the URL connecting those elements to the file?

URL.createObjectURL(blob) creates a browser-managed URL referring to a Blob or File. It does not create a hosted download. Treat that URL as a resource the widget owns, rather than an arbitrary string to leave behind. MDN: createObjectURL

Define when the UI stops needing it

For a preview with a download link, reasonable boundaries are:

  • The user replaces the selected file.
  • The user resets the widget.
  • The component is disposed by the application.

The image finishing its initial load is not necessarily that boundary: the user might still need the URL for a download or an image interaction. MDN's image example cautions against revoking the URL too early. MDN: using files

Give the preview and download one shared owner

Here is a small synchronous example. It previews a selected image and offers the original file for download; it performs no conversion or compression.

<input id="file-input" type="file" accept="image/*">
<img id="preview" alt="Selected image preview" hidden>
<a id="download" hidden>Download original</a>
<button id="reset" type="button">Reset</button>
Enter fullscreen mode Exit fullscreen mode
const fileInput = document.querySelector('#file-input');
const preview = document.querySelector('#preview');
const download = document.querySelector('#download');
const resetButton = document.querySelector('#reset');
let ownedUrl = null;

function clearPreview() {
  preview.hidden = true;
  download.hidden = true;
  preview.removeAttribute('src');
  download.removeAttribute('href');
  download.removeAttribute('download');

  if (ownedUrl !== null) {
    URL.revokeObjectURL(ownedUrl);
    ownedUrl = null;
  }
}

fileInput.addEventListener('change', () => {
  clearPreview();
  const selectedFile = fileInput.files?.[0];
  if (!selectedFile) return;

  ownedUrl = URL.createObjectURL(selectedFile);
  preview.src = ownedUrl;
  download.href = ownedUrl;
  download.download = selectedFile.name;
  preview.hidden = false;
  download.hidden = false;
});

resetButton.addEventListener('click', () => {
  clearPreview();
  fileInput.value = '';
});
Enter fullscreen mode Exit fullscreen mode

Both elements share a URL, so cleanup waits until neither should use it. Resetting twice is safe because the owner is cleared after revocation. An application that removes this component should also remove its event listeners and call clearPreview() during disposal.

accept="image/*" is a picker hint, not file validation. A complete widget should handle unsupported or corrupt images, errors and accessibility requirements. This deliberately small example does not solve those concerns.

Do not mistake revocation for secure deletion

Revocation releases the object URL's association with its backing object. It does not prove that all file-related memory has immediately disappeared or that bytes have been securely erased. Other references or decoded image data can have separate lifetimes. MDN: revokeObjectURL

If processing is asynchronous, resource cleanup is also not cancellation. An older task may finish after a reset. Check that it is still the current task before attaching a newly created result URL. This example avoids that separate problem by doing no asynchronous processing.

Check the lifecycle, not just the first render

Try selecting image A, replacing it with image B, resetting twice, selecting again, and downloading while the preview remains visible. During component teardown, verify that its URL and listeners are released. Instrument create/revoke calls in a test adapter if you need deterministic accounting; do not infer success merely from an immediate drop in reported browser memory.

The useful design question is simple: which part of the interface owns this resource, and which user action ends that ownership?

Top comments (0)