tesseract.js's worker-script carries the highest activity risk — 4 functions

Four of the five highest-risk functions in naptha/tesseract.js live in a single file — src/worker-script/index.js — combining cyclomatic complexity up to 40 with recent commit activity, making it a live regression surface right now.

Stephen Collins ·
Generated by hotspots · free & open source
Activity Risk13.49Low
Hottest FunctionloadAndGunzipFile

Antipatterns Detected

complex_branching6god_function4long_function4deeply_nested2

Run this on your own codebase

Hotspots runs locally in under a minute — no account, no data leaves your machine.

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is a god function and why does it matter in tesseract.js?

A god function is a single function that has taken on so many distinct responsibilities that it cannot be understood, tested, or changed in isolation. In practice, it shows up as a large function body with high cyclomatic complexity, high fan-out, and usually high line count — all three pattern flags appear together on `loadAndGunzipFile`, `loadLanguage`, `initialize`, and `recognize` in src/worker-script/index.js. The concrete problem is blast radius: because a god function coordinates many concerns at once, a change to one concern — say, how the cache bypass logic works — can silently affect behavior in an unrelated concern like gzip detection or Emscripten FS writes in the same function body. In tesseract.js, four of the six critical-band functions match this pattern, and they are all in the same file, which concentrates that blast radius in one place.

How do I reduce cyclomatic complexity in JavaScript?

The most direct technique is extract-method refactoring: identify a coherent sub-task inside a complex function, move it into a named function with a clear input/output contract, and call that function from the original. A cyclomatic complexity above 15 is a practical signal to start extracting; above 30 — where `loadAndGunzipFile` sits at CC 40 — it warrants immediate attention because branch coverage requires at minimum 40 test cases. A concrete first step for `loadAndGunzipFile` is to extract the file-acquisition logic (CDN URL construction, fetch vs. filesystem read, environment detection) into a separate `fetchTrainedData` function — this alone would remove roughly half the decision points from the outer function body and make the network-path logic independently testable. Guard clauses (returning early on known-bad conditions rather than nesting the happy path deeper) also reduce both cyclomatic complexity and nesting depth without requiring a full restructure.

Is tesseract.js actively maintained?

Yes, based on the data at commit a1ca80d. Four of the five top-risk functions — `loadAndGunzipFile` (risk score 13.49), `loadLanguage` (13.12), `initialize` (12.23), and `recognize` (11.78) — are all in the fire quadrant, meaning they are both structurally complex and were touched within the last 30 days. Each recorded 1 touch in the last 30 days and shows days_since_changed of 0, indicating a very recent push. The fifth top hotspot, the anonymous function in browser/getCore.js, is a debt-quadrant function that has not been changed in 152 days and received zero touches in the last 30 days. Active development and accumulated structural complexity are not mutually exclusive — the high activity-weighted risk scores in this repository reflect a codebase that is being actively evolved, with some functions that have grown more complex than their current test surface can safely cover.

How do I reproduce this analysis?

The analysis was produced using the Hotspots CLI, available at github.com/hotspots-dev/hotspots, against commit a1ca80d of naptha/tesseract.js. After checking out that commit with `git checkout a1ca80d`, run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root. The same command works on any local git repository without additional configuration — no hotspots account or API key is required for a local snapshot analysis.

What does activity-weighted risk mean?

Activity-weighted risk combines two independent signals: structural complexity — derived from cyclomatic complexity, nesting depth, and fan-out — and recent commit frequency. A function that scores high on structural complexity but has not been touched in months is serious technical debt, but it is not an immediate regression risk because no one is changing it right now. The anonymous function in browser/getCore.js illustrates this: risk score 10.6, CC 15, nesting depth 5, but 152 days since last change and zero touches in the last 30 days — structural debt, not a live risk. A function with high structural complexity that is being committed to recently is a live regression risk because each change navigates a complex decision tree under time pressure. The four fire-quadrant functions in src/worker-script/index.js are high-priority precisely because they combine both signals: cyclomatic complexity between 22 and 40, fan-out between 17 and 24, and all touched within the last 30 days. This prioritization focuses refactoring effort where it reduces the probability of introducing bugs today, not just where the code looks complicated in the abstract.

Across 94 functions analyzed at commit a1ca80d, six are rated critical and four of them live in src/worker-script/index.js — a single worker orchestration module that is simultaneously the most structurally complex and the most recently changed part of the codebase. The top-ranked function, loadAndGunzipFile, carries an activity-weighted risk score of 13.49 with a cyclomatic complexity of 40 and was touched within the last 30 days, making it a live regression risk rather than a backlog cleanup item. I would start any review effort there, then work down through loadLanguage, initialize, and recognize before stepping out to the fifth hotspot in src/worker-script/browser/getCore.js.

The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).

Top 5 Hotspots

FunctionFileRiskCCNDFO
loadAndGunzipFilesrc/worker-script/index.js13.540417
loadLanguagesrc/worker-script/index.js13.125421
initializesrc/worker-script/index.js12.222521
recognizesrc/worker-script/index.js11.826424
<anonymous>src/worker-script/browser/getCore.js10.61558

Large Repo Analysis

tesseract.js is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.

The concentration problem

Before looking at individual functions, the distribution tells its own story.

Triage Band Distribution
Fire7Debt9Watch6OK72

94 functions analyzed

Seven functions are in the “fire” quadrant — structurally complex and actively changing — while nine sit in “debt”: complex but untouched, accumulating blast-radius risk for the next time someone opens them. The four fire-quadrant functions in src/worker-script/index.js alone account for the bulk of the critical band.

Detected Antipatterns
Complex Branching×6Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
God Function×4God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.
Long Function×4Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
Deeply Nested×2Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.

Complex branching dominates the antipattern surface. God function and long function patterns appear together on every one of the top four hotspots, which is not a coincidence — each of those functions has grown to handle multiple distinct responsibilities inside a single async body. That growth pattern is what drives both the high cyclomatic complexity numbers and the elevated fan-out counts you will see below.


loadAndGunzipFile — src/worker-script/index.js

loadAndGunzipFile
src/worker-script/index.js
13.49
fire
CC 40
ND 4
FO 17
touches/30d 1

loadAndGunzipFile is a nested async function defined inside loadLanguage. Based on its name and the source excerpt, it handles everything involved in acquiring a .traineddata language model file: checking a local cache, deciding whether to bypass the cache based on a cacheMethod flag, constructing a CDN URL or falling back to a local filesystem path, branching on whether the runtime is Node.js, a web worker, or a browser extension context, fetching or reading the file, detecting gzip magic bytes, decompressing if needed, writing into the Emscripten virtual filesystem, and optionally persisting back to cache.

That is not one responsibility — it is at least six, and a cyclomatic complexity of 40 reflects each decision point precisely. With 40 independent execution paths, there are 40 minimum test cases needed to reach full branch coverage. In practice, async branches that span network calls, file I/O, and environment detection are rarely tested exhaustively.

The fan-out of 17 compounds this. In JavaScript, async callback chains and dynamic property dispatch (env === 'webworker' ? fetch : adapter.fetch) mean the actual coupling is likely higher than what static analysis captures. A change to how cache bypass works, for example, touches the CDN URL construction logic, the gzip detection logic, and the Emscripten FS write — all inside the same function body.

This function was touched in the last 30 days. That makes it a live regression surface, not a cleanup item for later.

Recommendation: Extract at least three sub-functions: one for cache resolution, one for file acquisition (URL vs. local path, fetch vs. filesystem read), and one for decompression and FS write. Each can then be tested in isolation. The goal is to get the remaining function body below CC 10 and fan-out below 10.


loadLanguage — src/worker-script/index.js

loadLanguage
src/worker-script/index.js
13.12
fire
CC 25
ND 4
FO 21
touches/30d 1

loadLanguage is the outer function that defines loadAndGunzipFile as a closure and then maps it over a parsed array of language codes. Its own complexity metrics — CC 25, fan-out 21 — are independent of the nested function and reflect the branching in the outer body: destructuring a deeply nested options payload, normalizing the langs argument from string or array, managing a shared progress counter, and coordinating the results of parallel file loads.

Fan-out of 21 is the highest of any function in the top five alongside initialize. In a JavaScript module where adapter methods are invoked through a shared object (adapter.readCache, adapter.writeCache, adapter.deleteCache, adapter.fetch), static fan-out analysis may actually undercount the real coupling because dynamic property access is not always resolvable at parse time. The true blast radius of a change to loadLanguage’s option-handling logic is wider than 21 suggests.

Both loadAndGunzipFile and loadLanguage share the same god_function and long_function patterns, which makes sense given that one is literally defined inside the other. Splitting loadAndGunzipFile out as a top-level module export would also mechanically reduce loadLanguage’s own line count and complexity.

Recommendation: Promote loadAndGunzipFile to a named module-level export. This alone reduces loadLanguage to a coordinator function, making its remaining branching (argument normalization, progress tracking, parallelism) easier to reason about and test.


initialize — src/worker-script/index.js

initialize
src/worker-script/index.js
12.23
fire
CC 22
ND 5
FO 21
touches/30d 1

initialize sets up the Tesseract API instance inside the worker. From the source excerpt, it handles config argument normalization (object vs. string), writing a config file into the Emscripten FS, constructing and calling TessBaseAPI.Init, and then running a recovery path when initialization returns -1: deleting cached traineddata files, reading a debug file to diagnose the failure mode, conditionally re-invoking loadLanguage to refresh the cache, and retrying Init a second time.

It is the only function in the top five flagged as deeply_nested, and its max nesting depth of 5 is what you would expect from that recovery path: the outer try block, a conditional on cacheMethod, a map over language codes, a check on dataFromCache, and a regex test on a debug string — each level adding cognitive load before you can reason about what actually happens on a bad initialization.

Cyclomatic complexity of 22 means 22 paths through a function that is also calling 21 distinct external functions. The self-referential call to loadLanguage inside initialize’s failure handler is worth noting: a change to loadLanguage’s signature or behavior needs to be validated against this recovery path, which is unlikely to be covered by happy-path tests.

Recommendation: Extract the failure-recovery logic — cache deletion, debug file inspection, cache refresh, retry — into a separate handleInitFailure async function. This would reduce initialize’s nesting depth by at least two levels and make the recovery path independently testable.


recognize — src/worker-script/index.js

recognize
src/worker-script/index.js
11.78
fire
CC 26
ND 4
FO 24
touches/30d 1

recognize has the highest fan-out of any function in the top five at 24, and a cyclomatic complexity of 26. Based on the source excerpt, it handles Tesseract parameter management (SaveParameters, SetVariable), conditional debug file setup, output configuration via processOutput, an auto-rotation branch that temporarily changes the page segmentation mode and calls FindLines to detect the skew angle before optionally re-calling setImage with the computed angle, an optional rectangle crop, and then the actual Recognize call.

Fan-out of 24 in a JavaScript async function means that a change to any one of those 24 callees — api.SetVariable, api.GetGradient, api.GetAngle, setImage, processOutput, and so on — potentially invalidates the behavior of recognize without touching recognize itself. In a library that wraps a WASM module, some of those callees are effectively foreign-function calls with no TypeScript type guarantees, making the coupling harder to reason about statically.

The auto-rotation path is the most branched sub-section: it checks the current page segmentation mode, conditionally overrides it, calls FindLines, reads the angle via one of two API methods depending on availability (GetGradient vs. GetAngle), and then decides whether the angle is large enough to warrant re-rendering. Each of those decisions is a path multiplier against the base CC.

processOutput, which recognize calls and which also appears in the context data as a fire-quadrant function in the same file with an activity-weighted risk score of 7.91, is itself being actively changed — meaning two coupled functions in the same file are both in the current development window.

Recommendation: Extract the auto-rotation logic into a dedicated detectAndApplyRotation function. It has a clear input (the current api state and image), a clear output (the final rotation radians), and its own branching that can be unit tested independently of the full recognition pipeline.


<anonymous> — src/worker-script/browser/getCore.js

<anonymous>
src/worker-script/browser/getCore.js
10.6
critical
CC 15
ND 5
FO 8
touches/30d 0

This is the one outlier in the top five: a critical-band function in the debt quadrant. It has not been touched in 152 days and recorded zero touches in the last 30 days. That makes it structural debt rather than a live regression surface — but the blast-radius risk is real when the next change arrives.

Based on the source excerpt, this anonymous module export resolves which compiled Tesseract WASM binary to load in a browser context. It checks for relaxedSimd support, then simd support, then falls back to a baseline binary, and in each case branches again on whether lstmOnly is true — producing six distinct binary filenames across the decision tree. A max nesting depth of 5 reflects that three-level SIMD capability check nested inside the lstmOnly conditional.

Cyclomatic complexity of 15 across a function whose entire job is selecting a file path is a sign that the branching structure has outgrown its original design. The backward-compatibility shim for TesseractCoreWASM (an older module name) adds another conditional at the end, and no contributors have touched this file in the last 90 days.

Debt-quadrant functions carry a particular kind of risk: they are not causing problems today, but the next developer who needs to add a new WASM variant — say, a third SIMD tier or a new architecture target — will need to navigate all 15 paths to do it safely.

Recommendation: Replace the nested SIMD capability checks with a lookup table: build an array of capability checks in priority order (relaxedSimd + lstmOnly, relaxedSimd, simd + lstmOnly, simd, lstmOnly, baseline) and iterate to find the first match. This collapses the nesting depth to 2 and reduces CC to roughly 6, making future binary variant additions a one-line change.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
complex_branching6
god_function4
long_function4
deeply_nested2

These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.

Reproduce This Analysis

git clone https://github.com/naptha/tesseract.js
cd tesseract.js
git checkout a1ca80d9e31c34512d0ded75ff8821ddcf3f2f91
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5

To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.

I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →

Related Analyses