This one comes straight out of Image Horse, the browser image editor I've been building with a Rust/WASM engine under a React front end. Undo was the feature that forced the issue: my first implementation copied the whole canvas every time you touched it, and the math on that stops being funny fast. A 2048×2048 photo is 16 MB of RGBA. Two hundred edits is 3.2 GB — inside a browser tab.
Most editors that survive this don't store states. They store edits. The cleaner way to see the difference is to watch the memory itself — what gets written on each edit, what undo actually does, and where the bytes are after a long session.
So here's a side-by-side. Same photo, same edits, two history strategies. Step through and watch them diverge.
States vs edits
Both strategies answer "what did the image look like one step ago?" — they just store different things to get there.
Snapshots store states. Every history entry is a complete, self-contained pixel buffer. Undo is a pointer swap, redo is a pointer swap, and the implementation fits in an afternoon. The cost is that memory scales with edit count times image size, and neither of those is under your control. Users bring 45-megapixel photos and then draw for an hour.
An operation log stores edits. Each history entry is a small description of what happened — this stroke, with these points, this brush. The current state is derived: take the nearest keyframe, replay the ops after it. Memory scales with what the user actually did, which for almost any session is a rounding error next to one pixel buffer. In Image Horse the ops serialize through postcard on the Rust side, and replaying a few dozen ops is comfortably under a frame.
The trade is determinism. Replay only works if applying the same op to the same pixels produces the same pixels, every time, forever. That means versioning your op format, keeping randomness seeded, and treating "apply" functions as things you can never casually change. A snapshot can't drift; a log can. You're trading memory for discipline.
What the log buys you later
Here's the part that made the decision for me: once history is data, features that were impossible become almost free.
- Persistent undo. The log is a few kilobytes — write it to IndexedDB on autosave and your undo stack survives a page reload. Snapshots at 16 MB each were never getting persisted.
- Branching history. Undo three steps and edit in a new direction? With a log that's a tree, not a discarded tail. Git for images stops being a metaphor.
- Macros and batch. Recorded ops replay against a different image just as happily. Record once, apply to fifty photos.
None of those were on my roadmap when I hit the memory wall. All of them fell out of fixing it.
When snapshots are still right
To be fair to the simple thing: if your images are small, sessions are short, or edits are global (filters over the whole frame, where an op saves you nothing on replay cost), snapshots are fine and the afternoon implementation wins. Delta-compressed snapshots — store only changed tiles — are a respectable middle ground, and it's where Image Horse lived for a while before I committed to the log.
But if you're building anything where people work — draw, retouch, iterate for an hour — store the edits. The bug the snapshot version was quietly incubating (an out-of-memory crash that eats an hour of someone's work) is exactly the kind that never shows up on your machine and always shows up on theirs.
Comments (0)
No comments yet. Be the first to share your thoughts!