DEV Community

xiaoxu
xiaoxu

Posted on

Routing Image Jobs Between Canvas and Sharp in Next.js

This is Part 2 of Building Piclume: Browser-First Image Tools. Read Part 1: Why I Built a Browser-First Image Tool with Next.js.

Routing Image Jobs Between Canvas and Sharp in Next.js

“Process images in the browser first” sounds like a binary architecture decision.

In a real image tool, it is a routing problem.

Browsers are very capable at decoding and re-encoding common image formats. They are not equally reliable for every input, every output, or every optimization goal. A JPG resize is a good candidate for the browser. HEIC input is not. PNG compression might technically be possible through canvas re-export, but that does not give the same output control as a dedicated server-side encoder.

For Piclume, I wanted one user workflow—upload, process, download—with two possible engines:

  • Browser Canvas for common, reliable local jobs.
  • Sharp on the server for compatibility-sensitive or server-preferred jobs.

The hard part is not calling either API. The hard part is choosing the path without making users learn image-format trivia first.

Start with a routing contract

The router should answer one narrow question:

Is this specific file, requested output, and tool route safe to try in this browser?

That question has more context than “does the browser support images?” A useful decision needs at least:

  • the input MIME type;
  • the requested output format;
  • the current tool route;
  • the browser's actual export capability;
  • product policy for quality-sensitive workflows.

Here is the decision flow in Mermaid:

Mermaid diagram 1

The key word is try. A capability check is a filter, not a promise. The browser can report support and still fail on a particular file, device, or memory-constrained session.

Keep the policy explicit

In Piclume, browser-first currently starts with JPG, PNG, and WebP inputs. The requested output must also be a format the browser can export reliably. JPEG and PNG are straightforward; WebP export is checked at runtime instead of assumed.

The routing logic also has a product rule: compress-png stays on the server path. Canvas can produce a PNG, but a general-purpose canvas export is not the same as a PNG-compression strategy with deliberate encoder control.

The simplified policy looks like this:

function shouldPreferBrowserProcessing({ file, outputFormat, toolSlug }) {
  if (typeof window === "undefined") return false;
  if (!browserInputMimeTypes.has(file.type)) return false;
  if (toolSlug === "compress-png") return false;
  if (toolSlug === "compress-image" && outputFormat === "png") return false;

  return supportsBrowserOutputFormat(outputFormat);
}
Enter fullscreen mode Exit fullscreen mode

This is intentionally conservative. It is better to use the compatibility path for a job that might have worked locally than to promise local processing and fail after the user has already invested time selecting files and adjusting settings.

The client router should be small

The client-side orchestration has three jobs:

  1. Ask whether the browser path is appropriate.
  2. Attempt local processing when it is.
  3. Fall back to the API if that attempt fails.
export async function processImageFromClient(options: ProcessImageOptions) {
  if (shouldPreferBrowserProcessing(options)) {
    try {
      const result = await processImageInBrowser(options);
      return { processor: "browser", ...result };
    } catch (error) {
      console.warn("Local processing failed; falling back to server.", error);
    }
  }

  return processImageOnServer(options);
}
Enter fullscreen mode Exit fullscreen mode

That try/catch is not an afterthought. It protects the main action: getting the user a usable result. A browser-side decoder can fail; a canvas export can return null; and an image can simply be too demanding for the current device. None of those cases should strand the user at an error state if the server path can complete the job.

The browser success path

On a supported path, the application decodes the File with createImageBitmap() where possible, falling back to an HTMLImageElement when necessary. It draws the source into a canvas at the requested dimensions, then exports a Blob through canvas.toBlob().

For JPEG output, the canvas first receives a white background. That makes the behavior for transparent PNG or WebP sources explicit instead of leaving the resulting matte color to chance. For resize jobs, the context uses high-quality image smoothing before drawing at the target dimensions.

Mermaid diagram 2

No API request is needed in this path. The Blob can be downloaded from the browser session after the result UI has shown the original and processed sizes.

The fallback path is part of the normal design

When local processing is not suitable—or when it fails—the client submits the file and processing options to a Next.js route handler. The server uses Sharp for compression, conversion, resize work, rotation, and output metadata.

The route returns the processed binary directly rather than creating a persistent public result page. Response headers carry the output metadata needed by the workspace, and Cache-Control: no-store makes the response behavior explicit.

Mermaid diagram 3

For HEIC and HEIF, Sharp is attempted first. In the deployment environment used by Piclume, a macOS sips fallback can help with known decode limitations before normal Sharp processing continues. That is an implementation detail, but it illustrates the broader point: file compatibility belongs behind a stable interface.

What happens in a mixed batch

Batch work makes the routing decision more visible. One upload can contain files that require different engines.

For example, a batch with JPG product photos and one HEIC image should not force every JPG through the server. Each file is evaluated independently, then the workspace aggregates the engines used into a simple status label.

Mermaid diagram 4

This gives a better cost and privacy profile than sending every file to the server, while still handling the formats that need it.

Test the decision boundaries, not just the happy path

The most useful tests for a hybrid image pipeline are boundary tests:

Case Expected engine What it proves
JPG → WebP compression Browser Common local path works end-to-end
PNG compression Server Product policy can override browser capability
HEIC → JPG Server Unsupported browser input reaches compatibility handling
Browser decode/export failure Server after retry Local failures do not become dead ends
Mixed JPG + HEIC batch Mixed Per-file routing and status aggregation work

This is also why I prefer a visible processing-engine label. It is useful for users, but it is just as useful for debugging and future analytics. Once the application knows which path produced each result, it can measure fallback rates instead of guessing where compatibility problems live.

The larger lesson

Browser Canvas and Sharp are not competing architectures here. They are complementary processors behind a single product workflow.

The browser handles the common path close to the user. The server handles the edge cases that need stronger format support or tighter output control. The router makes the decision, and the fallback keeps the promise that matters most: an uploaded image should lead to a result.

Try the current implementation at Piclume, or read the first article in this series for the product reasoning behind the browser-first approach.

Top comments (0)