When I started building ImgToolkit, the goal was simple: every image tool site I used either uploaded my files to some server I didn't trust, watermarked the output, or locked the useful features behind a 12/monthplan.Iwantedtobuildsomethingwhereeverythingrunsinthebrowser.Yourfilesneverleaveyourdevice.Noserver,noaccount,nopaywall.ThisisthetechnicalbreakdownofhowIgot100toolsworkingentirelyclientside.Thecoreidea:thebrowserispowerfulenoughModernbrowsershavetheCanvasAPI,WebAssembly,WebWorkers,andfilesystemaccess.Withtherightlibraries,youcandothingsthatfeltserveronlythreeyearsago.HeresthestackIsettledon:React+Vitefastbuilds,lazyloadedroutessoeachtoolonlyloadswhatitneedsCanvasAPIhandles80FFmpegWASMwastheoneIwasmostskepticalabout.A30MBWASMbinarythatrunsafullmediaprocessingpipelineinabrowsertab?Itworks.Buttherearegotchas:importFFmpegfrom"@ffmpeg/ffmpeg";importfetchFile,toBlobURLfrom"@ffmpeg/util";constffmpeg=newFFmpeg();awaitffmpeg.load(coreURL:awaittoBlobURL(/ffmpegcore.js,"text/javascript"),wasmURL:awaittoBlobURL(/ffmpegcore.wasm,"application/wasm"),);awaitffmpeg.writeFile("input.mp4",awaitfetchFile(file));awaitffmpeg.exec(["i","input.mp4","q:a","0","map","a","output.mp3"]);constdata=awaitffmpeg.readFile("output.mp3");TheWASMbinaryneedsSharedArrayBuffer,whichrequirescrossoriginisolationheaders(COOP+COEP).Gettingthoseheadersrightinproductiontooklongerthanwritingtheactualtool.DynamicimportontheFFmpegtoolswasessentialyoudontwant30MBloadingonthehomepage.AIbackgroundremovalwithzeroservercalls@imgly/backgroundremovalusesanONNXmodelservedfromaCDN.Firstloaddownloads 30MBofmodelweights.Afterthat,itscachedbythebrowser.importremoveBackgroundfrom"@imgly/backgroundremoval";constblob=awaitremoveBackground(imageFile,publicPath:"https://cdn.imgly.com/backgroundremoval/...",model:"medium",);TheresultisaPNGwithatransparentbackground,generatedentirelyintheusersbrowserusingWebGL/WASMinference.NoAPIkey,noperrequestcost,noserver.ThequalityisgenuinelygoodcomparabletoearlyRemove.bgresults.Thetrickypart:onnxruntimewebmustbeinstalledasadirectdependencyalongsidethelibrary,notjustapeerdependency.Tookmeanembarrassinglylongtimetodebugthat.ClientsidePDFmanipulationpdflibisunderrated.Youcanmerge,split,compress,addsignatures,rotatepages,andaddformfieldsallinthebrowser.importPDFDocumentfrom"pdflib";constmergedPdf=awaitPDFDocument.create();for(constfileoffiles)constbytes=awaitfile.arrayBuffer();constpdf=awaitPDFDocument.load(bytes);constpages=awaitmergedPdf.copyPages(pdf,pdf.getPageIndices());pages.forEach(p=>mergedPdf.addPage(p));constmerged=awaitmergedPdf.save();For"compressPDF",IreencodeallimagesinsidethePDFatlowerquality.Notperfect,butgets3060With100tools,theinitialbundlewouldbeenormousifnothandledcarefully.Everytoolpageisalazyloadedroute:constRemoveBackground=lazy(()=>import("@/pages/removebackground"));constFfmpegVideoToMp3=lazy(()=>import("@/pages/videotomp3"));Heavylibraries(FFmpeg,faceapi,backgroundremoval)aredynamicallyimportedinsidethepagecomponent,notatroutelevelsotheyonlyloadwhentheuseractuallyusesthattool.Initialpageloadisunder100KBofJS.AuserwhoonlycompressesimagesneverdownloadsanyFFmpegorONNXcode.Faceblurringwithoutaserver@vladmandic/faceapiloadsfacedetectionmodelsfromjsDelivrCDNonfirstuse.Themodels( 6MB)detectfaceboundingboxesinthebrowser.IthenapplyaCSSblurfiltertothecanvasregion:ctx.filter=blur(12/month plan. I wanted to build something where everything runs in the browser. Your files never leave your device. No server, no account, no paywall. This is the technical breakdown of how I got 100 tools working entirely client-side. The core idea: the browser is powerful enough Modern browsers have the Canvas API, WebAssembly, Web Workers, and file system access. With the right libraries, you can do things that felt server-only three years ago. Here's the stack I settled on: React + Vite — fast builds, lazy-loaded routes so each tool only loads what it needs Canvas API — handles 80% of image operations (resize, crop, rotate, watermark, convert formats) pdf-lib — pure JS PDF manipulation (merge, split, compress, add pages) pdfjs-dist — PDF rendering to canvas (for PDF to JPG conversion) FFmpeg WASM — video processing in the browser @imgly /background-removal — AI background removal using ONNX models Tesseract.js — OCR, runs a full Tesseract engine via WASM browser-image-compression — handles the heavy lifting for image compression The interesting challenges FFmpeg in the browser FFmpeg WASM was the one I was most skeptical about. A 30MB WASM binary that runs a full media processing pipeline in a browser tab? It works. But there are gotchas: import { FFmpeg } from "@ffmpeg/ffmpeg"; import { fetchFile, toBlobURL } from "@ffmpeg/util"; const ffmpeg = new FFmpeg(); await ffmpeg.load({ coreURL: await toBlobURL( /ffmpeg-core.js , "text/javascript"), wasmURL: await toBlobURL( /ffmpeg-core.wasm , "application/wasm"), }); await ffmpeg.writeFile("input.mp4", await fetchFile(file)); await ffmpeg.exec(["-i", "input.mp4", "-q:a", "0", "-map", "a", "output.mp3"]); const data = await ffmpeg.readFile("output.mp3"); The WASM binary needs SharedArrayBuffer, which requires cross-origin isolation headers (COOP + COEP). Getting those headers right in production took longer than writing the actual tool. Dynamic import on the FFmpeg tools was essential — you don't want 30MB loading on the homepage. AI background removal with zero server calls @imgly /background-removal uses an ONNX model served from a CDN. First load downloads ~30MB of model weights. After that, it's cached by the browser. import { removeBackground } from " @imgly /background-removal"; const blob = await removeBackground(imageFile, { publicPath: " https://cdn.imgly.com/background-removal/.. .", model: "medium", }); The result is a PNG with a transparent background, generated entirely in the user's browser using WebGL/WASM inference. No API key, no per-request cost, no server. The quality is genuinely good — comparable to early Remove.bg results. The tricky part: onnxruntime-web must be installed as a direct dependency alongside the library, not just a peer dependency. Took me an embarrassingly long time to debug that. Client-side PDF manipulation pdf-lib is underrated. You can merge, split, compress, add signatures, rotate pages, and add form fields — all in the browser. import { PDFDocument } from "pdf-lib"; const mergedPdf = await PDFDocument.create(); for (const file of files) { const bytes = await file.arrayBuffer(); const pdf = await PDFDocument.load(bytes); const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()); pages.forEach(p => mergedPdf.addPage(p)); } const merged = await mergedPdf.save(); For "compress PDF", I re-encode all images inside the PDF at lower quality. Not perfect, but gets 30–60% size reduction on scanned documents without any server. Lazy loading everything With 100 tools, the initial bundle would be enormous if not handled carefully. Every tool page is a lazy-loaded route: const RemoveBackground = lazy(() => import("@/pages/remove-background")); const FfmpegVideoToMp3 = lazy(() => import("@/pages/video-to-mp3")); Heavy libraries (FFmpeg, face-api, background-removal) are dynamically imported inside the page component, not at route level — so they only load when the user actually uses that tool. Initial page load is under 100KB of JS. A user who only compresses images never downloads any FFmpeg or ONNX code. Face blurring without a server @vladmandic/face-api loads face detection models from jsDelivr CDN on first use. The models (~6MB) detect face bounding boxes in the browser. I then apply a CSS blur filter to the canvas region: ctx.filter = blur({blurStrength}px) ; ctx.drawImage(canvas, x, y, w, h, x, y, w, h); ctx.filter = "none"; Works surprisingly well on photos with 1–4 faces. Degrades on crowds — but so does every commercial API at that task. What I learned The browser is ready. WebAssembly, ONNX inference, full PDF manipulation, video processing — it all works. The main limits are file size (very large files hit memory limits) and first-load time for WASM binaries. Lazy loading is non-negotiable. Without it, you're shipping 50MB of JS to every visitor regardless of which tool they use. Headers matter for WASM. SharedArrayBuffer requires COOP: same-origin and COEP: require-corp. Get these wrong and FFmpeg silently fails. Client-side means private by default. Users immediately trust a tool more when you can prove their files never leave their device. It's a genuine differentiator, not just a marketing claim. The site is at imgtoolkit.com — 100 tools, all free, all client-side. Happy to answer questions about any part of the implementation.