A live Figma preview is a filter, a debounce and a counter
Mokker puts the Figma frame you select on the screen of a 3D device and keeps it in sync while you edit. Figma tells a plugin about every change in the file. The work is deciding which ones to ignore.

In this story7 sections
Select a frame in Figma and Mokker shows it on the screen of a 3D phone. Change a word inside the frame and the phone updates. It feels like one live surface. It is not: it is a PNG export, rebuilt every time something relevant changes.
The hard part is "relevant". Figma's documentchange event fires for every edit anywhere in the file, from anyone. If the plugin exported on each one, typing a headline would queue an export per keystroke. Here is how Mokker decides what to do, in the order the code does it.
1. Most changes are not yours
When the user selects something, Mokker stores the node it exports: the top-level frame around the selection. Every change event is then checked against that node.
A property change or a new node counts only if it is the frame or sits inside it. The check walks up the parent chain until it reaches the frame or runs out:
function isNodeInsideOrEqual(node: BaseNode, container: SceneNode) { let current: BaseNode | null = node; while (current) { if (current.id === container.id) return true; current = current.parent; } return false;}Removed nodes are skipped. A deletion counts only when the deleted node is the frame itself, matched by id. Every edit outside the frame, on any page and by anyone, is ignored before any work starts.
The same rule applies to selection. Clicking from one layer to another inside the same frame does not export again, because the frame on screen has not changed.
2. Wait until the edits stop
A relevant change does not export straight away. It resets a timer, and the export runs when 100 ms pass without another relevant change. Selection changes get their own timer at 50 ms, added to stop exports queueing up while someone clicks quickly through layers.
The two numbers are different on purpose. Selection should feel instant. Edits come in bursts, and one export at the end of a burst is worth more than ten in the middle of it.
3. Throw away answers to old questions
A debounce reduces the number of exports. It does not stop two from overlapping, and exportAsync takes longer for a big frame than a small one. Without a guard, a slow export of the old selection can finish after a fast export of the new one and overwrite it.
Mokker numbers every export. Starting one increments a counter, and the result is used only if the counter has not moved while it was waiting:
const exportId = ++currentExportId;const imageBytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: 1 } });if (exportId !== currentExportId) return; // a newer export took overErrors follow the same rule. A failed export shows a message only if it is still the current one, so the user never sees an error about something they have already moved away from.
4. Preview small, render big on request
Until 22 January 2026 the preview exported at 2x. It now exports at 1x, a quarter of the pixels, which is enough for a texture on a phone screen in a small plugin window. The full-resolution texture is requested only when the user adds the mockup to the canvas.
That request is clamped to Figma's 4,096 pixel image limit, as the sandbox story explains.
5. Ask for the pages you need to watch
In January 2026 Mokker's manifest moved to documentAccess: "dynamic-page", where Figma loads pages on demand. In that mode a plugin must call figma.loadAllPagesAsync() before it registers for documentchange, and the controller does exactly that, then attaches the listener.
6. Do not flash a spinner for fast work
A fast export would flash a loading state for a split second, and that reads as flicker. The plugin sends the frame's size with each loading message. If the frame is 1,800 px or more on its longest side, or 1,200,000 square pixels or more in area, the indicator shows at once. Otherwise the UI waits 150 ms and shows it only if the export is still running.
- Debounce on edits inside the frame
- 100 ms
- Debounce on selection changes
- 50 ms
- Preview export scale, January 2026
- 1xfrom 2x
- Wait before a spinner on small frames
- 150 ms
The rule
Syncing from a canvas you do not own is mostly about saying no. If you are building one:
- Filter events to the subtree you display, before doing any work.
- Debounce edits and selection separately, with selection the faster of the two.
- Number each async job and drop any result that is no longer the latest.
- Preview at the lowest scale that looks right. Render full size only on request, clamped to the platform's limits.
- Decide whether to show a loading state from the size of the job, not after the fact.



