wave6 follow-up: gatsby-0001 — three filter-cache builders nodeTypeNames.includes -> Set

The Gatsby authors annotated each of the three call sites in
in-memory/indexing.ts with 'expensive at scale' comments. Their
diagnosis is correct: nodeTypeNames.includes(node.internal.type)
inside iterateNodes().forEach is O(N*T) per cache build.

For N=100k+ nodes typical of mature content sites and T=10-30
declared types per query, this fires on every type-filtered query.
gatsby develop in particular rebuilds caches per page render.

Fix: hoist Set<string> once at the top of each function. O(1) per
node lookup. Total cost O(N+T). Bench shows 8.4x at N=100k T=50;
2.7-4.7x at smaller scales.

Three call sites patched: ensureIndexByElemMatch (line 326),
ensureEmptyFilterCache (378), ensureIndexByElemMatchValue (504).
Author 'expensive at scale' comments updated to record the fix.
This commit is contained in:
russell@unturf.com 2026-04-25 10:15:32 -04:00
parent cd28454d2d
commit a150602100
No known key found for this signature in database
8 changed files with 285 additions and 1 deletions

View file

@ -0,0 +1,80 @@
# gatsby-0001: in-memory indexing — O(N×T) nodeTypeNames.includes per node walk
**Target:** gatsbyjs/gatsby
**Severity:** MEDIUM-HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**MOAD:** MOAD-0001 (A Sedimentary Defect)
**File:** `packages/gatsby/src/datastore/in-memory/indexing.ts:326, 378, 504`
**Language:** TypeScript
**Status:** open
## Description
Three filter-cache builders in Gatsby's in-memory datastore walk the full node store and check each node's type against a list of declared `nodeTypeNames` via `Array#includes`. The Gatsby authors annotate two of the three call sites with the comment `// This loop is expensive at scale (!)` and `// Expensive at scale` — they know the pattern is hot but the lookup is still a linear scan.
For N nodes (a typical content-heavy Gatsby site has 50K-500K nodes — every Markdown file, image, frontmatter object, GraphQL-introspected source becomes a node) and T type-names per query (typically 5-30 declared types per filter), per-cache-build cost is O(N×T).
## Root Cause
```typescript
// indexing.ts:323-336 — ensureIndexByElemMatch
} else {
// Here we must first filter for the node type
// This loop is expensive at scale (!)
getDataStore()
.iterateNodes()
.forEach(node => {
if (!nodeTypeNames.includes(node.internal.type)) { // O(T) per node
return
}
addNodeToFilterCache({ node, chain: filterPath, ... })
})
}
// indexing.ts:372-384 — ensureEmptyFilterCache (same pattern)
} else {
// Here we must first filter for the node type
// This loop is expensive at scale (!)
getDataStore().iterateNodes().forEach(node => {
if (nodeTypeNames.includes(node.internal.type)) { ... }
})
}
// indexing.ts:499-516 — ensureIndexByElemMatchValue (same pattern)
} else {
// Expensive at scale
getDataStore().iterateNodes().forEach(node => {
if (!nodeTypeNames.includes(node.internal.type)) { return }
addNodeToBucketWithElemMatch({ ... })
})
}
```
`Array#includes` is O(T) per call. Across N node iterations: O(N×T) per cache build. Filter caches are built per query, every page render in develop mode triggers more.
## Fix
Convert `nodeTypeNames` to a `Set<string>` at the top of each function. Per-iter cost drops to O(1).
```typescript
// In each function, before the forEach loop:
const nodeTypeNameSet = new Set(nodeTypeNames);
getDataStore().iterateNodes().forEach(node => {
if (!nodeTypeNameSet.has(node.internal.type)) { // O(1)
return;
}
...
});
```
Total cost drops from O(N×T) to O(N+T) per cache build. The Set-build cost (O(T)) is amortized over the N-node walk.
## Severity Note
Hot path on every Gatsby site build. Every developer running `gatsby develop` or `gatsby build` pays this on every query that filters by type. Large sites (Smashing Magazine-scale content sites with 100K+ pages) hit O(N×T) on every cache rebuild. The Gatsby authors flagged this in source comments — the data shape is acknowledged as a problem; the fix is a one-line hoist.
## Complexity Gate
- N=100,000 nodes × T=20 types: fixed must complete in <50ms
- k-scaling 5×: time ratio must be <17.5×