Across 6,357 functions in GoogleChrome/lighthouse, 275 sit in the critical band — and five of those are in the fire quadrant, meaning they combine high structural complexity with recent commit activity. I would start with _setPerfGaugeExplodey in report/renderer/explodey-gauge.js: at an activity-weighted risk of 15.09, a cyclomatic complexity of 23, and a fan-out of 71, it was touched once in the last 30 days — making it a live regression risk, not a cleanup item. Behind it, audit in core/audits/baseline.js carries an activity-weighted risk of 13.02 and was modified as recently as today, meaning any engineer shipping to that file this week is working in actively shifting ground.
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 |
|---|---|---|---|---|---|
_setPerfGaugeExplodey | report/renderer/explodey-gauge.js | 15.1 | 23 | 3 | 71 |
audit | core/audits/baseline.js | 13.0 | 11 | 2 | 12 |
audit | core/audits/agentic/llms-txt.js | 11.0 | 11 | 2 | 5 |
audit | core/audits/seo/robots-txt.js | 10.9 | 12 | 2 | 4 |
collectCanonicalURLs | core/audits/seo/canonical.js | 10.5 | 13 | 4 | 3 |
Quadrant distribution
6,357 functions analyzed
The quadrant picture tells two distinct stories. The 20 fire-quadrant functions are the immediate concern — structurally complex and being actively changed right now. The 821 debt-quadrant functions are the slower-burning problem: high structural complexity that hasn’t been touched recently but will carry serious blast-radius risk whenever the next development push arrives. The 5,495 ok-quadrant functions confirm that most of the codebase is stable and low-complexity — the risk is genuinely concentrated.
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.God Function×2God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×2Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Complex Branching×1Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.
Four of the five top hotspots are flagged as exit-heavy — meaning they contain multiple independent return paths that each require their own test case to exercise. Two are also flagged as god functions, which amplifies the blast-radius concern: a single function calling 71 other functions is a coordination point where a small change can propagate unexpectedly far.
Function-by-Function Analysis
_setPerfGaugeExplodey — explodey-gauge.js
This function renders lighthouse’s experimental SVG performance gauge — the one that breaks down individual Core Web Vitals metrics as arc segments on a semicircular dial. From the source excerpt I can see it handles geometry calculations, DOM manipulation for SVG elements, arc length computation, metric weighting, and element reuse all in one body. The code itself contains comments acknowledging hacks (// Hack, // HACK:This isn't ideal but it was quick), and it manually queries, creates, or reuses SVG child elements for each metric in a forEach loop — a pattern that fuses initialization and update logic in the same execution path.
The fan-out of 71 is the number that demands attention. In JavaScript, where dynamic property access and prototype-based dispatch can obscure dependencies that static analysis can’t fully resolve, 71 detected callees likely understates the true coupling. A cyclomatic complexity of 23 means there are at least 23 independent execution paths to test, and the god-function and long-function patterns confirm this is doing far more than one thing. It was touched once in the last 30 days — 28 days ago — so this is an actively changing file carrying live regression risk.
The concrete recommendation here is extract-method refactoring: pull the metric-group DOM creation and update logic into a dedicated _renderMetricArc helper, and separate the geometry/trigonometry setup (which already delegates to _determineTrig) from the DOM mutation pass. That alone would cut the fan-out materially and make the initialization-vs-update branching explicit rather than implicit in maybeFind fallbacks.
audit — baseline.js
This static async method is the entry point for lighthouse’s Baseline audit, which scans Chrome trace events to identify which web platform features a page uses and then reports their cross-browser baseline status. The source excerpt shows it filtering blink.webdx_feature_usage trace events, deduplicating them into a Map, resolving source locations from line/column numbers, then calling Baseline.getFeatureStatus per feature and assembling a results table.
With a cyclomatic complexity of 11 and two distinct for loops each containing early-continue guards, it carries multiple exit paths — the exit-heavy and god-function flags are both present. More pressing: it has been touched 2 times in the last 30 days and was modified 0 days ago, meaning it changed today. That makes it the single most active function in this list by recency. Any engineer landing a PR against baseline.js right now is modifying code that is structurally mid-complexity and accumulating logic quickly as the Baseline feature set expands.
The actionable step is to extract the trace-event filtering and deduplication into a separate _extractBaselineFeatures(trace) helper before this function grows further. That would isolate the data-gathering logic from the audit-result-assembly logic and make each independently testable.
audit — agentic/llms-txt.js
This function lives in the agentic/ subdirectory — a path that signals this is part of lighthouse’s emerging audit surface for AI-agent-oriented site features. Specifically it audits the presence and structure of an llms.txt file, a convention for signalling to LLM crawlers what a site contains. The source excerpt shows a clear waterfall of early returns: error message present → score 0; no status → score 0; server error range → score 0; client error range → not applicable. Then it validates content structure with three regex checks (H1 heading, markdown link, minimum length) and accumulates errors into a table.
The exit-heavy pattern here is the structural risk: 11 cyclomatic complexity with at least five distinct return sites means the function’s full behavior requires 11 test cases to exercise completely. Because this is an agentic audit — an area the team is actively building out — it has been touched 2 times in the last 30 days and was last changed 12 days ago. The agentic/ path naming suggests this audit is not yet settled API.
The most targeted improvement is decomposing the HTTP status waterfall into a _checkHttpStatus(status, content) helper that returns a result-or-null, keeping the content-structure validation logic clearly separated. That reduces the number of exit paths in the top-level audit method and makes the HTTP status handling independently testable.
audit — seo/robots-txt.js
The robots.txt audit function validates a page’s robots.txt response — checking HTTP status codes, handling empty content, flagging fetch errors, and then running the actual directive validation through a validateRobots call. The source excerpt reveals a similar exit-heavy waterfall to llms-txt.js: server error → score 0; client error or empty content → not applicable; error message → score 0; no status → score 0. A combined || condition checking both status >= HTTP_CLIENT_ERROR_CODE_LOW and content === '' is one of the more subtle paths and a potential source of confusion under future maintenance.
With a cyclomatic complexity of 12, one more branch than the llms-txt.js audit, and only one touch in the last 30 days (12 days ago), this is less active than baseline.js but structurally comparable. The shared structural shape between robots-txt.js and llms-txt.js — both being HTTP-fetch audits with a status-waterfall pattern followed by content validation — is worth noting: if the team extracts a shared _checkFetchedResource(status, content, errorMessage) utility, both audits could delegate their early-exit logic to it, reducing duplicated branching across two critical-band functions simultaneously.
collectCanonicalURLs — seo/canonical.js
This static method collects and classifies canonical and hreflang link elements from a page, separating absolute canonical URLs, relative canonical links, invalid canonical links, and alternate hreflang URLs into distinct output sets. The source excerpt shows a for...of loop over link elements with nested if/else branching that reaches a maximum nesting depth of 4 — the deepest in this top-five group.
The complex-branching flag is earned: at ND 4 and CC 13, a reader has to track whether they’re inside a canonical branch, a valid-with-base branch, a valid-without-base branch, or an alternate-hreflang branch simultaneously. The logic distinguishes between URL.parse(hrefRaw, base) === null (syntactically invalid even with a base) versus URL.parse(hrefRaw) === null (valid only with a base, therefore relative) — a subtle distinction that is easy to regress. It was touched once in the last 30 days, 28 days ago.
The concrete recommendation is to extract the inner URL-classification logic — the nested URL.parse calls and their four outcome branches — into a _classifyCanonicalLink(link) function returning a discriminated union type. That flattens the nesting in the loop body to a single dispatch and makes the four classification outcomes explicit and individually testable.
The broader picture
The five context-only functions — getArtifact in webmcp.js, renderTextURL and _createLink in details-renderer.js, getRequestForScript in script-helpers.js, and log in logger.js — are all in the watch quadrant: active within the last 30 days but low structural complexity. None warrants refactoring attention right now, but renderTextURL hitting a cyclomatic complexity of 10 (the moderate threshold) is worth keeping an eye on if details-renderer.js continues to accumulate changes.
The 821 debt-quadrant functions are the longer-term concern. They carry high structural complexity but have not been touched recently — when the next development push lands in any of those files, it will arrive with elevated blast-radius risk and no recent test-coverage pressure to offset it.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 4 |
god_function | 2 |
long_function | 2 |
complex_branching | 1 |
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/GoogleChrome/lighthouse
cd lighthouse
git checkout 1e1e01613a41345c799c1e776d657eb6c3811230
hotspots analyze . --mode snapshot --explain-patterns --force
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 →