Start here

Quick start & APIInstallOnline OCRCode generator

Models & pipelines

PP-OCRv6 modelsLanguagesPP-StructureV3PaddleOCR-VL

Deploy & integrate

Serving & APIPaddleOCR.jsMCP server

Fix & upgrade

TroubleshootingMigrate from 2.xReleases

Evaluate

Our benchmarkCompare OCR toolsLicense & cost

Tools

Online OCRInstall command builderCode generatorError fixer2.x → 3.x converterLanguage code finder

Language

English简体中文日本語Try OCR online
Deploy & integrate

PaddleOCR.js: PaddleOCR in JavaScript

@paddleocr/paddleocr-js is the PaddleOCR team's official JavaScript SDK. It runs PP-OCRv6 or PP-OCRv5 text detection and recognition in the browser with ONNX Runtime Web and OpenCV.js, so no server and no upload. We built our online OCR tool on it. This guide covers the API and the problems we had to solve to ship it.

Checked against PaddleOCR 3.7.0Updated 12 min read
On this page
  1. Install and first OCR
  2. Model presets and languages
  3. Construction options
  4. predict() parameters and result shape
  5. WebGPU vs WASM, threads and COOP/COEP
  6. Worker mode
  7. Drawing results: the viz subpath
  8. Production model loading: what we learned building /online
  9. How fast is it?
  10. Custom and fine-tuned models
  11. Node.js, React and other environments
  12. Questions

Short answer. Run npm install @paddleocr/paddleocr-js. Then call await PaddleOCR.create({ lang: "en", ocrVersion: "PP-OCRv6" }) and await ocr.predict(file). You get an array with one result per image, each with items: [{ text, score, poly }]. It is a browser SDK. The README doesn't document Node.js. For server-side JavaScript, call a PaddleOCR HTTP API instead.12

Install the scoped package. @paddleocr/paddleocr-js is the official one. The unscoped npm packages paddleocr and paddleocr-js come from other publishers, even though one copies the version number 0.4.2.2

Install and first OCR#

npm install @paddleocr/paddleocr-js
// jsDelivr's +esm build rewrites the SDK's bare imports, so this runs as-is
import { PaddleOCR } from "https://cdn.jsdelivr.net/npm/@paddleocr/paddleocr-js@0.4.2/+esm";

The official quick start:1

jsocr.js
import { PaddleOCR } from "@paddleocr/paddleocr-js";

const ocr = await PaddleOCR.create({
  lang: "ch",
  ocrVersion: "PP-OCRv5",
  ortOptions: {
    backend: "auto"
  }
});

const [result] = await ocr.predict(fileOrBlob);
console.log(result.items);

predict always resolves to an array, even for a single File, so destructure it. Inputs can be a Blob/File, ImageBitmap, ImageData, HTMLCanvasElement, HTMLImageElement or an OpenCV cv.Mat, or an array of them.1

The package is ESM-only. Its dist/index.mjs keeps bare imports (js-yaml, clipper-lib, @techstark/opencv-js, onnxruntime-web), so a browser can't load that file directly. You need a bundler, an import map, or jsDelivr's +esm URL above. We use the +esm import in production on our online tool, and it works in Chromium with the default main-thread mode.246

Model presets and languages#

Pick models with lang + ocrVersion, or name them explicitly. Three pairs are built in. They download from Baidu's CDN as uncompressed .tar archives:35

ModelsHow to requestLanguagesDownload
PP-OCRv6_small det + recocrVersion: "PP-OCRv6" + a supported langch, chinese_cht, en, japan + 48 Latin-script codes31.2 MB
PP-OCRv6_tiny det + recExplicit names only (below)Same model family; no Japanese76.3 MB
PP-OCRv5_mobile det + recocrVersion: "PP-OCRv5", or no options at allch, chinese_cht, en, japan21.5 MB
js
// PP-OCRv6 tiny has no lang mapping: name the models
await PaddleOCR.create({
  textDetectionModelName: "PP-OCRv6_tiny_det",
  textRecognitionModelName: "PP-OCRv6_tiny_rec"
});

The Latin set for PP-OCRv6 includes fr, de, es, pt, it, nl, pl, tr, vi, id, cs, sv and more. Every code in the SDK's Latin list works except pi. Korean, Cyrillic, Arabic, Thai and Devanagari models are not available in the SDK. Use the Python library for those (see the language list).3

Gotcha: lang alone defaults to PP-OCRv5. If you set only lang, ocrVersion becomes "PP-OCRv5". So { lang: "fr" } throws Unsupported lang/ocrVersion combination: lang="fr", ocrVersion="PP-OCRv5". Always pass ocrVersion: "PP-OCRv6". With neither option set, you get the PP-OCRv5_mobile pair.3

PP-OCRv6 medium, the Python default, isn't built in. Small is the most accurate model the SDK ships. In our CPU benchmark it matched medium on three of five documents.

Construction options#

OptionWhat it does
lang, ocrVersionPreset selection (above)
textDetectionModelName, textRecognitionModelNameExplicit built-in or custom model names
textDetectionModelAsset, textRecognitionModelAsset{ url } of a model .tar: custom models, or your own mirror of the built-ins
textDetectionBatchSize, textRecognitionBatchSizeInference batch sizes
ortOptionsbackend ("auto" | "webgpu" | "wasm"), wasmPaths, numThreads, simd
workertrue runs the whole pipeline in a dedicated Web Worker
pipelineConfigPaddleX-style YAML text or object. Direct options win if both are given.

Other methods are ocr.initialize(), ocr.getInitializationSummary() and ocr.dispose(). Call dispose() when you're done, because the ONNX sessions hold a lot of memory.1

predict() parameters and result shape#

The second argument takes the same tuning knobs as Python, in camelCase or snake_case:1

js
const [result] = await ocr.predict(imageBitmap, {
  textDetLimitSideLen: 64,     // or text_det_limit_side_len
  textDetLimitType: "min",
  textDetMaxSideLimit: 4000,
  textDetThresh: 0.3,
  textDetBoxThresh: 0.6,       // lower (e.g. 0.4) to catch faint text
  textDetUnclipRatio: 1.5,     // raise (e.g. 2.0) if letters get clipped
  textRecScoreThresh: 0.0      // drop lines below this confidence
});

The values shown are the SDK's default detection config.3 Each result looks like this:

js
{
  image:   { width, height },
  items:   [{ text: "Order #4471", score: 0.97, poly: [[x, y], [x, y], [x, y], [x, y]] }, ...],
  metrics: { detMs, recMs, totalMs, detectedBoxes, recognizedCount },
  runtime: { /* requested backend and provider metadata */ }
}

When you pass several images, detectedBoxes and recognizedCount are per image. detMs/recMs/totalMs cover the whole call and are identical on every element.1 For Python users: items[i].text is rec_texts[i], score is rec_scores[i] and poly is rec_polys[i].

WebGPU vs WASM, threads and COOP/COEP#

With backend: "auto", the SDK asks navigator.gpu for an adapter. It uses WebGPU if it gets one and WebAssembly otherwise. "webgpu" throws if WebGPU is unavailable, and "wasm" always uses the CPU.3 Your app is responsible for:1

  • COOP/COEP headers "when enabling threaded WASM or WebGPU". The official Vite demo sends Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: credentialless. Without cross-origin isolation, the browser has no SharedArrayBuffer, so WASM runs single-threaded.
  • Where the .wasm files come from (ortOptions.wasmPaths). In main-thread mode your bundler normally copies them from node_modules/onnxruntime-web/dist/.
  • A bundler that can emit module workers, if you use worker: true.
jsvite.config.js (from the official demo)
export default defineConfig({
  worker: { format: "es" },
  server: {
    headers: {
      "Cross-Origin-Opener-Policy": "same-origin",
      "Cross-Origin-Embedder-Policy": "credentialless"
    }
  }
});

Our online tool sends no COOP/COEP headers and still works. It's just single-threaded WASM when WebGPU isn't available. The measurements below were taken that way.6

Worker mode#

Inference blocks the thread it runs on. To keep your UI responsive, use the SDK's own worker. Here is the official example:1

js
import { PaddleOCR } from "@paddleocr/paddleocr-js";

const ocr = await PaddleOCR.create({
  lang: "ch",
  ocrVersion: "PP-OCRv5",
  worker: true,
  ortOptions: {
    backend: "wasm",
    wasmPaths: "https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/",
    numThreads: 2,
    simd: true
  }
});
  • It uses the package's worker, not ONNX Runtime's env.wasm.proxy, which it switches off to avoid nested workers. cv.Mat inputs aren't supported, and the browser needs OffscreenCanvas.13
  • Pin wasmPaths to the ONNX Runtime version you actually ship. If it's unset, the 0.4.2 build falls back to onnxruntime-web@1.24.3 on jsDelivr and logs a warning. The unversioned URL in the example above floats to whatever is latest.3
  • The worker bundle is large: dist/assets/worker-entry-*.js is 11.3 MB in 0.4.2.4 We have not tested worker mode through the +esm CDN build. Use a bundler for it.

Drawing results: the viz subpath#

@paddleocr/paddleocr-js/viz renders the PaddleOCR-style side-by-side image: boxes on the original at left, recognized text at right. For Chinese or Japanese text, load a CJK font.1

js
import { renderOcrToBlob } from "@paddleocr/paddleocr-js/viz";

const blob = await renderOcrToBlob(imageBitmap, result, {
  font: { family: "Noto Sans SC", source: "/fonts/NotoSansSC-Regular.ttf" }
});

It takes a single OcrResult, not the array. There is also an OcrVisualizer class with toBlob() and dispose(), and deterministicColor(index) for matching colors in your own overlays.1

Production model loading: what we learned building /online#

By default the SDK fetches models from paddle-model-ecology.bj.bcebos.com each time you call create().3 Three things went wrong for us in production, and the loader on our site handles each of them:6

  1. CORS headers come and go. Baidu's CDN reflects your Origin in Access-Control-Allow-Origin most of the time. In one batch of 16 requests, one response had no CORS header. That happened even with a site-specific query string to avoid a shared cache entry. In a browser that is a hard failure.
  2. Downloads are big: 31.2 MB for small, 21.5 MB for v5 mobile, 6.3 MB for tiny. You don't want users downloading them again on every visit.
  3. The SDK gives no progress events for model downloads.

The fix is to fetch the archives yourself, then hand the SDK blob: URLs through textDetectionModelAsset / textRecognitionModelAsset. Retry Baidu once with a fresh query string. If that fails, fall back to the identical ONNX export on Hugging Face (PaddlePaddle/<model>_onnx). Hugging Face hosts inference.onnx and inference.yml as loose files, so pack them into the uncompressed tar the SDK expects. Store the result in Cache Storage. Condensed from our ocr-engine.mjs:

jsmodel-loader.js (condensed from paddleocr.dev)
const BOS = "https://paddle-model-ecology.bj.bcebos.com/paddlex/official_inference_model/paddle3.0.0/";
const HF  = "https://huggingface.co/PaddlePaddle/";

async function getBytes(url) {
  const r = await fetch(url, { mode: "cors" });
  if (!r.ok) throw new Error(`HTTP ${r.status} ${url}`);
  return new Uint8Array(await r.arrayBuffer());
}

async function getModel(name) {                 // e.g. "PP-OCRv6_small_det"
  const cache = await caches.open("ocr-models-v1");
  const key = `/models/${name}.tar`;
  const hit = await cache.match(key);
  if (hit) return URL.createObjectURL(await hit.blob());

  let blob;
  for (const q of ["?src=myapp", `?r=${Math.random().toString(36).slice(2)}`]) {
    try { blob = new Blob([await getBytes(`${BOS}${name}_onnx_infer.tar${q}`)]); break; }
    catch { /* missing CORS header or network error: try again */ }
  }
  if (!blob) {                                  // same ONNX files, different host
    const base = `${HF}${name}_onnx/resolve/main/`;
    const [onnx, yml] = await Promise.all([getBytes(base + "inference.onnx"), getBytes(base + "inference.yml")]);
    blob = buildTar([{ name: "inference.onnx", bytes: onnx }, { name: "inference.yml", bytes: yml }]);
  }
  await cache.put(key, new Response(blob));
  return URL.createObjectURL(blob);
}

const [det, rec] = await Promise.all([getModel("PP-OCRv6_small_det"), getModel("PP-OCRv6_small_rec")]);
const ocr = await PaddleOCR.create({
  textDetectionModelName: "PP-OCRv6_small_det", textDetectionModelAsset: { url: det },
  textRecognitionModelName: "PP-OCRv6_small_rec", textRecognitionModelAsset: { url: rec },
  ortOptions: { backend: "auto" }
});
// buildTar(): a ~30-line ustar writer (512-byte headers, zero padding, two empty
// trailing blocks). See tarHeader()/buildTar() in /assets/ocr-engine.mjs.

The model names must stay the same as in inference.yml. The SDK checks that model_name matches the name you pass. Wrap caches calls in try/catch: Cache Storage can be missing or full in private windows. The full version adds streaming progress and abort support.16

How fast is it?#

One receipt, in the browser: tiny is 5× faster than small

PaddleOCR.js 0.4.2, WASM backend (no WebGPU), headless Chromium, 4-vCPU Xeon. Lower is faster.

ModelSeconds
PP-OCRv6 tiny1.14 s
PP-OCRv6 small5.82 s
Measured by paddleocr.dev, 2026-10-27.

On our photographed receipt, both models found all 27 lines and made the same single slip: 0rder (with a zero) for "Order".6 Small is the safer default for mixed documents and Japanese. Tiny makes sense on phones and slow connections when your text is Latin-script or Chinese. Expect faster numbers on a laptop with WebGPU or with threaded WASM behind COOP/COEP.

Custom and fine-tuned models#

A fine-tuned model works if you export it to ONNX and package it the way the SDK expects:1

  • An uncompressed ustar .tar. A .tar.gz fails to parse, because the SDK doesn't gunzip.
  • Containing inference.onnx and inference.yml, at the root or in a subfolder (matched by basename).
  • model_name in inference.yml must equal the name you pass to create().
bash
# from the folder containing your exported model
tar --format=ustar -cf my_rec_model.tar inference.onnx inference.yml

Mistakes throw an Error during initialization, such as a missing entry, a mismatched model_name or an ONNX load failure. There is no silent fallback.1 For converting Paddle models to ONNX, see the official "Obtaining ONNX models" doc linked from the browser deployment page.

Node.js, React and other environments#

  • Node.js: not documented. The package describes itself as a "Browser-based OCR SDK", depends on onnxruntime-web, and has no Node entry point. On the server, use self-hosted PaddleOCR serving (our Node fetch client is tested), or the hosted API's @paddleocr/api-sdk (Node.js 18+).2
  • React / Vue / Next.js: create the instance once, for example in a module-level promise, and only on the client. In Next.js, import it inside useEffect or a client-only dynamic import so it never runs during server rendering.
  • Mobile apps: native Android and iOS demos exist in the PaddleOCR repo (deploy/ppocr-android, deploy/ios_demo) if a WebView isn't enough.

Questions#

Is PaddleOCR.js official?

Yes. @paddleocr/paddleocr-js is published from the paddleocr-js/ folder of the PaddlePaddle/PaddleOCR repo under Apache-2.0. The unscoped paddleocr-js and paddleocr npm packages are unrelated third-party projects.

Does PaddleOCR.js upload my images?

No. Images are decoded and processed in the page with ONNX Runtime Web. The network is used only for the SDK code and the model files.

Can I use PaddleOCR.js in Node.js?

It isn't documented or designed for it: it is a browser SDK built on onnxruntime-web and OpenCV.js. Use PaddleOCR's HTTP serving or the hosted API SDK from Node instead.

Why does PaddleOCR.create throw "Unsupported lang/ocrVersion combination"?

You probably set only lang, so ocrVersion defaulted to PP-OCRv5, which supports only ch, chinese_cht, en and japan. Add ocrVersion: "PP-OCRv6".

Can it read PDFs, tables or Korean?

Not in 0.4.2. It runs only text detection and recognition on images, with the PP-OCRv5/v6 models listed above. Render PDF pages to a canvas first (for example with PDF.js). For tables, layout or Korean, use the Python library with PP-StructureV3 or the matching language model.

Model download fails with a CORS error. What now?

Baidu's CDN occasionally answers without Access-Control-Allow-Origin. Retry, mirror the tar files on your own origin, or load the Hugging Face PaddlePaddle/<model>_onnx files and pack them as a tar, as in the loader above.

How big is it?

The SDK code is small (dist/index.mjs is about 82 KB). Most of the weight is OpenCV.js, ONNX Runtime's WASM, and the models: 6.3 MB for PP-OCRv6 tiny up to 31.2 MB for small. Cache the models and the second visit is quick.

Sources

  1. SDK README: paddleocr-js/packages/core/README.md (install 10, quick start 15–30, model selection 40–88, batch/ORT options 92–102, pipeline config 105–126, predict params 132–144, return value 148–153, worker mode 157–180, viz 184–218, API 222–231, host responsibilities 248–254). Browser deployment page: docs/version3.x/inference_deployment/cross_platform/browser.en.md.
  2. npm registry, 2026-10-27: @paddleocr/paddleocr-js 0.4.2 (published 2026-06-11, Apache-2.0); package.json (description, ESM-only exports . and ./viz, dependencies). Search results for unscoped look-alikes paddleocr (1.2.0) and paddleocr-js (0.4.2, publisher agions).
  3. SDK source at commit dab3fe3: src/pipelines/ocr/shared.ts (language mapping 68–167, defaults 525–542), src/pipelines/ocr/default-config.ts, src/resources/model-asset.ts (model URLs), src/runtime/ort.ts (backends 14, 75–86), paddleocr-js/docs/architecture.md (worker and wasmPaths behaviour 56–71), apps/demo/vite.config.js (COOP/COEP headers).
  4. jsDelivr, 2026-10-27: /npm/@paddleocr/paddleocr-js@0.4.2/+esm (HTTP 200, rewrites dependencies to jsDelivr URLs) and the package file list (dist/index.mjs 81,588 B; worker bundle 11,341,486 B).
  5. Model archive sizes: Content-Length of the official *_onnx_infer.tar files on Baidu's CDN (paddle-model-ecology.bj.bcebos.com), checked 2026-10-27. Identical ONNX exports on Hugging Face (PaddlePaddle/<model>_onnx).
  6. paddleocr.dev measurements, 2026-10-27: headless Chromium, WASM backend (no WebGPU), 4-vCPU Intel Xeon @ 2.10 GHz Linux container, PaddleOCR.js 0.4.2 loaded from jsDelivr +esm; our receipt-photo test image; CDN CORS behaviour observed while building /online. Loader source: /assets/ocr-engine.mjs.
  7. PP-OCRv6 language coverage (tiny has no Japanese): PP-OCRv6 algorithm doc.
Updated October 27, 2026·How we verify facts