DEV Community

tony huang
tony huang

Posted on

Local-first photo editing in the browser: what the pipeline split actually looks like

If you build anything in the browser that touches user photos, you eventually run into the same wall: the cheap operations want to be local, and the expensive ones cannot be.

Here is how I ended up splitting the pipeline, and the constraints that forced each decision.

The cost asymmetry nobody mentions in the docs

A brightness slider is a loop over pixels. A crop is a coordinate transform. A JPEG re-encode is a codec call. None of these need a server, and if you put them behind one you have added latency, bandwidth and a privacy problem to something that costs you nothing to run locally.

A diffusion step is different. It needs a GPU, it costs real money per call, and there is no version of it that runs acceptably in a browser tab today. That is not a temporary limitation of my code — it is the state of the hardware.

So the pipeline naturally splits: everything deterministic runs in the client, everything generative runs on a server, and the boundary between them is the honest place to draw your pricing model.

What the local layer actually costs

Canvas-based operations are fast until they are not. The three things that bit me:

1. getImageData is a synchronous trap. Every pixel-level pass that reads back from the canvas stalls the main thread while the GPU drains. On a 4000×3000 photo that is a visible freeze. The fix is to do the pixel work once per adjustment change, not once per frame, and to debounce slider input — otherwise you are running the full loop on every mousemove event.

2. Re-encoding is the real cost, not the filter. Applying a filter is milliseconds; encoding the result to JPEG at quality 0.9 to show a preview is where the time goes. Previewing at display resolution and only re-encoding at full resolution on export makes the interaction feel instant.

3. Memory is worse than you think. An RGBA buffer for a 12-megapixel photo is 48 MB. Keep a source copy, a working copy and an undo stack and you are at a few hundred megabytes on a phone. Mobile Safari will kill the tab without ceremony. Downscaling the working copy to the viewport size and keeping the original as a compressed blob is the only thing that survived real testing.

None of that is exotic, but it means "local-first" is an architectural commitment rather than a checkbox.

The network panel is the honest test

The nice side effect of this split is that it is verifiable from outside. Open DevTools, switch to the Network tab, and:

  • Apply a tone adjustment. Requests appearing there means the claim is false.
  • Use the generative feature. Requests are expected — that part really does cost money per call.
  • Export. If the only difference between the free and paid path is this step, you have found the business gate, which is a separate thing from a technical limit.

A tool that passes the first check and is open about the second is telling you the truth about its own cost structure. That is a better signal than any badge.

The privacy argument is a side effect

I did not start with a privacy pitch. But once the local layer exists, the useful consequence follows: photos that never leave the device are photos people are willing to use. Scans of documents, pictures of kids, product shots under NDA — the category that gets excluded from every cloud editor by default becomes available.

That is a real product difference, and it comes from the architecture, not from a policy page.

Where the split breaks

Two honest limits, because anyone claiming otherwise is selling something.

WebAssembly models are not a shortcut. You can run a small model in the browser, and people do. But it is slow enough to be unusable for anything interactive on a laptop, and it drains the battery doing it. For the operations people actually want (restoration, background removal, upscaling, style generation) the server is the only option that produces good output in reasonable time.

The boundary moves. Every year a model that needed a GPU gets smaller. The split I have today will be wrong in a couple of years, and rebuilding around a new boundary is cheaper than pretending the old one is permanent.


I build aiphotoeditor.best along these lines: the local layer (tone, filters, crop, resize, format conversion, compression) is free and unlimited because it costs nothing to run; the AI layer is capped at three free edits a day and then one-time credit packs, because each call costs money. If you want to check the split for yourself, the network panel test above works on it too.

Top comments (0)