Skip to content
>_ TrueFileSize.com
TrueFileSize Editorial··Updated ·8 min read

5 Browser File API Gotchas: Memory, Slicing & Metadata

File upload code often works with one small fixture and fails when the test matrix includes many previews, awkward chunk boundaries, repeated selections, or a file too large to buffer comfortably. The useful response is not a browser-specific anecdote; it is a set of assertions that reproduce the risky behavior.

Gotcha 1: data URLs copy the whole file

FileReader.readAsDataURL() reads the complete file and represents it as a base64 string. Base64 adds roughly one third to the encoded payload before accounting for the original File, decoded image data, DOM references, and application state.

function previewWithDataUrl(file, img) {
  const reader = new FileReader();
  reader.addEventListener('load', () => {
    img.src = String(reader.result);
  });
  reader.readAsDataURL(file);
}

For local previews, a blob URL avoids embedding a second base64 representation in the DOM. Every object URL should be revoked when the resource is no longer accessible. MDN specifically warns against revoking immediately after an image loads when users still need to save or open that image.

function mountPreview(file, container) {
  const objectUrl = URL.createObjectURL(file);
  const img = document.createElement('img');
  img.src = objectUrl;
  container.appendChild(img);

  return function removePreview() {
    img.remove();
    URL.revokeObjectURL(objectUrl);
  };
}

Test cleanup by repeatedly adding and removing previews while watching the browser's memory tools. Do not publish a universal memory limit: available memory and process termination behavior vary by browser, operating system, device, and the rest of the page.

Gotcha 2: Blob.slice() uses an exclusive end offset

According to the File API behavior documented by MDN, blob.slice(start, end) includes start and excludes end. The final chunk is normally smaller when the file size is not an exact multiple of the chunk size; that is expected, not a Safari-specific bug.

async function uploadInChunks(file, uploadChunk) {
  const chunkSize = 5 * 1024 * 1024;
  let offset = 0;
  let index = 0;

  while (offset < file.size) {
    const end = Math.min(offset + chunkSize, file.size);
    const chunk = file.slice(offset, end);
    const expectedSize = end - offset;

    if (chunk.size !== expectedSize) {
      throw new Error(
        `Unexpected chunk size at ${offset}: expected ${expectedSize}, got ${chunk.size}`,
      );
    }

    await uploadChunk({ chunk, index, start: offset, endExclusive: end });
    offset = end;
    index += 1;
  }
}

The server should verify offsets, expected total size, and a final checksum. Test files at chunkSize - 1, chunkSize, chunkSize + 1, and several non-round sizes. That catches boundary errors without relying on an undocumented browser claim.

Gotcha 3: lastModified is not a capture date

File.lastModified is the file's reported modification time in milliseconds. MDN states that when a modification date is not known, the current date is returned. It is therefore unsuitable as a unique identifier or as proof of when a photo was captured.

function fileFingerprintHint(file) {
  return {
    name: file.name,
    size: file.size,
    type: file.type,
    lastModified: file.lastModified,
  };
}

The combined fields above are useful for UI hints but can still collide. Use a content hash when identity matters. For image capture time, read EXIF metadata when present, while remembering that metadata can be missing or edited.

Gotcha 4: selecting the same file may not produce a new change

The change event represents a changed input value. Upload interfaces that support retrying the same file should clear the input after taking a reference to the selected File.

const input = document.querySelector('input[type="file"]');

input.addEventListener('change', async (event) => {
  const selectedFile = event.target.files?.[0];
  if (!selectedFile) return;

  event.target.value = '';
  await processFile(selectedFile);
});

Add an automated UI test that selects the same fixture twice and asserts that the application handles both attempts. This verifies the product behavior directly instead of assuming every browser exposes the same picker details.

Gotcha 5: arrayBuffer() reads the complete Blob

Blob.arrayBuffer() resolves with an ArrayBuffer containing the entire Blob. For large inputs, this creates a whole-file allocation. Blob.stream() returns a ReadableStream, allowing incremental processing where the downstream API supports it.

async function consumeBlob(blob, consumeChunk) {
  const reader = blob.stream().getReader();

  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      await consumeChunk(value);
    }
  } finally {
    reader.releaseLock();
  }
}

Streaming does not automatically make every operation incremental. For example, crypto.subtle.digest() requires the complete input and does not provide streaming hashing. Use a vetted incremental implementation or calculate the hash on a streaming server path when large-file checksums are required.

Do not publish a device-RAM-to-safe-file-size table without a reproducible lab setup. A safer test records the browser version, device, available memory conditions, fixture size, operation, and whether the page remained responsive.

A reproducible test matrix

| Risk | Fixtures and assertion | |---|---| | Preview cleanup | Add and remove many JPG samples; verify every object URL is revoked | | Chunk boundaries | Slice exact-size files at N-1, N, and N+1; assert offsets and total bytes | | Metadata assumptions | Compare files with missing or deliberately changed timestamps | | Same-file retry | Select the same fixture twice in Playwright and assert two attempts | | Whole-file allocation | Compare arrayBuffer() and stream() on the target devices using the same file |

Start with 1MB and 100MB download tests, then increase size only when the product needs that range. Record measurements from the actual target environment rather than copying a generic threshold.

Sources

See also: Fix Corrupt File Upload Errors · Large File Upload Performance · Testing File Upload with Playwright