Problem
collectRawPathReferences builds and runs one RegExp per candidate path, per document:
function hasRawPathReference(content: string, path: string): boolean {
const pattern = new RegExp(`(^|[^A-Za-z0-9_./-])${escapeRegExp(path)}(?=$|[^A-Za-z0-9_./-])`)
return pattern.test(content)
}
function collectRawPathReferences(content: string, availablePaths: string[]): string[] {
const normalizedContent = content.replace(/\\/g, '/')
return availablePaths.filter((path) => hasRawPathReference(normalizedContent, path))
}
It is called from for (const sourcePath of filePaths), so the whole graph build is N² regex compilations and N² full-content scans over the vault.
This runs synchronously inside a useMemo on the main thread, and it re-runs on every autosave while the graph panel is open. The invalidation chain:
knowledge-graph-panel.tsx:338 graph = useMemo(..., [agentSources, contentByPath, markdownPaths, openFileContentByPath])
knowledge-graph-panel.tsx:333 openFileContentByPath = useMemo(..., [openFiles])
workspace-shell.tsx:1365 openFiles = useMemo(..., [openFilePaths, fileCache])
workspace-shell.tsx:1051 handleSaveFile → setFileCache(...)
use-editor-drafts.ts:29 debounceMs = 600
Location
apps/web/src/lib/kb-graph.ts:75-95 (hasRawPathReference, collectRawPathReferences), called from :139-155
Impact
Measured by copying the three functions verbatim into an isolated benchmark:
| vault size |
regex compiles |
actual (ms) |
with precompiled regex (ms) |
speedup |
| 50 |
2 500 |
16.5 |
5.9 |
2.8x |
| 100 |
10 000 |
76.3 |
36.0 |
2.1x |
| 250 |
62 500 |
450.3 |
256.5 |
1.8x |
| 500 |
250 000 |
1 935.8 |
1 184.5 |
1.6x |
| 1 000 |
1 000 000 |
9 491.6 |
5 164.6 |
1.8x |
At 500 notes that is a ~2 s synchronous freeze of the whole tab, every time the editor autosaves. At 1 000 notes, ~9.5 s.
Suggested fix
Precompiling the regexes out of the loop is not enough — measured at only 1.6-1.8x, because the dominant cost is the N² content scans, not the compilations.
The fix has to be algorithmic: scan each document once, extracting path-shaped tokens, and resolve those against a Set of known paths. That turns O(N² x length) into O(N x length).
Better still, and it also solves the companion issue about the panel downloading the whole vault: build the graph server-side and return it from an endpoint, so neither the O(N²) nor the vault content ever touches the browser main thread.
Source: independent verification pass (Claude Opus 5), measured locally. Not part of the HN-001..HN-063 batch.
Problem
collectRawPathReferencesbuilds and runs oneRegExpper candidate path, per document:It is called from
for (const sourcePath of filePaths), so the whole graph build is N² regex compilations and N² full-content scans over the vault.This runs synchronously inside a
useMemoon the main thread, and it re-runs on every autosave while the graph panel is open. The invalidation chain:Location
apps/web/src/lib/kb-graph.ts:75-95(hasRawPathReference,collectRawPathReferences), called from:139-155Impact
Measured by copying the three functions verbatim into an isolated benchmark:
At 500 notes that is a ~2 s synchronous freeze of the whole tab, every time the editor autosaves. At 1 000 notes, ~9.5 s.
Suggested fix
Precompiling the regexes out of the loop is not enough — measured at only 1.6-1.8x, because the dominant cost is the N² content scans, not the compilations.
The fix has to be algorithmic: scan each document once, extracting path-shaped tokens, and resolve those against a
Setof known paths. That turns O(N² x length) into O(N x length).Better still, and it also solves the companion issue about the panel downloading the whole vault: build the graph server-side and return it from an endpoint, so neither the O(N²) nor the vault content ever touches the browser main thread.
Source: independent verification pass (Claude Opus 5), measured locally. Not part of the HN-001..HN-063 batch.