Engineering
Running Tesseract.js in production
Getting Tesseract.js to recognise a word takes ten minutes. Running it in production for years takes an understanding of what it downloads, when, and from where — because that is where it breaks.
EasyInvoiceOCR · Published · 10 min read
Tesseract.js is a JavaScript wrapper around Tesseract, the open-source OCR engine, compiled to WebAssembly so it can run in a browser. It is an independent community project; we use it, we contribute to its issue tracker, and we have no affiliation with it beyond that.
What makes it interesting in production is not the recognition API, which is small. It is the asset story underneath.
What actually gets downloaded
A recognition run pulls three separate things, and confusing them is the source of most deployment problems.
- The worker script — the JavaScript that runs recognition off the main thread.
- The WASM core — the compiled engine. Several variants exist (plain, SIMD, relaxed-SIMD, LSTM-only builds of each) and the browser gets the fastest one it can execute.
- The language data — one .traineddata file per language, and these are by far the largest component.
Language models are the expensive part
Each language is a separate multi-megabyte file. Our five base models total 32.75 MB: English 10.42 MB, Spanish 7.98 MB, German 6.77 MB, French 5.99 MB and Arabic 1.60 MB. Adding the WASM cores brings the vendored total to 76.00 MB.
That figure alarms people until you note what it is not: none of it is in the initial page bundle. A model is fetched once, on demand, when someone actually runs recognition in that language, and then cached. A visitor who only merges two PDFs downloads none of it.
Combined language modes are not extra models
Tesseract accepts a language argument like eng+ara, which loads two models into a single recognition pass so a bilingual page can be read without choosing a side. It is worth being precise about the arithmetic: seven options in a language picker can mean five model files. We offer five base languages and two combined modes, not seven independent models.
The default is a CDN, and that has consequences
By default Tesseract.js fetches its language data from a public CDN at run time. That is convenient and it works — until it does not. A stalled asset request leaves a conversion hanging mid-flight, and because the failing request never touches your own servers, the outage is invisible to your monitoring while being entirely visible to your users.
There is a second, quieter issue. The default asset path is not pinned to a published version, and the download is not integrity-checked, so the bytes fed to the recognizer can change without any consumer noticing. This is an open discussion in the project rather than a criticism of it — the same tension exists in any run-time asset fetch.
Self-hosting the assets
We now serve the worker, the cores and the language models from our own origin. A setup script copies the worker and cores out of node_modules — so they always match the installed version rather than drifting against whatever a CDN currently serves — and downloads the language data once at build time, never at run time.
This removes the third-party dependency from the recognition path entirely. It also removes a subtler privacy leak: fetching the engine from a third-party CDN means that third party sees a request, with a referrer, every time someone starts a conversion. No document content, but a request pattern that maps to intent.
The failure mode self-hosting introduces
This is the part worth passing on, because we learned it the expensive way. When you vendor the models, the set of languages your interface offers and the set your build script downloads become two separate lists, in two different files, with nothing enforcing that they match.
Ours drifted. Two languages were selectable in the picker whose model files had never been added to the download step. Because the asset path pointed at our own origin there was no CDN to quietly fall back to: the request 404'd and recognition failed for those two languages only. Nothing in the test suite caught it, because no test connected the two lists.
The fix that mattered was not adding the two files. It was an invariant test that fails in both directions — a language offered but not vendored, and a model vendored but no longer offered — and that reads the build script as source text so it needs neither network access nor the downloaded assets to run.
Practical advice if you are doing this
Four things we would tell our past selves.
- Pin an explicit version of the language data rather than relying on a path that resolves to whatever is current.
- If you self-host, add a test that every language your UI offers resolves to a file that exists.
- Check the network tab while the worker starts. A missing model looks like 'this language does not work', not like a missing file.
- Do not use a character whitelist with the LSTM engine, and do not request the legacy engine mode against data that has no legacy component. Both make output worse while looking like tuning.
What this does not change
Self-hosting moves where the bytes come from. It does not change what recognition does — we use the standard model set, byte-identical to what Tesseract.js fetches by default, deliberately, because a vendoring change that also silently altered output would be miserable to debug later.
It also does not make the application network-free. Document bytes are processed locally in the browser and are never uploaded; a short job record — filename, file type, size, page count and a key identifying the attempt — is still transmitted so a conversion allowance can be enforced server-side.
Primary sources
- Tesseract.js — the project itself, including the worker options that control asset paths.
- tessdata — Tesseract language data — the published .traineddata files, including the standard 4.0.0 set used here.
- Tesseract documentation on page segmentation and engine modes — reference for the PSM and OEM arguments discussed above.
- WebAssembly — MDN — background on the compilation target and on SIMD support in browsers.
Five conversions are free. Document bytes are processed in your browser.
Related articles
- Browser OCR
What is browser OCR?
Browser OCR reads text from images and PDFs inside the browser tab instead of on a server. How it works, how it compares to cloud OCR, and where each one is the better choice.
- Product
Extracting invoice data in Arabic, French and mixed scripts
Right-to-left layouts, Eastern Arabic numerals and bilingual invoices break parsers built for one script. What actually goes wrong, and how it is caught.