# Canvas Image LOD
Removes zoom lag in Obsidian Canvas on boards with many large images.
**Your image files are never modified.**
## The problem
Obsidian Canvas is plain DOM. Every time the zoom level changes, Chromium
re-rasterizes each image **from its source resolution**. A 4800×3000 photo drawn
150 px wide on screen is still recomputed from 14 megapixels — on every zoom
step, for every image in view.
Measured on one real board:
| | |
|---|---|
| Images | 104 |
| On disk | 372 MB |
| Total | 307 megapixels |
| Decoded as RGBA | **~1.2 GB** |
That gigabyte is re-rasterized as you zoom. It is the whole reason the board
stutters.
## What the plugin does
It builds downscaled proxies of each image and feeds every `
` the one that
matches its real on-screen size. Zoom out and a 128 px copy is drawn; zoom in
and the original comes back.
Simulated against the same board (tiers `128/320/768/1600`, headroom 1.15):
| Zoom | Megapixels before | After | Gain |
|------|-------------------|-------|------|
| 10% | 307 | 14.8 | 20.8× |
| 25% | 307 | 18.7 | 16.4× |
| 50% | 307 | 22.5 | 13.6× |
| 100% | 307 | 51.2 | 6.0× |
| 150% | 307 | 122.0 | 2.5× |
Numbers come from one vault. Yours will differ with image sizes and board layout.
## Design notes
**Zoom is read from the transform matrix** (`getComputedStyle().transform`)
rather than from canvas internals, so Obsidian updates do not break it.
**Each image is fully decoded exactly once.** Tiers are then built as a chain —
1600 from the original, 768 from 1600, 320 from 768 — which is both faster and
sharper than jumping straight from 4800 to 128.
**Upgrades are deferred until the gesture ends.** Downgrades apply immediately
because the proxies are already in memory; originals are only reloaded 200 ms
after you stop moving, so heavy files never load mid-zoom.
**DOM reads and writes are split into phases.** All sizes are measured first,
then all swaps applied — otherwise the browser would recompute layout once per
image.
**Node widths are cached in a `WeakMap`** and refreshed through a
`ResizeObserver`, so `offsetWidth` is never touched during a frame.
**Prewarm walks the board's JSON, not the DOM.** Obsidian unmounts distant
nodes, so they are not in the DOM to be found; this way proxies are ready for
images you have not scrolled to yet.
**Files are read through `vault.readBinary`**, falling back to `fetch` for
external URLs.
## Storage
Proxies live in **IndexedDB inside your Obsidian profile — not in your vault**.
This matters: on a large vault the proxy set runs to roughly 100 MB. Kept under
`.obsidian`, all of it would be pushed through Obsidian Sync, count against your
storage quota, and — because cache keys include each file's mtime — quite
possibly arrive useless on the other machine anyway. It is derived data, and it
is cheaper to rebuild than to carry.
Memory is bounded separately. Every object URL keeps its blob alive, so entries
are evicted least-recently-used once the RAM budget is exceeded; anything
currently on screen is pinned and never revoked. Evicted proxies are reloaded
from IndexedDB without re-decoding.
Both budgets are configurable, and cache keys invalidate themselves when you
edit an image.
## Install
Copy `main.js`, `manifest.json` and `styles.css` into
`/.obsidian/plugins/canvas-image-lod/`, then enable the plugin under
**Settings → Community plugins**.
## Commands
- Toggle proxy swapping
- Build proxies for the current canvas
- Restore original images on the current canvas
- Clear proxy cache
## Limitations
- Desktop only. WebKit cannot encode WebP from a canvas and silently returns
PNG, `OffscreenCanvas` needs Safari 16.4+, and decoding large images on a
phone risks the OS killing the WebView. Mobile support needs its own work.
- SVG and video nodes are left alone.
- Text and markdown cards are untouched. This plugin fixes raster cost, not DOM
cost — a board of 500 text cards will be exactly as slow as before.
- Exporting a canvas to an image restores originals first and waits for them to
load, but it hooks the export command by name pattern. If that command is
renamed upstream, use **Restore original images on the current canvas**
manually before exporting.
## Development
No build step — `main.js` is plain CommonJS, edit and reload.
```bash
cd test && npm install && npm test
```
Three suites:
- `loadtest.js` — the module loads, `onload`/`onunload` survive
- `bindtest.js` — deliberately broken canvas leaves never break plugin loading
- `cachetest.js` — IndexedDB round trip and pruning, memory eviction and
pinning, the export wrapper, legacy cache removal, settings rendering
`simulate.js` measures the level-of-detail win against a real board; edit the
vault path at the top before running it.
## License
MIT