# Pdfium Impl Performance

> 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

- Skill: `impertio-studio/pdfium-impl-performance` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add impertio-studio/pdfium-impl-performance`
- Raw SKILL.md: https://api.skillmd.com/api/skills/impertio-studio/pdfium-impl-performance/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- License: MIT
- Author: Impertio-Studio (https://skillmd.com/u/impertio-studio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/impertio-studio/pdfium-impl-performance

---


# 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:

1. Binding. Loading the PDFium library is expensive. Bind once, reuse forever.
2. Parallelism. PDFium is not thread safe. The `thread_safe` feature makes
   multi-threaded code correct but never faster. Throughput comes from
   process-level parallelism.
3. 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`:

```rust
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:

```rust
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.

```rust
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)?;
    // process bitmap
}
```

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.

