lighthouse's audits and renderer carry the highest activity risk — 5 to address first

Five critical-band functions spanning lighthouse's renderer and audit core are both structurally complex and actively changing, with _setPerfGaugeExplodey carrying a fan-out of 71 and an activity-weighted risk of 15.09.

Stephen Collins ·
Generated by hotspots · free & open source
Activity Risk15.09Low
Hottest Function_setPerfGaugeExplodey

Antipatterns Detected

exit_heavy4god_function2long_function2complex_branching1

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 an exit-heavy function and why does it matter in lighthouse?

An exit-heavy function contains multiple independent return statements or early-exit paths, each of which represents a distinct execution branch that must be reached by a separate test case to be covered. In lighthouse's audit functions, this pattern appears because audits must handle a waterfall of failure modes — fetch errors, HTTP status codes, missing content, validation failures — before reaching a success path, and each branch is a potential place where incorrect score or display values could be returned. Four of the five top hotspots carry this pattern, including `audit` in `baseline.js` and `audit` in `robots-txt.js`. Practically, this means a test suite that only exercises the happy path leaves the majority of these functions' behavior unverified, and a change to the early-exit ordering can silently alter audit scores for edge-case inputs.

How do I reduce cyclomatic complexity in JavaScript?

The most reliable technique is extract-method refactoring: identify one coherent decision cluster inside a complex function and move it into a named helper with a clear return type. A cyclomatic complexity above 10 is a signal to consider splitting; above 15, splitting is almost always worthwhile. For `_setPerfGaugeExplodey` specifically, which has a cyclomatic complexity of 23, I would start by pulling the per-metric SVG element creation and update logic — currently inlined in a `forEach` — into a `_renderMetricArc(dom, groupOuter, metric, ...)` helper. That single extraction eliminates several branches from the top-level function and makes the initialization-vs-update distinction an explicit parameter rather than an implicit `maybeFind` fallback, cutting the measurable complexity roughly in half while improving readability.

Is lighthouse actively maintained?

Yes, and the commit data confirms it. The `audit` function in `core/audits/baseline.js` was touched twice in the last 30 days and has a days_since_changed of 0 — as of this analysis, it changed today. `audit` in `core/audits/agentic/llms-txt.js` was also touched twice in the last 30 days and last changed 12 days ago, reflecting active development on the new agentic audit surface. The presence of 20 fire-quadrant functions — structurally complex code that is actively changing — is itself a sign of a living, evolving codebase. Active development and structural debt are not mutually exclusive: lighthouse is clearly being extended at pace, which is exactly why the fire-quadrant functions carry genuine near-term regression risk.

How do I reproduce this analysis?

The Hotspots CLI is available at github.com/hotspots-dev/hotspots. This analysis was run against commit `1e1e016` of GoogleChrome/lighthouse. After checking out that commit with `git checkout 1e1e016`, run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root to reproduce the full function-level results. The same command works on any local git repository without additional configuration.

What does activity-weighted risk mean?

Activity-weighted risk combines a function's structural complexity — derived from cyclomatic complexity, nesting depth, and fan-out — with how frequently that function has been touched in recent commits. A function that is structurally complex but hasn't been changed in years scores lower than one that is moderately complex and modified weekly, because the actively-changing function is where bugs are most likely to be introduced right now. For example, `_setPerfGaugeExplodey` has an activity-weighted risk of 15.09 partly because its fan-out of 71 and cyclomatic complexity of 23 create a large structural surface, and partly because it was touched within the last 30 days — meaning that complexity is being navigated by engineers under current development pressure. This prioritization helps teams spend refactoring effort where it lowers the probability of near-term regressions, not just where the code happens to look complicated.

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

FunctionFileRiskCCNDFO
_setPerfGaugeExplodeyreport/renderer/explodey-gauge.js15.123371
auditcore/audits/baseline.js13.011212
auditcore/audits/agentic/llms-txt.js11.01125
auditcore/audits/seo/robots-txt.js10.91224
collectCanonicalURLscore/audits/seo/canonical.js10.51343

Quadrant distribution

Triage Band Distribution
Fire20Debt821Watch21OK5495

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.

Detected Antipatterns
Exit Heavy×4Exit Heavy
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

_setPerfGaugeExplodey
report/renderer/explodey-gauge.js
15.09
critical
CC 23
ND 3
FO 71
touches/30d 1

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.

Fan-Out (distinct function calls) 71
threshold: 15

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

audit
core/audits/baseline.js
13.02
critical
CC 11
ND 2
FO 12
touches/30d 2

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

audit
core/audits/agentic/llms-txt.js
10.95
critical
CC 11
ND 2
FO 5
touches/30d 2

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

audit
core/audits/seo/robots-txt.js
10.88
critical
CC 12
ND 2
FO 4
touches/30d 1

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

collectCanonicalURLs
core/audits/seo/canonical.js
10.54
critical
CC 13
ND 4
FO 3
touches/30d 1

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.

Max Nesting Depth 4
threshold: 4

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:

PatternOccurrences
exit_heavy4
god_function2
long_function2
complex_branching1

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 →

Related Analyses