DEV Community

Cover image for Scaling a Single React App to 71+ Browser-Based Tools Without Killing Load Time
Vijay Kanna
Vijay Kanna

Posted on

Scaling a Single React App to 71+ Browser-Based Tools Without Killing Load Time

The problem with "just add another tool"

When you're building one image tool, performance is easy. When you're building 71 of them in the same app — resize, compress, crop, PDF merge, format converters, exam-photo presets, social media templates — the naive approach (import everything, bundle it all together) turns your app into a multi-megabyte JavaScript payload before a user has even picked a tool.

This is the actual engineering problem behind ResizeHub, which now has 71+ tools across 11 categories, all running client-side with zero server uploads. Here's how the architecture holds up at that scale.

Stack, and why each piece earns its place

  • React + TypeScript — type safety matters more, not less, as tool count grows. A shared ImageProcessor interface that every tool implements catches integration bugs at compile time instead of in production.
  • Vite — its native ES modules dev server and Rollup-based production build made code-splitting dramatically easier to reason about than older bundlers, which matters a lot once you have dozens of independent tool routes.
  • HTML5 Canvas API — the actual compression/resize/crop engine, shared across tools rather than reimplemented per-tool.
  • Cropper.js — for interactive cropping UI specifically (aspect-ratio locking, circular crop for signatures) rather than rebuilding drag-handle math from scratch.
  • Pica — for high-quality image downscaling; the browser's native canvas scaling can introduce visible aliasing on large downscales, and Pica's algorithm handles this noticeably better.
  • Cloudflare Pages — static hosting with edge caching, which matters since 100% of the actual processing work happens in the user's browser, not on any server at all.

Lesson 1: Route-level code splitting isn't optional past a handful of tools

With React Router and dynamic import(), each tool becomes its own chunk:

const PhotoResizer = lazy(() => import('./tools/PhotoResizer'));
const PdfCompressor = lazy(() => import('./tools/PdfCompressor'));
const SignatureCropper = lazy(() => import('./tools/SignatureCropper'));
Enter fullscreen mode Exit fullscreen mode

Without this, every visitor downloads the code for all 71 tools just to use one. With it, a user landing on the SSC photo tool only pulls that tool's bundle plus shared core — not the PDF merger, not the social media template generator, not tools they'll never touch in that session.

Lesson 2: Share the processing core, isolate the UI

The temptation with many similar tools is to copy-paste a working tool and tweak it. That works for the first five tools and becomes unmaintainable by tool thirty. The fix that held up: a shared core engine (resize, compress-to-target-KB, format conversion, crop math) that every tool imports, with only the UI and preset configuration differing per tool.

// shared core
export function resizeToTarget(canvas: HTMLCanvasElement, spec: ToolSpec) { ... }

// per-tool, just config
const sscPhotoSpec: ToolSpec = {
  width: 200, height: 230, maxKB: 20, format: 'jpeg', background: 'light'
};
const ibpsPhotoSpec: ToolSpec = {
  width: 200, height: 230, maxKB: 50, format: 'jpeg', background: 'light'
};
Enter fullscreen mode Exit fullscreen mode

This means adding tool #72 for a new exam board is a config object, not new processing logic — and a bug fix in the core engine fixes it everywhere at once instead of needing to be copy-pasted across dozens of files.

Lesson 3: Category-level grouping helps both users and bundlers

Organizing 71 tools into 11 categories (Resize, Compress, Convert, Crop & Edit, PDF, Exam Tools, Social Media, Enhancement, Utility, Batch, Privacy) wasn't just a UX decision — it maps naturally onto chunk boundaries too. Tools within a category often share more code (e.g., all exam-photo tools share background-validation logic) than tools across categories, so grouping by category keeps related code physically close and shared chunks smaller and more targeted.

Lesson 4: Batch processing needs a different execution model

Single-image tools can process synchronously without the user noticing. Batch tools (batch resize, batch compress, batch convert across many files) need to avoid blocking the main thread, or the UI freezes on a 50-image batch. The practical fix is chunked async processing with requestIdleCallback or a Web Worker for the heavier operations, processing a few images at a time and yielding back to the browser between chunks so the UI stays responsive and progress can actually update.

Lesson 5: Type-safe tool specs prevent an entire category of bugs

With 71 tools, many sharing similar-but-not-identical specs (every government exam board has slightly different photo requirements), a loosely-typed config object is a bug magnet. A strict ToolSpec interface, checked at compile time, catches "someone typo'd the KB limit for one exam board" before it ships rather than after a user's application gets rejected because of it — which matters more here than in most apps, since a wrong spec has a real downstream cost for someone filling out a government form.

What this adds up to

None of these are exotic techniques individually — code splitting, shared core logic, typed configs are all fairly standard React practice. The interesting part is that at 5-10 tools, skipping all of this barely matters. At 70+, skipping any one of them compounds into real load-time and maintainability problems. The architecture decisions that don't matter early are exactly the ones that determine whether scaling to "one more tool" stays cheap or becomes expensive.

If you're curious how this plays out in practice, the live result — 71+ tools, all client-side, no uploads — is at resizehub.in.

Curious from others in the comments

If you've built a similar "many small tools, one app" product, I'd like to compare notes — specifically on whether you went with route-based splitting like this or a micro-frontend approach instead, and where that tradeoff started to matter for you.

Top comments (0)