Memory leaks when navigating between documents -- blob URLs never revoked #202

Closed
opened 2026-04-07 11:08:35 +02:00 by marcel · 1 comment
Owner

Context

The frontend creates blob URLs via URL.createObjectURL() to display document file previews (PDFs, images). These blob URLs are never cleaned up with URL.revokeObjectURL().

Affected files:

  • frontend/src/routes/documents/[id]/+page.svelte (line 40)
  • frontend/src/routes/enrich/[id]/+page.svelte (line 42)

A grep for revokeObjectURL across the entire frontend/src/ directory returns zero results.

Impact

Each navigation to a document page creates a new blob URL that holds the entire file contents in memory. For a typical PDF (1-5 MB), navigating through 20 documents leaks 20-100 MB of browser memory that is never freed until the tab is closed.

The enrich workflow is especially affected since users navigate through documents sequentially, accumulating blob URLs with every page visit.

Root Cause

Both loadFile() functions call URL.createObjectURL(blob) and assign the result to fileUrl, but:

  1. When the $effect re-fires (e.g. navigating to a different document), the old blob URL is overwritten without being revoked.
  2. When the component is destroyed (navigating away), no cleanup runs.

Proposed Fix

Add cleanup in two places:

  1. In loadFile() -- revoke the previous URL before creating a new one:
async function loadFile(id: string) {
    isLoading = true;
    fileError = '';
    if (fileUrl) URL.revokeObjectURL(fileUrl);
    fileUrl = '';
    // ... rest unchanged
}
  1. On component destroy -- revoke via onDestroy or $effect cleanup:
import { onDestroy } from 'svelte';

onDestroy(() => {
    if (fileUrl) URL.revokeObjectURL(fileUrl);
});

Both files need the same fix.

## Context The frontend creates blob URLs via `URL.createObjectURL()` to display document file previews (PDFs, images). These blob URLs are never cleaned up with `URL.revokeObjectURL()`. **Affected files:** - `frontend/src/routes/documents/[id]/+page.svelte` (line 40) - `frontend/src/routes/enrich/[id]/+page.svelte` (line 42) A grep for `revokeObjectURL` across the entire `frontend/src/` directory returns zero results. ## Impact Each navigation to a document page creates a new blob URL that holds the entire file contents in memory. For a typical PDF (1-5 MB), navigating through 20 documents leaks 20-100 MB of browser memory that is never freed until the tab is closed. The enrich workflow is especially affected since users navigate through documents sequentially, accumulating blob URLs with every page visit. ## Root Cause Both `loadFile()` functions call `URL.createObjectURL(blob)` and assign the result to `fileUrl`, but: 1. When the `$effect` re-fires (e.g. navigating to a different document), the old blob URL is overwritten without being revoked. 2. When the component is destroyed (navigating away), no cleanup runs. ## Proposed Fix Add cleanup in two places: 1. **In `loadFile()`** -- revoke the previous URL before creating a new one: ```typescript async function loadFile(id: string) { isLoading = true; fileError = ''; if (fileUrl) URL.revokeObjectURL(fileUrl); fileUrl = ''; // ... rest unchanged } ``` 2. **On component destroy** -- revoke via `onDestroy` or `$effect` cleanup: ```typescript import { onDestroy } from 'svelte'; onDestroy(() => { if (fileUrl) URL.revokeObjectURL(fileUrl); }); ``` Both files need the same fix.
marcel added the bug label 2026-04-07 11:08:40 +02:00
Author
Owner

Already fixed as part of the createFileLoader hook refactor.

What's in place:

  • frontend/src/lib/hooks/useFileLoader.svelte.ts — the hook revokes the previous blob URL before creating a new one (on every loadFile() call) and exposes a destroy() method that revokes the current URL on component teardown.
  • documents/[id]/+page.svelte — calls onDestroy(() => fileLoader.destroy())
  • DocumentEditLayout.svelte (covers /documents/[id]/edit and /documents/new) — same pattern
  • enrich/[id]/+page.svelte — no longer uses file loading directly; the concern from the original report is gone

Full test coverage in src/lib/hooks/__tests__/useFileLoader.svelte.test.ts (5 tests, all green):

  • sets fileUrl after successful fetch
  • sets fileError on non-ok response
  • revokes previous URL before creating a new one
  • revokes URL on destroy
  • does not revoke when no URL has been set

No createObjectURL calls remain outside the hook.

Already fixed as part of the `createFileLoader` hook refactor. **What's in place:** - `frontend/src/lib/hooks/useFileLoader.svelte.ts` — the hook revokes the previous blob URL before creating a new one (on every `loadFile()` call) and exposes a `destroy()` method that revokes the current URL on component teardown. - `documents/[id]/+page.svelte` — calls `onDestroy(() => fileLoader.destroy())` - `DocumentEditLayout.svelte` (covers `/documents/[id]/edit` and `/documents/new`) — same pattern - `enrich/[id]/+page.svelte` — no longer uses file loading directly; the concern from the original report is gone Full test coverage in `src/lib/hooks/__tests__/useFileLoader.svelte.test.ts` (5 tests, all green): - sets fileUrl after successful fetch - sets fileError on non-ok response - revokes previous URL before creating a new one - revokes URL on destroy - does not revoke when no URL has been set No `createObjectURL` calls remain outside the hook.
Sign in to join this conversation.
No Label bug
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#202