cyrill23 downloadsStandalone WebGL graph view with a worker-based force layout that stays responsive in huge vaults by collapsing large folders into metadata nodes.
A standalone alternative to Obsidian's graph view, built to stay responsive in vaults where the built-in view drops to a slideshow.
Its defining rule: a folder holding more than a configurable number of files (default 20) is drawn as one metadata node instead of one node per note. Everything else behaves the way the built-in graph view does — same filters, same forces, same colour groups, same right-click menus, same hover preview rules, familiar controls, its own saved configuration, and the same app language.
Measured on a 27,266-note vault (desktop Obsidian 1.14.4): 1,546 drawn nodes · 4,795 links · 24 folder clusters holding 26,942 notes · model build 42 ms · one frame 0.4–0.5 ms.
For a complete user-focused walkthrough, see the Fast Graph View User Guide.
Install Fast Graph View from Settings → Community plugins, or copy main.js, styles.css, and manifest.json from a release (or local build) into:
<vault>/.obsidian/plugins/fast-graph-view/
Then enable Fast Graph View under Settings → Community plugins.
Open Settings → Fast Graph View and turn on Allow vault-wide metadata access. Vault-wide enumeration is off by default (including after upgrades) until explicitly enabled.
Ctrl/⌘+P).Escape, click Stop, or run Stop graph timelapse animation to halt it.Folders over the threshold collapse into a single node sized by what it stands for and labelled <name> (<count>). Hovering a cluster node shows a summary card with rolled-up counts: files directly in the folder and in subfolders, incoming links, outgoing links, internal links not drawn, and total file size.
maxVisibleNodes, default 2500): acts as a hard ceiling. After the threshold rule runs, the heaviest remaining folders fold until the drawn node count fits, so a 100k-note vault still opens cleanly.mode): direct counts only files immediately inside the folder; recursive counts the entire subtree, which works better for deeply nested vaults.maxMetadataNodes, default 1200): tag and unresolved-link nodes are capped separately, since in large vaults they are the other two ways a graph becomes unreadable.| Action | Behaviour |
|---|---|
| Mouse wheel | Zooms anchored under the cursor |
| Drag empty canvas | Pans the viewport |
| Drag a node | Pins the node for the duration of the drag and releases it on pointerup so it rejoins the layout (matching native forceNode behaviour) |
| Hover a node | Highlights connected neighbours and dims the rest of the graph |
Ctrl/⌘ + hover a note |
Opens Obsidian's page preview via the registered hover source (defaultMod: true) |
| Click a note | Opens the note in the same leaf (with navigation history back to the graph) |
Ctrl/⌘-click or middle-click a note |
Opens the note in a new tab |
Ctrl/⌘+Alt-click a note |
Opens the note in a split pane |
Ctrl/⌘+Alt+Shift-click a note |
Opens the note in a new window |
| Click a folder cluster | Expands up to 25 notes from that folder into the graph |
| Right-click a folder cluster | Folds expanded notes back into the cluster |
| Click a tag node | Filters the current graph to that tag |
| Control panel | Filters, colour groups (including drag-to-reorder), display options, and force sliders matching the built-in graph view |
On Obsidian Mobile (Android, iPhone, and iPad):
The filter box supports the native graph's query syntax plus is:cluster for folder nodes:
"exact phrase" — exact substring matchtag:#name or #name — notes with the given tagpath:folder or folder:folder — notes under a folder pathfile:name or name:name — match file nameext:md — match file extensionlink:target or to:target — notes linking to targetbacklink:source or from:source — notes linked from sourceis:orphan — notes with no linksis:cluster — folded folder cluster nodes-term — negate any term or operatorConfigure clustering and rendering budgets in Settings → Fast Graph View:
| Setting | Option key | Default | Description |
|---|---|---|---|
| Allow vault-wide metadata access | allowVaultEnumeration |
false |
Required opt-in before the plugin lists vault file paths or reads cached links, tags, sizes, and dates. |
| Fold up large folders | cluster.enabled |
true |
Collapse folders above the file threshold into single metadata nodes. |
| Files per folder before folding | cluster.threshold |
20 |
Minimum number of notes in a folder before it collapses into a cluster node (1–200 on the slider). |
| Count notes | cluster.mode |
direct |
Count notes directly inside the folder (direct) or across its entire subtree (recursive). |
| Node budget | cluster.maxVisibleNodes |
2500 |
Hard ceiling on drawn nodes; the largest remaining folders fold until the graph fits. |
| Tag and unresolved link limit | cluster.maxMetadataNodes |
1200 |
Maximum number of non-file metadata nodes (tags and unresolved links) shown at once. |
| Shorten long names | cluster.maxLabelChars |
24 |
Truncate drawn canvas labels to this many characters (0 disables truncation; hovered nodes always show their full name). |
| Show folder details on hover | cluster.showMetadata |
true |
Display the summary card with note counts, link weights, and folder size when hovering a cluster. |
| Show node and frame counts | cluster.showStats |
true |
Show the status line in the bottom corner with drawn node/link counts and build/draw timings. |
Fast Graph View registers its own fast-graph-view and fast-localgraph view types through Obsidian's public plugin API. It does not unregister, replace, or modify the built-in graph plugin.
data.json.| Layer | What it does |
|---|---|
src/graph/source.ts |
Reads links out of metadataCache only. No file is ever read from disk, so a cold build costs one pass over cached frontmatter/link lists. |
src/graph/build.ts |
Produces a structure-of-arrays model (Float32Array positions, Int32Array kinds/paths, CSR adjacency). Nothing per-node is allocated while drawing. |
src/layout/* |
Barnes-Hut force simulation running in a blob worker, with an inline time-sliced fallback when workers are unavailable. The main thread receives position buffers and never touches the quadtree. |
src/render/renderer.ts |
One WebGL2 instanced draw call per primitive: SDF discs for nodes, segments for links, arrowheads, selection rings. Labels are a separate 2D overlay that only redraws when the visible set changes. |
src/view/engine.ts |
The frame loop stops. Once the layout alpha is under 0.001 and 60 frames have painted nothing, requestAnimationFrame is unscheduled; the next wheel, pointer, or filter event wakes it. Idle cost is zero, not 60 fps of nothing. |
Deliberate departures from the built-in view, and why:
Escape does nothing and Animate only restarts it. Here the start
control is the stop control while the reveal runs: Animate becomes Stop under Display,
the collapsed panel's wand becomes a square, Escape halts it, and a
Stop graph timelapse animation command joins the palette for exactly as long as there is
something to halt, so it can be bound to a hotkey. It also ends when every note has been
shown.metadataCache, which
does not index search over content; adding it would mean reading every file, which is the
thing this plugin exists to avoid.main.js, so the three-file install contract is
unchanged and the panel, settings tab, hover card, status line, commands and canvas folder
labels all follow the app language. Counts are pluralised per language through
Intl.PluralRules, which matters for Russian and Polish and for French treating zero as
singular. A traditional-Chinese Obsidian falls back to English rather than to Simplified,
since the two are not interchangeable scripts. The few diagnostics interpolated into a
fallback status message — a WebGL failure reason, a worker crash — stay in English, as does
any exception text the browser itself supplies.is-loading spinner in the filter field. The
model rebuild is 42 ms at 27k notes, which is not a wait worth a spinner.theme-dark/theme-light is read from the body class, matching how the native view
picks its palette.maxLabelChars (default 24), which native does not do. A vault
whose median title is 37 characters — and whose unresolved links are labelled with their
whole linktext, path included — otherwise overlaps 900 px strings across the graph. Only the
canvas sees the cut: filters, node identity, hover preview and the file menu keep the full
label, and the hovered node is always drawn whole. 0 turns shortening off.isModEvent is reimplemented locally as modEventPaneType (the helper is not exported).file, so reopening a saved local graph does not silently re-root.defaultMod: true; plain hover only emphasizes graph neighbors.Requires Node.js >=22.0.0 and pnpm.
pnpm install
pnpm build # esbuild production bundle + tsc --noEmit
pnpm test # unit & security regression tests
pnpm lint # ESLint check (including eslint-plugin-obsidianmd)
pnpm check:release # verify manifest.json, package.json, versions.json, and API rules
The built plugin consists of the three files at the repository root: main.js, styles.css, and manifest.json. pnpm watch rebuilds on change.
For a ready-to-run local vault, use the checked-in test-vault:
pnpm install
pnpm build
pnpm deploy
Open test-vault in Obsidian, enable the plugin, and start the live development loop:
pnpm dev:vault
Launch Obsidian with --remote-debugging-port=9223 for automatic plugin reloads after each build. Set OBSIDIAN_VAULT=/path/to/another/vault to deploy to a different vault.
tools/)tools/ is a verification harness and is not bundled into the plugin. tools/obs-cdp.mjs drives a running Obsidian renderer over the Chrome DevTools Protocol (--remote-debugging-port=9223) with trusted Input events, and tools/harness/*.mjs contains the test scenarios:
trusted.mjs — hover, click, right-click, wheel, pan, node drag, idle-loop, click navigationpanel.mjs — control panel interactionslocal.mjs — local graph behaviourstandalone.mjs — standalone registration and teardowncrash-watch.mjs — renderer reload journalingLaunch Obsidian with background throttling disabled so Chromium keeps producing frames even when another window covers it (otherwise requestAnimationFrame pauses and timing measurements reflect frame-sink starvation):
flatpak run md.obsidian.Obsidian \
--user-data-dir=<harness profile> --remote-debugging-port=9223 \
--disable-backgrounding-occluded-windows --disable-renderer-backgrounding \
--disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion
Version bumps synchronize package.json, manifest.json, and versions.json via the version script in package.json (with tag-version-prefix="" in .npmrc so tags match Obsidian's 1.2.3 format):
pnpm version patch # Bug fixes (e.g. 1.0.1 -> 1.0.2)
pnpm version minor # New features (e.g. 1.0.1 -> 1.1.0)
pnpm version major # Breaking changes (e.g. 1.0.1 -> 2.0.0)
Verify the release metadata and built assets locally, push the commit and tag to GitHub, and publish the release with the three plugin files (main.js, manifest.json, and styles.css):
pnpm check:release
pnpm build
pnpm test
git push && git push --tags
gh release create <version> main.js manifest.json styles.css --title "<version>" --generate-notes
This plugin is licensed under the GNU General Public License v3.0 (GPLv3) with an additional linking exception under Section 7 allowing it to be linked with and run inside the proprietary Obsidian application. See the LICENSE file for full details.