At commit 1b8977a, Phaser has 1,897 analyzed functions, 119 of them rated critical — and every single one of the top five hotspots is in the fire quadrant, meaning they are both structurally complex and actively changing right now. GetValueOp leads with an activity-weighted risk score of 19.31, touched 1 time in the last 30 days and modified just 1 day ago, making it a live regression risk rather than a cleanup backlog item. I would start there, then move to PCTDecode (19.26) and GraphicsWebGLRenderer (17.68) — three functions that collectively account for some of the heaviest branching and nesting complexity in the codebase.
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 |
|---|---|---|---|---|---|
GetValueOp | src/tweens/builders/GetValueOp.js | 19.3 | 34 | 9 | 14 |
PCTDecode | src/textures/parsers/PCTDecode.js | 19.3 | 29 | 9 | 21 |
GetValue | src/utils/object/GetValue.js | 17.9 | 18 | 6 | 6 |
GraphicsWebGLRenderer | src/gameobjects/graphics/GraphicsWebGLRenderer.js | 17.7 | 60 | 5 | 18 |
IsometricCullTiles | src/tilemaps/components/IsometricCullTiles.js | 17.1 | 41 | 7 | 2 |
Large Repo Analysis
phaser 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.
Codemod / Tooling Files in Results
The ownKeys function appears in two files under scripts/tsgen/bin/ — Parser.js and publish.js. These are TypeScript declaration generation scripts, tooling that lives outside the game engine itself and is not part of the runtime bundle. Their elevated scores reflect scripting complexity in the build toolchain, not in the engine code. To exclude them from future analyses, add { "exclude": ["scripts/"] } to .hotspotsrc.json in the repository root.
Phaser is one of the most widely used open-source 2D game frameworks in JavaScript, and at nearly 1,900 analyzed functions the surface area is substantial. Before getting into individual functions, it helps to see how risk is distributed across the codebase.
1,897 functions analyzed
The quadrant picture is striking: 345 functions are in the fire quadrant and zero are in the debt quadrant. Every structurally complex function in this repo has also been recently touched. There is no dormant complexity to defer — it is all live.
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Deeply Nested×5Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.Exit Heavy×5Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Long Function×4Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.God Function×3God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Hub Function×2Hub Function
Many other functions call this one — a change here ripples widely through callers.
Across the five hotspots, every single one flags complex branching, deep nesting, and multiple exit paths. Three are tagged as god functions. That combination — many paths through the function, deep nesting that makes those paths hard to trace, and multiple return points that make coverage hard to verify — is the signature of code that is expensive to change safely.
GetValueOp — src/tweens/builders/GetValueOp.js
GetValueOp sits at the heart of Phaser’s tween system. Its job, as the source excerpt makes clear, is to interpret the many forms a tween property value can take — a plain number, an array of keyframes, a string operator like '+=400' or 'random(10, 100)' — and return the appropriate getEnd, getStart, and getActive callbacks for the tween engine to invoke at runtime.
The problem is that this single function has to handle every one of those input shapes through a deeply nested chain of typeof checks, Array.isArray guards, substring parsing, and brace-matching logic. That produces a cyclomatic complexity of 34 — more than three times the threshold where splitting becomes worthwhile — nested to a maximum depth of 9. Nesting at that level means reasoning about the innermost branches requires mentally tracking at least nine enclosing conditions simultaneously. The 14 fan-out calls compound this: changes here can propagate to 14 distinct callees, and in JavaScript’s dynamic dispatch environment that number likely understates the true coupling.
The exit-heavy and god-function patterns both fire here. Multiple early returns, combined with the function’s role as a universal interpreter for tween property syntax, mean that adding or modifying any one input shape risks disturbing the others.
The most direct refactoring move is to extract each input type into its own named handler — a numberValueOp, arrayValueOp, stringOperatorValueOp — and reduce GetValueOp to a dispatcher that delegates to whichever handler matches. That would bring cyclomatic complexity per unit well below 10, make each path independently testable, and contain the blast radius of future tween API changes.
PCTDecode — src/textures/parsers/PCTDecode.js
PCTDecode is a full text-format parser for Phaser’s PCT texture atlas format, and the source excerpt shows just how much it takes on in one function: header validation, version parsing, CRLF normalization, a multi-pass line loop, pending-block state management, folder expansion, and frame coordinate calculation — all interleaved in a single body.
At CC 29 and nesting depth 9, the branching is about as deep as GetValueOp, but the fan-out here is even higher at 21. That is the broadest coupling of any function in this analysis. In a JavaScript parser that relies on string manipulation utilities, index math, and format-specific helpers, 21 distinct callees means a change to any one of those helpers — or to the PCT format spec itself — creates a wide surface of potential breakage inside this function.
The pending-block state variable is a specific concern visible in the excerpt: it creates implicit ordering dependencies between iterations of the line loop. Code that depends on the value of a variable set in a previous loop iteration is notoriously hard to reason about under change.
I would recommend splitting PCTDecode into at least three pieces: a header-and-version validator, a line tokenizer/normalizer, and a frame-layout resolver that consumes the tokenized output. The pending-block pattern should become an explicit state machine or a two-pass approach rather than a flag threaded through a single loop. That decomposition would also make the CRLF normalization trivially unit-testable in isolation.
GetValue — src/utils/object/GetValue.js
GetValue is a utility for deep property lookup with a fallback. The source excerpt shows it handles at least four distinct cases: null/numeric source short-circuit, direct hasOwnProperty hit on a primary source, direct hit on an alternate source, and dotted-key traversal across both sources simultaneously — with its own early-exit logic for each found/not-found combination at each depth level.
The hub-function pattern fires here, which is the right read: GetValue is almost certainly called throughout the codebase as the standard way to pull configuration values out of nested objects. That makes it a high-blast-radius function even at a moderate fan-out of 6, because the fan-in (the number of places that call it) is almost certainly very large.
CC 18 with nesting depth 6 is the result of trying to handle both the single-source and dual-source traversal cases in one body, with a hand-rolled dot-notation parser layered on top. A concrete first step: separate the dotted-key traversal into its own GetDeepValue(source, keys, defaultValue) helper, then let GetValue call it. That alone would cut the cyclomatic complexity roughly in half and make the dotted-key logic independently testable — important given that edge cases in deep property access (missing intermediate keys, prototype-chain collisions) are subtle.
GraphicsWebGLRenderer — src/gameobjects/graphics/GraphicsWebGLRenderer.js
GraphicsWebGLRenderer has the highest raw cyclomatic complexity in this set at 60 — six times the moderate threshold. The source excerpt shows why: it is a command-buffer interpreter. It walks an array of drawing commands and dispatches each through a large switch statement, with separate path logic for BEGIN_PATH, CLOSE_PATH, FILL_PATH, and presumably many more drawing primitives. Each case is its own mini-state machine that modifies shared variables like path, lastPath, currentMatrix, and pathOpen.
The nesting depth of 5 is actually lower than the other hotspots, which reflects the fact that the switch keeps individual cases flatter than chained if-else trees. But at CC 60, the sheer number of independent execution paths — each a potential test case — is the dominant concern. Fan-out of 18 means the renderer is directly invoking 18 distinct callees, including render node dispatch calls like customRenderNodes.FillPath || defaultRenderNodes.FillPath that introduce runtime polymorphism on top of the static coupling count.
This function was touched 1 time in the last 30 days and modified just 1 day ago. Any engineer adding a new drawing primitive or modifying path-close behavior right now is doing so inside a 60-path function with shared mutable state. The god-function and long-function patterns both apply.
The appropriate refactoring here is to extract each switch case — or at minimum each logical group of cases — into its own handler function, and to make the shared mutable state (path, currentMatrix, etc.) explicit as a context object passed between handlers rather than closed-over variables. That would reduce the per-handler cyclomatic complexity to single digits and make individual drawing primitives testable without constructing a full command buffer.
IsometricCullTiles — src/tilemaps/components/IsometricCullTiles.js
IsometricCullTiles determines which tiles in an isometric tilemap layer are visible within the camera frustum and need to be rendered. The source excerpt shows it handles four traversal orders — right-down, left-down, right-up, and presumably left-up — each implemented as its own pair of nested for loops with identical inner filter logic checking tile existence, index, visibility, alpha, and isometric bounds.
CC 41 with nesting depth 7 is the result of that repetition. The traversal-order branching at the top level, each branch containing double-nested loops with multi-condition guards, creates a large number of independent execution paths. What makes this function stand out from the others is its extremely low fan-out of 2. The complexity here is almost entirely internal — a self-contained maze of loops and conditionals rather than a coordination hub.
The exit-heavy pattern applies to the inner loop: each tile goes through a series of guard conditions with continue statements before being pushed to the output array. That logic is identical across all four traversal orders, which is both the problem and the solution — the inner filter should be extracted into a shouldIncludeTile(tile, x, y, layer, camera, skipCull) predicate, and then each traversal order becomes a simple loop body that calls it. That extraction alone would drop the cyclomatic complexity substantially and make the culling predicate independently testable for edge cases like zero-alpha tiles or out-of-bounds indices.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
complex_branching | 5 |
deeply_nested | 5 |
exit_heavy | 5 |
long_function | 4 |
god_function | 3 |
hub_function | 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/phaserjs/phaser
cd phaser
git checkout 1b8977a62fe2e7b632a57fa1c42a4377e0f6e364
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 →