How we built a 100% client-side image tool with Next.js

By manxlr · · 8 min read

QR Brand processes images, generates QR codes, builds ZIP files and writes multi-page PDFs — and has no backend at all. This post walks through how that works, the trade-offs, and what it took to make a client-side app rank in search. If you're building something similar, I hope it saves you some time.

Why client-side?

  • Privacy as architecture, not policy. If files never leave the device, there is nothing to leak, log, or subpoena.
  • Zero marginal cost. The user's CPU does the work, so a traffic spike costs nothing in compute.
  • Speed. No upload/download round-trip; a 20 MB photo is processed instantly.
  • Works offline once loaded.

The image pipeline

Uploaded files become object URLs (URL.createObjectURL) so the browser can display them without reading them into JavaScript memory. On export, each image is drawn onto an off-screen <canvas>at its target size, then the QR code is drawn on top.

The key decision was storing the QR position as percentages of the image — x, y and size relative to width. The preview canvas and the export canvas are different sizes, and batches contain images of different dimensions; percentages make placement resolution-independent. The size is a percentage of width so the code stays square, and the vertical clamp is scaled by the aspect ratio so the code can never be pushed off a tall image.

Encoding uses canvas.toBlob() with the right MIME type and quality — PNG for lossless, JPG/WebP at 0.92.

Batch export: ZIP and PDF in the browser

For multiple images, blobs are collected and zipped with JSZip, then handed to the user through a temporary anchor element. PDFs are built with jsPDF: each image is scaled to fit the page with a margin and added on its own page. Everything is sequential and awaited, which keeps peak memory low enough for batches of hundreds of photos.

QR generation and logos

Codes are generated with qr-code-styling, which supports dot styles and center images. When a logo is enabled, the error-correction level switches from M to H so the code survives the occlusion (more in the error-correction explainer). Styling changes regenerate the code automatically, debounced through a ref that tracks the previous settings.

Performance: keep the heavy stuff off the critical path

The first version shipped as one big client component, and the homepage hero was a 4.5 MB GIF. The rebuild:

  • Pages are server components, statically generated, with all text in the initial HTML.
  • The editor and its libraries load via next/dynamic with ssr: false and a fixed-height skeleton (no layout shift).
  • The demo GIF became a ~450 KB WebM/MP4 with a poster image and preload="none" — about 10× smaller.

SEO for a client-side tool

A pure single-page tool gives search engines almost nothing to index. The fix was to treat each user intent as a page: Instagram posts, restaurant menus, batch processing and so on. Each is statically generated from a typed data file, pre-configures the tool (for example the 1080×1080 preset), and carries unique copy, steps, tips and FAQs with matching FAQPage, WebApplication and BreadcrumbList structured data. The sitemap is generated from the same registries, so a new page is one object in an array.

Trade-offs

  • No server means no scan analytics — which is the point, but it rules out dynamic codes.
  • Very large batches are bounded by the device's memory.
  • Monetization has to respect the promise: optional donations and clearly-labelled partner links, no ad trackers.

If you need a browser-based tool built — for privacy, cost, or speed reasons — get in touch.

Keep reading

Keep QR Brand free

No ads, no tracking, no paywalls — QR Brand is funded by people who found it useful. If it saved you time, you can buy us a coffee. Pay what you want; it's completely optional.

Buy us a coffee

Payment is optional and never unlocks features. Processed externally by Lemon Squeezy.