| name | pdfium-impl-performance |
| description | Use when a pdfium-render program is slow, uses too much memory, or must process many PDFs or pages: web servers rendering PDFs per request, batch jobs, multi-page rendering loops. Prevents the rebind-per-request mistake (binding PDFium on every call instead of once), the false-parallelism mistake (expecting the thread_safe feature to speed work up when it only serializes calls behind a mutex), the segfault-from-unsynchronized-threads mistake, and the per-page allocation waste of rebuilding PdfRenderConfig inside the loop. Covers the bind-once pattern with OnceCell/OnceLock, the thread_safe feature trade-off, process-level parallelism, render-config reuse, and render cost knobs. Keywords: pdfium-render performance, pdfium slow, render PDF fast, bind once, OnceCell, OnceLock, axum_once_cell, thread_safe feature, multi-threading pdfium, parallel PDF processing, pdfium segfault threads, PdfRenderConfig reuse, large PDF memory, pdfium web server, why is pdfium slow, render many pages fast
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires pdfium-render 0.8,0.9. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
pdfium-impl-performance
PDFium is fast at the page level but has three performance traps that dominate
real pdfium-render programs. This skill addresses all three:
- Binding. Loading the PDFium library is expensive. Bind once, reuse forever.
- Parallelism. PDFium is not thread safe. The
thread_safe feature makes
multi-threaded code correct but never faster. Throughput comes from
process-level parallelism.
- Allocation. Rebuilding a
PdfRenderConfig or a bitmap for every page wastes
work in a render loop.
Default API surface: pdfium-render 0.9.x. Version notes for 0.8.x are inline.
The cost model
| Operation | Cost | Rule |
|---|
| Bind the PDFium library | high, one-time | bind once at startup, store in a static |
| Load a document | moderate, per file | unavoidable per document |
Build a PdfRenderConfig | small | build once, reuse across pages |
| Render a page to a bitmap | the real work | the figure to optimize |
| A fresh 2000x2000 bitmap | around 11 ms on WASM (issue #35) | reuse the render config; do not rebuild per page |
Lever 1: bind PDFium once
Binding produces a Box<dyn PdfiumLibraryBindings> and is the most expensive
setup step. Pdfium::default() performs the full bind-with-fallback. Calling
it per web request or per file is the most common performance bug (issue #59).
ALWAYS bind once at process startup and reuse the Pdfium instance. Store it in
a process-wide static.
Async server (axum, tokio)
The verified examples/axum_once_cell.rs pattern stores the instance in a
tokio::sync::OnceCell wrapping a tokio::sync::Mutex:
use pdfium_render::prelude::*;
use tokio::sync::{Mutex, OnceCell};
static PDFIUM: OnceCell<Mutex<Pdfium>> = OnceCell::const_new();
async fn pdfium() -> &'static Mutex<Pdfium> {
PDFIUM
.get_or_init(|| async { Mutex::new(Pdfium::default()) })
.await
}
A request handler then does let guard = pdfium().await.lock().await; and runs
all PDFium work inside the guard scope.
Synchronous program
For a non-async program, std::sync::OnceLock (Rust standard library) holds the
instance without an extra dependency:
use pdfium_render::prelude::*;
use std::sync::OnceLock;
static PDFIUM: OnceLock<Pdfium> = OnceLock::new();
fn pdfium() -> &'static Pdfium {
PDFIUM.get_or_init(Pdfium::default)
}
The once_cell crate's Lazy is an equivalent option. The principle is the
same: one bind for the whole process.
Lever 2: the threading reality
PDFium is NOT thread safe. The crate documentation states it plainly: "Pdfium
makes no guarantees about thread safety and should be assumed not to be thread
safe."
The thread_safe feature (ON by default) "achieves thread safety by locking
access to Pdfium behind a mutex; each thread must acquire exclusive access to
this mutex in order to make any call to Pdfium." Every PDFium call is serialized
as if single-threaded. The documentation is explicit: "This approach offers no
performance benefit."
This produces two hard rules:
- NEVER expect multi-threading with the
thread_safe feature to speed work up.
It guarantees correctness, not throughput.
- NEVER call PDFium from multiple threads with the
thread_safe feature
disabled. Unsynchronized concurrent calls cause random segfaults (issue #12).
For real throughput, the Pdfium authors "specifically recommend that parallel
processing, not multi-threading, be used to process multiple documents
simultaneously." Run multiple OS processes, each with its own Pdfium
instance. See references/examples.md.
Release 0.9.0 implements Send and Sync for all object instances, which makes
sharing documents, pages, and objects across threads compile cleanly. On 0.8.x,
object instances are not Send/Sync and cannot be moved across threads.
Send/Sync changes what compiles; it does not change the no-speedup fact.
Lever 3: reuse the render config
render_with_config takes &PdfRenderConfig by shared reference. Build the
config ONCE, before the page loop, and pass the same reference for every page.
let config = PdfRenderConfig::new()
.set_target_width(2000)
.set_maximum_height(2000);
for page in document.pages().iter() {
let bitmap = page.render_with_config(&config)?;
}
NEVER call PdfRenderConfig::new() inside the loop body. Issue #35 shows that
per-page allocation in the render path is a measurable cost: a fresh 2000x2000
bitmap takes around 11 ms on WASM.
Render cost knobs
The output size is the dominant render cost. Tune it on PdfRenderConfig:
| Knob | Effect |
|---|
set_target_width / set_target_height | scale to a preferred size, aspect kept |
set_maximum_width / set_maximum_height | cap a dimension, never exceed it |
set_fixed_width / set_fixed_height / set_fixed_size | force an exact size (added 0.8.37) |
scale_page_by_factor | scale relative to the natural page size |
use_print_quality | higher fidelity at higher cost; leave off for screen output |
ALWAYS render at the smallest size the output actually needs. Rendering at
300 DPI when the consumer shows a 96 DPI thumbnail wastes most of the work.
Decision tree
pdfium-render program is slow?
|
+-- Slow per request, or PDFium loaded on each call?
| -> bind once: OnceCell (async) or OnceLock (sync)
|
+-- Multi-threaded but no speedup?
| -> expected with thread_safe; switch to process-level parallelism
|
+-- Random crashes under concurrency?
| -> thread_safe is disabled; re-enable it, or serialize calls yourself
|
+-- Slow inside a page render loop?
| -> build PdfRenderConfig once outside the loop; reuse it
|
+-- Output larger than the consumer needs?
-> shrink target/fixed size; turn use_print_quality off for screen
Version table
| Item | 0.8.x | 0.9.x |
|---|
thread_safe feature, default ON | present | present |
Send / Sync on object instances | not implemented | implemented in 0.9.0 |
set_fixed_width / _height / _size | added 0.8.37 | present |
Pdfium::default() bind-with-fallback (also tries cwd) | extended in 0.8.12 | present |
Critical rules
- ALWAYS bind PDFium once per process and store the
Pdfium instance in a
static (OnceCell for async, OnceLock for sync). NEVER rebind per request
or per file.
- ALWAYS build a
PdfRenderConfig once and reuse it across pages. NEVER
construct it inside the render loop.
- NEVER expect the
thread_safe feature to make multi-threaded work faster. It
serializes every call behind a mutex.
- NEVER call PDFium from multiple threads with
thread_safe disabled. That
causes segfaults.
- For real throughput, use process-level parallelism: one
Pdfium per process.
- ALWAYS render at the smallest output size the consumer needs.
- On 0.8.x, object instances are not
Send/Sync; cross-thread sharing needs
0.9.0 or later.
Companion skills
pdfium-core-architecture for the thread-safety stance and ownership tree.
pdfium-core-memory for the lifetime model behind shared instances.
pdfium-core-bindings-setup for the thread_safe feature flag and binding.
pdfium-syntax-rendering for PdfRenderConfig and render_with_config.
pdfium-impl-wasm for the WASM heap and bitmap cost on WASM.
Reference files
references/methods.md : complete API signatures with version annotations.
references/examples.md : working, verified Rust examples.
references/anti-patterns.md : real failures, why they happen, and the fix.