Canvas Image LOD

by Ilia Gorbunov
5
4
3
2
1
Score: 50/100

Description

Zoom-aware level of detail for images on Obsidian Canvas

Reviews

No reviews yet.

Stats

0
stars
24
downloads
0
forks
4
days
4
days
4
days
0
total PRs
0
open PRs
0
closed PRs
0
merged PRs
0
total issues
0
open issues
0
closed issues
4
commits

README file from

Github

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 <img> 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 <vault>/.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.

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