Migrating from PDF.js to Nutrient Web SDK
Mozilla’s PDF.js(opens in a new tab) is an open source JavaScript PDF renderer maintained by Mozilla and used as Firefox’s built-in viewer. It focuses on parsing and rendering PDF documents using the browser’s <canvas> element.
PDF.js is a solid starting point, but building a production-ready PDF experience requires stitching together multiple libraries for annotations, forms, search, and export. Nutrient Web SDK provides all of these out of the box.
Want to skip the manual migration? Try getapdfviewer.com(opens in a new tab) — an agent-assisted quick start that sets up Nutrient in your project automatically.
Viewer setup
Setting up a viewer with PDF.js requires configuring a web worker and CMap/font paths, as well as wiring together four separate components.
PDF.js
import { GlobalWorkerOptions, getDocument } from "pdfjs-dist";
// 1. Configure the worker.GlobalWorkerOptions.workerSrc = new URL( "pdfjs-dist/legacy/build/pdf.worker.min.mjs", import.meta.url,).toString();
// 2. Initialize four components.const pdfjs = await import("pdfjs-dist/web/pdf_viewer.mjs");const eventBus = new pdfjs.EventBus();const linkService = new pdfjs.PDFLinkService({ eventBus });const findController = new pdfjs.PDFFindController({ linkService, eventBus,});const viewer = new pdfjs.PDFViewer({ container: document.getElementById("pdf-container"), eventBus, linkService, findController,});linkService.setViewer(viewer);
// 3. Load the document and wire everything.const loadingTask = getDocument({ url: "document.pdf", cMapUrl: `https://unpkg.com/pdfjs-dist@${version}/cmaps/`, standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${version}/standard_fonts`,});const pdfDocument = await loadingTask.promise;linkService.setDocument(pdfDocument);findController.setDocument(pdfDocument);viewer.setDocument(pdfDocument);
// 4. Set initial scale after pages initialize.eventBus.on("pagesinit", () => { viewer.currentScaleValue = "auto";});You also need to import pdfjs-dist/web/pdf_viewer.css and handle cleanup (destroying the worker and document on unmount).
Nutrient
npm install @nutrient-sdk/viewerimport NutrientViewer from "@nutrient-sdk/viewer";
const instance = await NutrientViewer.load({ container: document.getElementById("pdf-container"), document: "document.pdf",});Nutrient needs no worker configuration, no CMap paths, and no component wiring. The viewer handles rendering, text selection, links, and cleanup automatically. The rest of this guide walks through each PDF.js feature and shows the Nutrient equivalent.
Nutrient guide: Getting started · Open a document
Deep dive: Setting up a custom PDF.js viewer in React · PDF.js EventBus guide
Text search
Both libraries support full-document text search. PDF.js exposes it through an event-driven controller, while Nutrient ships with a search UI and a single async API.
PDF.js
Search requires dispatching events on the event bus and listening for results through two separate event handlers:
// Trigger a search.eventBus.dispatch("find", { type: "", query: "search term", caseSensitive: false, highlightAll: true, findPrevious: false,});
// Listen for results.eventBus.on("updatefindmatchescount", (evt) => { console.log( `${evt.matchesCount.current} of ${evt.matchesCount.total}`, );});
eventBus.on("updatefindcontrolstate", (evt) => { if (evt.state === pdfjs.FindState.NOT_FOUND) { console.log("No results"); }});Nutrient
Nutrient includes a built-in search UI. You can also trigger search programmatically:
const results = await instance.search("search term");instance.setSearchState((state) => state.set("results", results));Nutrient guide: Search UI · Text search API
Annotations
PDF.js leaves annotation tooling to the application — coordinate math, overlay rendering, and persistence all sit on your side. Nutrient ships with a full annotation system, including a UI and persistence model.
PDF.js
PDF.js has no built-in annotation creation API. Building a custom annotation system requires:
- Coordinate conversion — Translate between the PDF space and screen space using
viewport.convertToPdfPoint(). - React Portals — Inject overlay elements into the PDF.js page DOM.
- Text selection tracking — Track selection via
pointerupevents andRange.getClientRects(). - Third-party libraries — Libraries like
annotpdfembed annotations into the PDF binary, andreact-rndprovides draggable and resizable elements. - Manual rerendering — Rerender on zoom and rotation changes by listening to
scalechangingandrotationchangingevents.
A text highlight alone involves tracking selection, extracting client rects, converting each to PDF coordinates, storing the annotation, and rendering colored <div> overlays via portals.
PDF.js 3.x+ added a built-in AnnotationEditorLayer for basic free text, ink, stamp, and highlight annotations. But it doesn’t support comments, threads, or database syncing; it’s designed for local markup and export via saveDocument().
Nutrient
// Create a highlight annotation.const annotation = new NutrientViewer.Annotations.HighlightAnnotation({ pageIndex: 0, rects: [ new NutrientViewer.Geometry.Rect({ left: 50, top: 100, width: 200, height: 20, }), ], color: NutrientViewer.Color.YELLOW,});
await instance.create(annotation);Nutrient supports 20+ annotation types out of the box, including highlights, ink, stamps, notes, shapes, and measurement tools — all with a built-in UI, real-time collaboration, and XFDF/Instant syncing.
Nutrient guide: Annotation types · Create annotations · Import/export
Forms
Reading and writing PDF form values takes a different shape in each library. PDF.js requires opting into form mode and iterating page annotations; Nutrient exposes form fields through a direct API.
PDF.js
Interactive forms require setting the annotation mode and manually extracting values:
const viewer = new pdfjs.PDFViewer({ container, eventBus, linkService, annotationMode: pdfjs.AnnotationMode.ENABLE_FORMS,});
// Reading form values requires iterating all pages.for (let i = 1; i <= pdfDocument.numPages; i++) { const page = await pdfDocument.getPage(i); const annotations = await page.getAnnotations(); for (const annot of annotations) { if (annot.subtype === "Widget") { console.log(annot.fieldName, annot.fieldValue); } }}Nutrient
Forms are interactive by default. Read and set values programmatically:
const formFields = await instance.getFormFields();await instance.setFormFieldValues({ name: "John Doe" });Nutrient guide: Form fields · Read form fields · Fill form fields
Thumbnails and navigation
Thumbnail rendering and page navigation require manual wiring in PDF.js and are built into Nutrient’s sidebar.
PDF.js
PDF.js provides a PDFThumbnailViewer class, but you need to create a container, wire it to the event bus, and sync it with the main viewer:
const thumbnailViewer = new pdfjs.PDFThumbnailViewer({ container: document.getElementById("thumbnail-container"), eventBus, linkService,});thumbnailViewer.setDocument(pdfDocument);
eventBus.on("pagechanging", (evt) => { thumbnailViewer.scrollThumbnailIntoView(evt.pageNumber);});For custom thumbnails, render pages to small canvases with page.render() and manage lazy loading with IntersectionObserver.
Nutrient
Thumbnails are built into the sidebar and synced automatically:
instance.setViewState((v) => v.set("sidebarMode", "THUMBNAILS"));Nutrient guide: Thumbnail sidebar · View state · Zooming
Document outline and metadata
Both libraries can read PDF outline, metadata, and attachments. PDF.js exposes them as raw data you render yourself; Nutrient renders them in the sidebar by default.
PDF.js
const outline = await pdfDocument.getOutline();const metadata = await pdfDocument.getMetadata();const attachments = await pdfDocument.getAttachments();You then need to build your own sidebar UI, handle outline navigation via linkService.goToDestination(), and manage file downloads for attachments.
Nutrient
The outline sidebar is built in:
instance.setViewState((v) => v.set("sidebarMode", "BOOKMARKS"));Nutrient guide: Document outline · Bookmarks
Performance and loading
Performance tuning is hands-on in PDF.js, where you configure workers, range requests, and render cancellation yourself. Nutrient handles these automatically.
PDF.js
Optimizing PDF.js requires manual configuration:
- Worker setup — Configure
GlobalWorkerOptions.workerSrcyourself. - Range requests — Set
rangeChunkSizeanddisableAutoFetchto control fetching. - Render cancellation — Track active
RenderTaskinstances and call.cancel()on fast scroll. - DPI management — Account for
window.devicePixelRatioin canvas operations. - Streaming — Use
PDFDataRangeTransportfor custom data sources.
Nutrient
Performance optimizations are handled automatically, including progressive loading, render prioritization, and memory management. No configuration is needed for standard use cases.
Nutrient guide: Performance best practices · Streaming · Accessibility
Server-side processing
Server-side PDF workflows in PDF.js require a different library for each task. Nutrient consolidates them into a single API.
PDF.js
Server-side workflows require stitching together multiple tools:
| Task | Library |
|---|---|
| Text extraction | pdfjs-dist in Node.js |
| Page manipulation | pdf-lib |
| Thumbnail generation | pdf2pic (requires GraphicsMagick) |
| Office → PDF | LibreOffice (headless) |
| eBook → PDF | Calibre |
| HTML → PDF | Puppeteer |
| OCR | ocrmypdf |
Nutrient
Nutrient Web SDK and Document Engine handle server-side processing through a unified API — text extraction, page manipulation, format conversion, optical character recognition (OCR), and rendering — without requiring system-level dependencies.
Nutrient guide: Text extraction · OCR · Conversion · Page manipulation · Save/export
Further resources
- Get started with Nutrient Web SDK
- getapdfviewer.com(opens in a new tab) — Agent-assisted quick start
- Build a React PDF viewer with pdfjs-dist and Next.js
- JavaScript PDF viewer tutorial with PDF.js
- PDF.js vs. Nutrient
- Complete guide to PDF.js
- Migrate from react-pdf