On this page
- Install and first OCR
- Model presets and languages
- Construction options
- predict() parameters and result shape
- WebGPU vs WASM, threads and COOP/COEP
- Worker mode
- Drawing results: the viz subpath
- Production model loading: what we learned building /online
- How fast is it?
- Custom and fine-tuned models
- Node.js, React and other environments
- 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
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
| Models | How to request | Languages | Download |
|---|---|---|---|
| PP-OCRv6_small det + rec | ocrVersion: "PP-OCRv6" + a supported lang | ch, chinese_cht, en, japan + 48 Latin-script codes | 31.2 MB |
| PP-OCRv6_tiny det + rec | Explicit names only (below) | Same model family; no Japanese7 | 6.3 MB |
| PP-OCRv5_mobile det + rec | ocrVersion: "PP-OCRv5", or no options at all | ch, chinese_cht, en, japan | 21.5 MB |
// 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#
| Option | What it does |
|---|---|
lang, ocrVersion | Preset selection (above) |
textDetectionModelName, textRecognitionModelName | Explicit built-in or custom model names |
textDetectionModelAsset, textRecognitionModelAsset | { url } of a model .tar: custom models, or your own mirror of the built-ins |
textDetectionBatchSize, textRecognitionBatchSize | Inference batch sizes |
ortOptions | backend ("auto" | "webgpu" | "wasm"), wasmPaths, numThreads, simd |
worker | true runs the whole pipeline in a dedicated Web Worker |
pipelineConfig | PaddleX-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
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:
{
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-originandCross-Origin-Embedder-Policy: credentialless. Without cross-origin isolation, the browser has noSharedArrayBuffer, so WASM runs single-threaded. - Where the
.wasmfiles come from (ortOptions.wasmPaths). In main-thread mode your bundler normally copies them fromnode_modules/onnxruntime-web/dist/. - A bundler that can emit module workers, if you use
worker: true.
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
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.Matinputs aren't supported, and the browser needsOffscreenCanvas.13 - Pin
wasmPathsto the ONNX Runtime version you actually ship. If it's unset, the 0.4.2 build falls back toonnxruntime-web@1.24.3on 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-*.jsis 11.3 MB in 0.4.2.4 We have not tested worker mode through the+esmCDN 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
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
- CORS headers come and go. Baidu's CDN reflects your
OrigininAccess-Control-Allow-Originmost 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. - 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.
- 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:
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?#
PaddleOCR.js 0.4.2, WASM backend (no WebGPU), headless Chromium, 4-vCPU Xeon. Lower is faster.
| Model | Seconds |
|---|---|
| PP-OCRv6 tiny | 1.14 s |
| PP-OCRv6 small | 5.82 s |
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.gzfails to parse, because the SDK doesn't gunzip. - Containing
inference.onnxandinference.yml, at the root or in a subfolder (matched by basename). model_nameininference.ymlmust equal the name you pass tocreate().
# from the folder containing your exported model
tar --format=ustar -cf my_rec_model.tar inference.onnx inference.ymlMistakes 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 Nodefetchclient 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
useEffector 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
- 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.
- 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-alikespaddleocr(1.2.0) andpaddleocr-js(0.4.2, publisheragions). - 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). - 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). - Model archive sizes: Content-Length of the official
*_onnx_infer.tarfiles on Baidu's CDN (paddle-model-ecology.bj.bcebos.com), checked 2026-10-27. Identical ONNX exports on Hugging Face (PaddlePaddle/<model>_onnx). - 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. - PP-OCRv6 language coverage (tiny has no Japanese): PP-OCRv6 algorithm doc.