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
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
loadAndGunzipFile | src/worker-script/index.js | 13.5 | 40 | 4 | 17 |
loadLanguage | src/worker-script/index.js | 13.1 | 25 | 4 | 21 |
initialize | src/worker-script/index.js | 12.2 | 22 | 5 | 21 |
recognize | src/worker-script/index.js | 11.8 | 26 | 4 | 24 |
<anonymous> | src/worker-script/browser/getCore.js | 10.6 | 15 | 5 | 8 |
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.
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.
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 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 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 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 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
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:
| Pattern | Occurrences |
|---|---|
complex_branching | 6 |
god_function | 4 |
long_function | 4 |
deeply_nested | 2 |
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 →