← dompdf.js Studio

Coexisting with jsPDF and html2canvas: Multi-Library PDF Best Practices

Most projects that adopt dompdf.js already ship PDF export through an existing library — jsPDF for programmatic drawing, html2canvas for screenshot-style capture, or an older full-featured engine. The first questions the team asks are practical: can the new library coexist with the old one, will they conflict, and do we have to migrate everything at once? The answer is that dompdf.js coexists cleanly with existing PDF libraries, because they occupy different positions in the toolchain. jsPDF is a low-level drawing API, html2canvas turns DOM into bitmap images, and dompdf.js is a complete DOM-to-vector-PDF engine; they are complementary rather than interchangeable, and routing by scenario is a sound architecture rather than a compromise. This article covers the coexistence playbook in depth: the positioning of each library and the scenarios where each wins, the namespace and bundling risks of running several PDF libraries in one project, a routing layer that dispatches to the right engine per call, lazy loading and bundle-size control, a step-by-step migration path from jsPDF to dompdf.js that never requires a big-bang rewrite, and the recurring problems and best practices of multi-library coexistence. The playbook also covers the awkward middle ground most teams actually live in: a legacy codebase where the old library is deeply embedded and cannot be removed quickly. In that situation the goal is not elegance but containment — clear boundaries, explicit routing, and measurable usage — so the legacy code degrades gracefully while the new engine earns its place feature by feature. After reading it, you will know whether your project should coexist or migrate, and exactly how to execute either choice with minimal risk — turning a potential pile of technical debt into a deliberate, documented engineering decision.

Where Each Library Wins: Positioning and Scenarios

jsPDF is a low-level PDF library: you draw text, lines, and images through API calls, which gives complete control over the output but means converting HTML requires you to implement layout yourself — a large engineering effort for anything beyond simple content. html2canvas renders the DOM to a canvas and exports an image; integration is easy, but the output is a bitmap with unselectable text, blur at zoom, and larger file sizes. dompdf.js snapshots the DOM and writes vector PDFs directly, with selectable text, small files, and automatic pagination — a different value proposition from either.

Scenario routing follows from these positions. Data-driven tables, reports, contracts, and multi-page documents are dompdf.js territory: vector output and pagination match the requirements exactly. Pixel-perfect replication of a marketing page, with all its visual effects, remains html2canvas territory, because a bitmap snapshot preserves exactly what the browser painted. Programmatic chart drawing or precise coordinate layout is jsPDF territory, where the drawing API's flexibility shines. Defining these boundaries explicitly is what makes coexistence coherent instead of chaotic.

The governing principle of coexistence is single responsibility: one export entry point, one engine doing the work — never two libraries rendering the same document. Assign ownership by feature module: legacy features keep their original engine, new features default to dompdf.js, and a shared export service layer routes between them so business code never depends on a specific library. When the day comes to switch engines, only the routing configuration changes and the business layer stays untouched; that architecture is the most stable form of multi-library coexistence.

One caveat keeps the positioning honest: capabilities overlap at the edges, and a feature that works well with html2canvas today may be better served by dompdf.js tomorrow as templates evolve. Review the routing decisions periodically — at least when a new major version of any engine ships — and treat the positioning as a living map rather than a fixed contract, so the architecture keeps matching reality.

Do They Conflict? Namespaces and Build Risks

dompdf.js is a pure frontend library with no dependency on jsPDF or html2canvas, and the three do not collide on global names: jsPDF exposes a global jsPDF, html2canvas exposes html2canvas, and dompdf.js exports the DomPDF class as an ES module, so in a modular project they are naturally isolated. The real risks live in bundling and versions: when several libraries depend on the same underlying utility package at different versions, you can get duplicated code or subtle behavioral divergence between the copies.

When libraries are loaded as global scripts from a CDN, watch load order and namespace overwrites: if two scripts write to the same window property, the later one silently replaces the earlier one. Prefer ES modules or a bundler so each library's namespace stays inside its module scope. If global scripts are unavoidable, save the references into your own namespaced variables immediately after loading, so later scripts cannot clobber them without you noticing.

The build-level risk is mostly size: dompdf.js ships a WASM core and is heavier than a pure-JS library, and keeping all three libraries in the main bundle raises the initial payload noticeably. The standard answer is dynamic import — put every PDF-related module behind lazy loading so nothing PDF-related is fetched until the user clicks export. The main bundle then carries none of the PDF libraries' cost, and coexistence stops being a performance problem at all.

Inherited global state is a subtler risk than name collisions: if any library reads or mutates shared globals like window.devicePixelRatio, font lists, or timers, the interaction can change behavior in ways that are hard to attribute. When symptoms appear only after both libraries have loaded, bisect by changing the load order, and consider isolating libraries in separate iframes or workers if the interference persists.

Code Example: Routing by Scenario — One Entry Point, Many Engines

The routing layer's job is to give business code a single exportPdf function and hide the engine selection inside it: in auto mode the function picks dompdf.js when WASM is available, and callers can force an engine explicitly when a feature has known requirements. New features get the new engine, legacy features keep their exact behavior, and the blast radius of any change is confined to the routing layer — which is precisely the property that lets a team migrate in parallel without freezing feature work.

Note the dynamic imports in the bitmap branch: html2canvas and jsPDF are downloaded only when that branch actually runs, so the dompdf.js path never pays for them. The inverse arrangement works too — if the project is jsPDF-centric and dompdf.js is the newcomer, put dompdf.js behind the dynamic import. The principle is the same in both directions: load on demand, let the engine that works do the work, and keep the other engines' bytes off the user's critical path.

Beyond routing, record engine usage in the export service layer: how many exports ran on dompdf.js, how many on the legacy path, and what the legacy path's failure rate looks like. This telemetry is the basis for the migration decision — when dompdf.js covers nearly everything and the legacy engines see only sporadic calls, scheduling their removal becomes a data-driven decision rather than a guess, and the team can decommission with confidence.

The routing function is also the natural place to centralize options that differ per engine: paper format, margins, and pagination behavior are expressed differently in each library, and normalizing them at the boundary keeps business code stable when engines change. Document the mapping in the service layer so the differences are visible in one place instead of scattered across feature code.

import { DomPDF } from 'dompdf.js';
// jsPDF and html2canvas already exist in this project

function supportsWasm() {
  return typeof WebAssembly !== 'undefined';
}

async function exportPdf(source, { engine = 'auto' } = {}) {
  if (engine === 'dompdf' || (engine === 'auto' && supportsWasm())) {
    // Vector path: reports, contracts, multi-page documents
    const pdf = new DomPDF();
    pdf.addPage(source, { format: 'A4' });
    pdf.save('document.pdf');
    return;
  }
  // Bitmap fallback: old browsers or pixel-perfect snapshot scenarios
  const { default: html2canvas } = await import('html2canvas');
  const { jsPDF } = await import('jspdf');
  const canvas = await html2canvas(source);
  const img = canvas.toDataURL('image/png');
  const doc = new jsPDF();
  doc.addImage(img, 'PNG', 0, 0, 210, 297);
  doc.save('document.pdf');
}

Lazy Loading and Bundle-Size Control

The cost of coexistence is mostly bytes, and lazy loading is the only correct answer. Wrap each PDF library in its own dynamic-import module, load none of them at page initialization, and fetch only the engine the route selects when the user clicks export. Initial page size and the export feature become fully decoupled: however heavy the export stack is, it never delays first paint, which is the most user-friendly shape coexistence can take.

Size control extends to the resource layer: the wasm file and fonts are separate requests, and with sensible cache headers they download once and then serve from cache. Give wasm and font resources versioned filenames and long cache lifetimes, so an upgrade changes only the affected files and repeat exports approach zero load time. In high-frequency export products this turns the second export into a near-instant operation without any code changes.

On the bundler side, watch the boundaries of tree-shaking: if a dynamically imported module contains side-effectful code, it can end up in the main bundle anyway. Inspect the build output's chunking to confirm PDF code landed in its own chunk, and use the bundler's analysis plugin to see the size composition of every chunk. When a bundle grows unexpectedly, this view shows at a glance which library's code leaked into the main path, and the fix is usually one import statement away.

For products with aggressive performance budgets, consider serving the legacy engines only to environments that need them: a server-side capability check or a client-side feature flag can decide which chunk the browser downloads, so users on modern browsers never fetch the bitmap path at all. This pushes lazy loading one step further, from per-export to per-user, and it is easy to implement once the routing layer already exists.

A Gradual Migration Path from jsPDF to dompdf.js

If the end goal is migrating to dompdf.js, a gradual route beats a big-bang rewrite. Phase one is coexistence: old and new features run in parallel and each is validated independently. Phase two makes dompdf.js the default for new features while legacy features stay put. Phase three switches high-traffic legacy features one at a time, comparing the output of both engines on the same input before each switch — pagination, fonts, and image rendering must match expectations. Phase four removes the legacy libraries.

Comparative validation is the heart of migration. For every feature being migrated, prepare a set of representative inputs — different text lengths, pages with images, mixed Chinese and English documents — generate with both engines, and compare page counts, text content, and key styles page by page. Record the differences as a checklist and classify each as either a must-fix defect or an acceptable detail difference, so quality standards do not drift silently during the migration.

Keep both engines switchable during the transition: the routing layer retains the engine parameter, and a production incident can be rolled back to the legacy engine in minutes rather than hours. After the migration completes, do not delete the old code immediately — keep the switch for one release cycle, watch the production telemetry, and clean up only after the data confirms stability. Every step of a gradual migration is reversible and verifiable, which keeps team stress low and success rates far above a weekend rewrite.

During migration, freeze the legacy engines' versions: a background dependency update on the old library can shift its output and invalidate your comparison baselines, contaminating the validation data. Record the exact versions used for each comparison run, and re-verify the baseline whenever either engine changes, so the migration decisions rest on apples-to-apples comparisons.

Coexistence Pitfalls and Best Practices

Recurring problem one: two libraries both generate PDFs and their file naming or trigger buttons interfere. The fix is centralization — file names, trigger buttons, and download logic all live in the export service layer, and business code never calls a library directly. Problem two: bundle size doubles after adding the second library. The fix is dynamic import for all PDF libraries, verified by inspecting the chunk output. Problem three: upgrading one library changes another's behavior. The fix is version pinning plus a full export regression run before every upgrade.

Best-practice summary: single responsibility — one export entry, one engine per document; unified routing — business code never depends on a library directly; lazy loading — the working engine is the only one on the critical path; version pinning — upgrades go through regression first; data-driven decisions — export telemetry determines the migration rhythm. These five rules turn multi-library coexistence from chaotic technical debt into a clear engineering structure that any team member can understand at a glance.

Finally, document the coexistence architecture in the project's technical documentation: why dompdf.js was introduced, the routing rules, each engine's scenarios and known limitations, and the migration roadmap. Documentation is the preservative of a coexistence architecture — after personnel changes, new members do not have to reverse-engineer the decisions, and the rationale survives. Coexistence does not have to be permanent, but clear documentation keeps it controlled, maintainable, and exitable for as long as it lasts.

And budget for the endgame: coexistence has a natural expiration date once dompdf.js covers the remaining scenarios, and the removal of the legacy libraries is itself a project — dependency cleanup, bundle re-audit, and a final regression pass on the features that previously used the old engines. Scheduling that cleanup explicitly, rather than letting it drift, is what keeps the multi-library era from becoming permanent.

⚡ 现场演示(点击生成 PDF)

下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:

Hello from dompdf.js!

这是由 dompdf.js 渲染的示例 PDF 内容。