Use when building PDF viewers that load multiple documents, navigate between pages, or run for extended periods. Prevents the #1 production PDF.js issue: memory leaks from unreleased PDFDocumentProxy, uncleaned pages, and orphaned canvas contexts. Covers destroy/cleanup lifecycle, canvas reuse, render task cancellation, blob URL revocation, and large PDF handling strategies. Keywords: memory leak, destroy, cleanup, PDFDocumentProxy, canvas, blob URL, garbage collection, browser tab slow, PDF viewer uses too much memory, page gets slower, out of memory.
Use when building PDF viewers that load multiple documents, navigate between pages, or run for extended periods. Prevents the #1 production PDF.js issue: memory leaks from unreleased PDFDocumentProxy, uncleaned pages, and orphaned canvas contexts. Covers destroy/cleanup lifecycle, canvas reuse, render task cancellation, blob URL revocation, and large PDF handling strategies. Keywords: memory leak, destroy, cleanup, PDFDocumentProxy, canvas, blob URL, garbage collection, browser tab slow, PDF viewer uses too much memory, page gets slower, out of memory.
license
MIT
compatibility
Designed for Claude Code. Requires pdfjs-dist 5.x.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
pdfjs-core-memory-management
Quick Reference
Resource Lifecycle Overview
Resource
Acquire
Release
When to Release
PDFDocumentProxy
getDocument().promise
.destroy()
When switching documents or unmounting viewer
PDFPageProxy
pdfDoc.getPage(n)
.cleanup()
When page scrolls off-screen or is evicted from pool
RenderTask
page.render(params)
.cancel()
BEFORE destroying page, re-rendering, or unmounting
Canvas context
canvas.getContext('2d')
Reset dimensions + clearRect
Before reuse or removal from DOM
TextLayer
new TextLayer(params)
.cancel()
Before destroying the page container
AnnotationLayer
annotationLayer.render()
.cancel()
Before destroying the page container
Blob URL
URL.createObjectURL(blob)
URL.revokeObjectURL(url)
Immediately after passing to getDocument()
Event listeners
addEventListener()
removeEventListener()
On destroy/unmount
Destroy Order (CRITICAL)
ALWAYS destroy resources in this exact order:
1. Cancel all active RenderTasks
2. Cancel TextLayer and AnnotationLayer instances
3. Call PDFPageProxy.cleanup() on all loaded pages
4. Clear and reset canvas elements
5. Remove event listeners
6. Call PDFDocumentProxy.destroy()
7. Revoke any blob URLs
8. Remove DOM elements
Violating this order causes errors. Destroying a PDFDocumentProxy while a RenderTask is active throws an unhandled exception in the worker.
Critical Warnings
NEVER call PDFDocumentProxy.destroy() without first cancelling all active RenderTask instances -- the worker throws unhandled exceptions for in-flight operations.
NEVER keep references to PDFPageProxy objects after calling PDFDocumentProxy.destroy() -- they become invalid and any method call throws.
NEVER create a new canvas for each page render in a single-page viewer -- reuse the same canvas and clear it. Creating canvases without removing old ones leaks GPU memory.
NEVER load a PDF from a blob URL without revoking it after getDocument() resolves -- each unreleased blob URL holds the entire PDF binary in memory.
NEVER rely on garbage collection to clean up PDF.js resources -- the worker thread and internal caches are NOT released by GC. ALWAYS call destroy() explicitly.
ALWAYS cancel RenderTask before starting a new render on the same canvas -- concurrent renders corrupt canvas state.
ALWAYS clean up PDF.js resources on SPA route changes -- leaving active workers behind is the most common memory leak in production PDF viewers.
ALWAYS use a page pool with a maximum size for multi-page viewers -- keeping every visited page in memory crashes long-running sessions.
Decision Tree: Cleanup Strategy
Is the user navigating away from the PDF viewer entirely?
├── YES → Full Cleanup
│ ├── Cancel all RenderTasks
│ ├── Cancel TextLayer/AnnotationLayer
│ ├── Cleanup all PDFPageProxy instances
│ ├── Destroy PDFDocumentProxy
│ ├── Revoke blob URLs
│ └── Remove DOM elements and event listeners
│
└── NO → Is the user switching to a different PDF?
├── YES → Document Swap
│ ├── Cancel active RenderTasks
│ ├── Destroy PREVIOUS PDFDocumentProxy
│ ├── Revoke previous blob URL
│ ├── Clear canvas
│ └── Load new document
│
└── NO → Is the user scrolling/navigating pages?
├── YES → Page Pool Management
│ ├── Use IntersectionObserver for visibility
│ ├── Cancel RenderTask on off-screen pages
│ ├── Call cleanup() on evicted pages
│ ├── Clear off-screen canvases
│ └── Keep pool size under budget (e.g., 5-10 pages)
│
└── NO → Is the user zooming/resizing?
├── YES → Re-render Cleanup
│ ├── Cancel current RenderTask
│ ├── Clear canvas
│ ├── Create new viewport
│ └── Start new render
│
└── NO → No cleanup needed
PDFDocumentProxy.destroy()
The most important cleanup method. Releases the worker thread, internal page cache, and all document data.
destroy() returns a Promise. ALWAYS await it when loading a replacement document to ensure resources are fully released before allocating new ones.
PDFPageProxy.cleanup()
Releases cached rendering data (operator list, image data) for a specific page. The page can be re-fetched via getPage() after cleanup -- it is NOT destroyed, just evicted from cache.
// Cleanup a page that scrolled off-screenfunctionevictPage(page: PDFPageProxy): void {
page.cleanup();
}
Key distinction: cleanup() releases the cached data but the PDFPageProxy object remains valid. You can call render() again later and it will re-fetch data from the worker. Use this for page pooling, NOT for final teardown.
RenderTask.cancel()
ALWAYS cancel active render tasks before any of these operations:
Destroying the page or document
Starting a new render on the same canvas
Removing the canvas from the DOM
Navigating to a different page (single-page viewer)
Setting canvas.width = 0 forces the browser to release the backing store. This is the ONLY reliable way to free canvas GPU memory without removing the element from the DOM.
PDF.js reads the full data during getDocument(). Once the promise resolves, the blob URL is no longer needed. Revoking it immediately releases the duplicate copy held by the URL reference.
IntersectionObserver Page Lifecycle
For multi-page viewers, use IntersectionObserver to manage the render/cleanup cycle: