Memory retention scales dramatically with raster image payload when PDF encryption is enabled
jspdf-encryption-raster-memory-repro.zip
Description
Hi, I found a memory-retention issue when using jsPDF with raster images and PDF encryption.
The problem first appeared in an application I was working on, so I reduced it step by step to make sure it was not caused by my framework or PDF wrapper. After removing Vue, html2canvas, html2pdf.js, and the application renderer, I can still reproduce the same behavior with raw jsPDF.
When encryption is disabled, JPEG-based PDFs release almost all of the memory after generation.
When encryption.userPassword is enabled, a much larger amount of JavaScript heap stays retained after:
pdf.output('blob')finishes- references to the jsPDF instance and image data are cleared
- the source canvas is reset
- Chromium garbage collection is forced
- another 2 seconds are allowed to pass, followed by another forced GC
The retained memory also grows with the number of raster pages.
Environment
- jsPDF:
4.2.1 - Browser: Chromium
- OS: Windows
- Test runner: Playwright
- Output API:
pdf.output('blob') - Encryption option:
encryption: { userPassword: 'phase321' }
Reproduction summary
The attached reproduction:
- loads
jspdf/dist/jspdf.umd.min.js - creates a synthetic 1191 x 1461 canvas
- converts it to JPEG or PNG
- adds the same raster to N PDF pages with
addImage() - compares encryption OFF vs ON
- calls
pdf.output('blob') - clears references and zeroes the source canvas
- forces GC with Chromium CDP
- measures JS heap again after 2 seconds
Results
JPEG Data URL scaling
| Pages | Encryption OFF retained after 2s | Encryption ON retained after 2s |
|---|---|---|
| 5 | ~0.4 MB retained | ~18.1 MB retained |
| 10 | ~0.4 MB retained | ~35.9 MB retained |
| 15 | ~0.4 MB retained | ~53.6 MB retained |
| 20 | ~0.4 MB retained | ~71.3 MB retained |
| 25 | ~0.4 MB retained | ~89.0 MB retained |
Generated PDF sizes are effectively the same between OFF and ON for each page count. For the 25-page JPEG case the PDF is about 4.44 MB in both cases.
25-page representation controls
| Image input | Encryption OFF retained | Encryption ON retained |
|---|---|---|
| JPEG Data URL | ~0.45 MB | ~89.0 MB |
| JPEG Uint8Array | ~4.9 MB | ~93.5 MB |
| PNG Data URL | ~125.6 MB | ~2614.4 MB |
The PNG case is especially severe: the generated PDF is about 124.47 MB, while the encrypted run retains about 2.55 GiB of JS heap after the forced-GC + 2-second measurement window.
Expected behavior
After PDF serialization finishes and caller-owned references are released, enabling encryption should not cause heap retention to remain roughly proportional to the embedded raster payload.
Some temporary extra allocation during encryption/serialization is expected, but the retained heap should become reclaimable after output completes and references are dropped.
Actual behavior
With encryption disabled, JPEG runs return to a small retained baseline.
With userPassword encryption enabled, retained heap grows almost linearly
with page count / raster payload and remains after repeated forced GC.
The same behavior also reproduces when the JPEG is passed as a Uint8Array, so it does not appear to be specific to Data URLs.
Additional observations
-This was originally observed in an application renderer, then isolated to this raw jsPDF reproduction. -I am reporting this as memory retention rather than a confirmed permanent memory leak, because I have only measured the heap after forced GC and a 2-second wait.
Notes
- The effect reproduces with jsPDF directly.
- It is not specific to a framework or wrapper.
- JPEG Data URL and JPEG Uint8Array both reproduce it.
- PNG makes the effect dramatically larger.
- PDF generation itself succeeds and Blob sizes are valid.
- This was originally observed in an application renderer, then isolated to this raw jsPDF reproduction.
Minimal reproducible example
I created a small standalone reproduction that uses only jsPDF.
It does not use:
- Vue
- html2canvas
- html2pdf.js
- any PDF wrapper
The reproduction:
- Creates a synthetic browser canvas
- Converts it to JPEG or PNG
- Creates a jsPDF document
- Adds the image to multiple pages using
addImage() - Runs the same test with encryption OFF and ON
- Calls
pdf.output('blob') - Clears the references
- Forces Chromium GC
- Measures JS heap again after 2 seconds
I have attached the reproduction files.
To run it:
npm install
npx playwright install chromium
npm test
The test prints retained heap for security OFF and ON and writes
results.json.
If this is expected behavior, or if there is a recommended way to avoid retaining the encrypted raster data after serialization, I would appreciate guidance.
I can also provide additional heap measurements or test a proposed fix if that would help.
Source: parallax/jsPDF