← All notes
Native iOS Development6 min

A debounce is not a latency budget

On Endeo we learned to stop kicking off handwriting recognition and persistence on every stroke. That debounce was necessary. It was also easy to mistake for a latency fix. The Pencil still felt late when Vision, thumbnails, or a SwiftUI parent rebuild ran on the main thread during the stroke. A quiet background queue is not the same thing as ink that keeps up with the tip.

Two clocks people collapse into one

Drawing latency is the gap between the tip moving and pixels updating. It is measured in frames. Predicted touches exist because that gap is visible even when your code is careful. Apple's guidance is blunt: incorporate predicted locations, treat them as temporary, and discard them when the real event arrives.

Work latency is everything else that happens because of a stroke — OCR, CloudKit or local save, thumbnail rebuild, search index update. That work belongs on a coalesced schedule measured in hundreds of milliseconds to a few seconds. Collapsing the two clocks produces the wrong fix: you lengthen the debounce while the canvas still hitchens on every curve.

Endeo needs both. The pen is the primary instrument during a sermon note. Search needs text later. Sync needs a durable drawing eventually. None of those later jobs get to steal the frame budget while the tip is down.

What actually steals the stroke

The failures we saw were boring and local. A Vision pass started too eagerly. A page thumbnail built with PKDrawing.image on the main thread while the user was still writing. A SwiftUI parent that recreated the UIViewRepresentable mid-session and tore down the PKCanvasView. A drawingPolicy that still allowed finger drawing, so scroll and ink fought for the same gestures.

None of those are fixed by waiting five seconds to save. The save debounce only stops a second class of stutter: the one that appears after a flurry of strokes when persistence and recognition pile up. Useful. Incomplete.

The checklist we run now is short. Keep expensive work off the main thread. Debounce persistence and recognition separately from rendering. Prefer pencilOnly unless finger ink is a real product requirement. Confirm makeUIView is not firing again every state change. Only then argue about predicted-touch rendering if a custom path needs it.

Debounce the derived work, not the ink

Recognition stays disposable derived data on Endeo. That decision already told us not to treat OCR as the document. It also tells us when recognition is allowed to run: after the drawing has been quiet long enough that a Vision pass will not fight the next stroke, and on a background queue so a dense page cannot freeze the tool.

Persistence follows the same shape with a longer fuse. canvasViewDrawingDidChange fires often. Writing on every callback drops frame rate on long notes. Coalesce into a short buffer for local store writes, and a longer one for optional sync, with a flush on resign-active so a backgrounded session does not strand ink in memory.

The numbers are product choices, not magic. Half a second to a second is enough to coalesce local writes without feeling like the app forgot the page. Several seconds is enough for optional network sync. Neither number is a substitute for keeping the canvas responsive while those timers are still counting.

How we know the budget held

Simulator checks prove compile and layout. They do not prove Pencil feel. We already treat a simulator pass as insufficient for Pencil work; the latency budget inherits that rule. The test is a real Pencil on a mid-tier iPad, a multi-page note with dense ink, and a session long enough that thumbnails, recognition, and saves have all had a chance to misbehave.

Pass means the tip and the stroke stay visually glued through fast curves, erasures, and tool changes while background work is intentionally busy. Fail means any hitch that makes the user lift the Pencil and wait. We do not average that away with a green Instruments trace from a cold launch.

If the hitch only appears when recognition is enabled, the recognition schedule is wrong. If it appears with recognition off and saves deferred, look at the canvas path itself. The debounce settings are innocent until those two cases are separated.

What we treat as done

A Pencil surface is done when stroke rendering has an explicit budget, derived work has an explicit debounce, and the two are not tuned as if they were the same dial. Predicted touches, main-thread rules, and representable lifecycle are part of the rendering budget. OCR and sync timers are not.

On Endeo that split is what let handwriting search ship without teaching people that the pen was heavy. The ink stays immediate. The index catches up when the page goes quiet. Confusing those jobs is how a careful debounce still ships a laggy Pencil.

Questions

Isn't delaying OCR enough to keep Apple Pencil feeling fast?
No. Delaying OCR and saves prevents backlog stutter after a flurry of strokes. Tip-to-ink latency still fails if recognition, thumbnails, or view rebuilds run on the main thread during the stroke.
Do you always need predicted touches?
PKCanvasView already aims for low latency. Predicted touches matter most on custom drawing paths. Either way, keep derived work off the stroke path before you chase prediction tuning.

Sources

  1. Minimizing latency with predicted touches — Apple guidance on predicted touches for reducing perceived input lag.
  2. Read between the strokes with PencilKit — WWDC26 — PencilKit handwriting recognition and keeping heavy recognition work off the UI path.

Have something to build?

Tell us what you're working on and we'll tell you honestly whether we're the right fit.

Work with us