Description: On-device AI, WASM threads, target-size compression, inpainting and 3D LUTs in a single HTML file. 7 real lessons from building APIC Web.
Most web developers can build a form, fetch an API and center a div.
Browser experts know what the browser refuses to do, what happens on a phone instead of your laptop, and how to ship real software with no server at all.
I found out which one I was while building APIC Web: a free photo editor, background remover, object remover, converter and compressor where your photos never leave your device. No upload queue, no backend, and the whole thing is one HTML file hosted on GitHub Pages.
Every test below is a real bug or design decision from that build. Score yourself honestly. đ
Test 1: Why did my AI step silently never finish on phones?
APIC runs AI models in the browser with ONNX Runtime Web (WebAssembly). Multi-threaded WASM needs SharedArrayBuffer, and that needs special response headers (COOP/COEP). A plain static host like GitHub Pages doesn't send them, so crossOriginIsolated is false.
On desktop, the runtime usually degrades gracefully. On phones it often doesn't, and the AI step just hangs with no error. The fix was to stop relying on the fallback:
// Static hosts can't send COOP/COEP, so threads aren't available.
// Force the one configuration that behaves the same everywhere.
ort.env.wasm.numThreads = 1;
ort.env.wasm.proxy = false;
const session = await ort.InferenceSession.create(modelBytes, {
executionProviders: ['wasm'],
enableCpuMemArena: false,
enableMemPattern: false,
});
Where headers are available, APIC checks self.crossOriginIsolated before asking for more than one thread.
Developer: "It works on my laptop."
Expert: knows that "falls back gracefully" is a claim to test on a real phone, not a promise.
Test 2: Can you tell a slow download from a dead one?
APIC's AI models are big: 5 MB (Lite), 44 MB (Fast), 176 MB (Maximum) for background removal, and about 208 MB for the highest-quality object remover. A fixed 30-second timeout would kill every slow phone connection.
So the timeout is stall-based: abort only if no bytes arrive for 40 seconds. A huge model on bad Wi-Fi is allowed to take as long as it needs. A dead link is not. After that it goes into the Cache API so the second run is instant:
const CACHE = 'apic-models-v1';
const cache = await caches.open(CACHE);
let hit = await cache.match(url);
if (hit) return hit.arrayBuffer(); // cached: no download
const bytes = await downloadWithStallTimeout(url); // abort only if the stream goes quiet
await cache.put(url, new Response(bytes)); // wrapped in try/catch: private mode & quota can fail
return bytes;
And if a model can't load at all, the tool steps down instead of failing: Ultra (LaMa) â MI-GAN â a quick patch fill.
Developer: fetch() with a timeout.
Expert: measures progress, caches the result, and always has a plan B.
Test 3: Can your app hit "under 200 KB" without guessing?
Upload forms say "max 200 KB" and users play quality-slider roulette. APIC's Compress tool does a binary search on JPEG quality instead:
async function findQualityForTarget(canvas, targetBytes) {
let lo = 0.05, hi = 0.98, best = null;
for (let i = 0; i < 7; i++) {
const mid = (lo + hi) / 2;
const blob = await toBlob(canvas, 'image/jpeg', mid);
if (blob.size <= targetBytes) { best = { blob, q: mid }; lo = mid; } // fits: try higher quality
else { hi = mid; } // too big: go lower
}
return best;
}
Seven encodes find the highest quality that still fits. If even the lowest quality is too big, APIC shrinks the image by about 20% and searches again, up to 8 times. If it still can't make it, it tells you the real size instead of pretending.
Developer: quality = 0.7 and hope.
Expert: turns a guess into a search and is honest when the answer is "can't".
Test 4: Why did my transparent PNG turn black as a JPEG?
JPEG has no alpha channel. When a canvas with transparent pixels is encoded as JPEG, those pixels come out black. After background removal (which creates lots of transparency), that's a nasty surprise.
The fix is to paint white first, then draw the image on top:
function flattenToWhite(src) {
const out = document.createElement('canvas');
out.width = src.width; out.height = src.height;
const ctx = out.getContext('2d');
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, out.width, out.height);
ctx.drawImage(src, 0, 0);
return out;
}
APIC also tells you which formats keep transparency (PNG, WebP) and which don't, right in the Convert panel.
Developer: assumes a canvas is just pixels.
Expert: knows every output format has rules about alpha.
Test 5: Do you process the whole image, or only what changed?
Removing an object means running an inpainting model (LaMa, a Fourier-convolution network, or the lighter MI-GAN). LaMa works on a fixed 512Ă512 window, so feeding it a 12-megapixel photo would be slow and blurry.
APIC only processes the area around your brush:
- Take a region around the mask (at least 512 native pixels when the photo allows).
- Run the model on just that crop.
- Feather the edge with box blurs, colour-match the fill to its surroundings, and re-add film grain so it doesn't look "too clean".
- Paste it back.
Small edit, small compute, and the rest of your photo is untouched.
Developer: runs the model on the full image.
Expert: shrinks the problem first, then blends the result back so nobody can see the seam.
Test 6: Can you export your whole colour grade as a .cube LUT?
APIC has curves, an 8-band HSL mixer and 3-way colour grading. People wanted to use those grades in DaVinci Resolve or Premiere. A .cube file is a 3D lookup table: for every input colour, what colour comes out.
The trick: you don't need to reverse-engineer your pipeline. Feed an identity grid of colours through the same pixel function, and write down what comes out:
const N = 33; // 33Ă33Ă33 lattice
const buf = new Uint8ClampedArray(N * N * N * 4);
let i = 0;
for (let b = 0; b < N; b++)
for (let g = 0; g < N; g++)
for (let r = 0; r < N; r++) { // red changes fastest, as .cube requires
buf[i++] = Math.round(r / (N - 1) * 255);
buf[i++] = Math.round(g / (N - 1) * 255);
buf[i++] = Math.round(b / (N - 1) * 255);
buf[i++] = 255;
}
processPixels(buf, gradeWithoutSpatialEffects); // same code that edits your photo
const lines = ['TITLE "APIC grade"', 'LUT_3D_SIZE 33',
'DOMAIN_MIN 0.0 0.0 0.0', 'DOMAIN_MAX 1.0 1.0 1.0', ''];
for (let j = 0; j < buf.length; j += 4)
lines.push(`${(buf[j]/255).toFixed(6)} ${(buf[j+1]/255).toFixed(6)} ${(buf[j+2]/255).toFixed(6)}`);
One catch: a LUT can only hold colour operations. Clarity, sharpen, vignette and grain depend on neighbouring pixels, so they're excluded and the UI says so.
Developer: exports a screenshot of the look.
Expert: samples the function itself.
Test 7: Why does your resized image look soft?
ctx.drawImage(img, 0, 0, smallW, smallH); // one big jump: soft, aliased
Shrinking a large image in one drawImage call looks muddy. APIC's Resize tool halves the image step by step until it's within 2Ă of the target, then does one final pass:
function highQualityResize(src, targetW, targetH) {
let cur = src, w = src.width, h = src.height;
while (w > targetW * 2 || h > targetH * 2) {
const nw = Math.max(targetW, Math.round(w / 2));
const nh = Math.max(targetH, Math.round(h / 2));
const step = document.createElement('canvas');
step.width = nw; step.height = nh;
const c = step.getContext('2d');
c.imageSmoothingEnabled = true;
c.imageSmoothingQuality = 'high';
c.drawImage(cur, 0, 0, nw, nh);
cur = step; w = nw; h = nh;
}
// ...then one last high-quality pass onto the exact target size
}
Developer: drawImage once and move on.
Expert: knows the order of operations changes the result.
Bonus: How do you ship a Web Worker inside a single HTML file?
No build step, no separate worker.js. APIC keeps the worker's source as a string, turns it into a Blob and starts it from a Blob URL:
const SRC = `/* worker code as a string */`;
const worker = new Worker(
URL.createObjectURL(new Blob([SRC], { type: 'application/javascript' }))
);
Heavy inpainting runs off the main thread, and the whole app is still one file you can host anywhere.
Try it yourself
APIC is built by Akhouri Systems. It runs in your browser, and your images are never uploaded. The only thing the browser downloads is the AI model files (from Hugging Face), cached after the first run. Being upfront about the trade-offs: editing needs no account, you sign in with one tap on Google only when you export, and the web version shows one small banner ad to stay free. The offline PC app has none.
| App | What it is | Lessons it proves | Get it |
|---|---|---|---|
| đźď¸ APIC Web | Edit, pro colour tools, remove objects, remove backgrounds with AI, resize, convert, compress, 15-network social presets, batch ZIP | Tests 1-7 | Open APIC Web |
| đť APIC for PC | Windows version, runs fully offline, no account | The desktop twin | Download (zip) |
If you're learning how the browser works outside a tutorial, open it, drop a photo, then open DevTools and see what's going on under the hood. That's the fastest way I know to level up.
Your score
- 0-2 tests known: You're a web developer. Great start, now break something on a real phone.
- 3-5: Solid. You've shipped something real.
- 6-7: You're a browser expert. Come argue with me in the comments.
Which test did you fail first? Mine was [the AI step that worked on desktop and silently hung on phones]. Tell me yours below đ
Top comments (131)
Nice Post!!
Fastest reading đ đ¤Ł
nope...
1st --> comment on post
2nd --> read the post
lol
Okay this thread looks good...
yea... very clean
Lol I can't use gif I'm missing my signature meme
I'll be back
Bro lol you're reminding me my memes
𤣠@puneetkumar2010 gone
Sad đ Puneet Kumar is no more......
Online now, he will come after a while
Hehehe
I also want to go cuz I have work :(
Okay then Bye
Oh really?
Yeah
Ok bye! đđđđđ¤
Hmm
đđââď¸
Bro look this isn't it cool?
What is use or that?
It's TTS bro we can use it to convert text to speech.
Woahhh what due to its UI first impression I failed to understand đ đ
Bro bruh you are making these good products when I can use laptop and use it, test it lol đ
Bro I didn't make it lol 𤌠Somebody else did. I just shared it with you
Oh ui opened in 1 4th of screen so I didn't understood that much
Ohhh why is that lol
Bro idk đ¤ˇââď¸ đ
đ
đ
Puneet bhaiya wapas aagye yee
đ¤Łđ¤Ł
đ¤Łđ¤Ł
Sly guy hehe
Puneet iz backkkkk
đ¤Ł
Legend never dies
so we are immortal... đ
Yeah we will be remembered forevrr
yea!
Redirected!
𤣠bruhh
Peak cinema đĽ
Okay guys bye.
đđ¤
bye bye didi
Bye bye @ everyone
Bye Bye good night
Bye bye by the way you have a message in vc
Good night kaha hai bekar Night.
Mental stress of IITian
vc? anyway see you tomorrow! đ´
now I am truly gone okay?
Ugh it's only 8:00?
Oh then let's do something else...
Yeah! Stress đŹ
Oh this candy is available there as well?!
Yeah,It is available in Hyderabad.
đŻ
It is available all over India. One rupees đ¤Ł
Good morning everyone đ What do you gys think about this place?
Peak India đŽđł
@technogamerz
youtube.com/shorts/apiVexGkwlE?si=...
Didi youtube.com/shorts/H9aUOKwv4sI?si=...
đ đ¤Ł
youtube.com/shorts/zaoSNCP0w-g?si=...
youtube.com/shorts/zaoSNCP0w-g?si=...
Happy birthday Dear Faseehđ°đđ
THIS IS THE DAY WHEN MY ONE OF THE LEGENDARY BEST BRO AND CTO @ AKHOURI SYSTEMS SPAWNED IN THE PLANET EARTH
SOOOOOOO
HAPPY BIRTHDAY BROOOOOO <---FROM AKHOURI ANMOL KUMAR
HAPPPY BIRTHDAY CTOOOOOO<--- FROM AKHOURI SYSTEMS
HAPPY BIRTHDAY @effessdev
MAY GOD BLESS *US *đ¤Ł
Thanks đđđđ
btw I am back!!! đĽ
When i came back nobody's here lol
đđ
This is not the latest post so not replying đ
Hahaha hahaha hahaha đ Peak CTO
Kya aapka image editor meri school photo ko aacha kar sakta hai?đ¤
None of them knew they were sitting next to a future IITian lol đ
Oh not latest post ugh my mistake đ
lol CTO & broness level peaked...
yeah try APIC WEB AI upscale it can.
Please aap hi kar dijiye na mujhe aalas aa raha hai
just say you don't want to sign up there bruh
Aisa nahi hai mere pass iska app tak hai lekin mujhe editing nahi aati hai.
Bro sign up kar diya hai please ab kar do na.
didi, wo thoda GPU instensive upscale hai and mai abhi cousins ke saath gaming kar rha hu isliye please aap manage kar lijiyega ya mai baad mei kardu
Okay! Rahne dijiye! đNo problem đ
ruhh that emoji really didi my cousins will get angry if I leave the game, I can see that you guys are chatting and I also wanted to do, but because I'm not in my home, I can't do it properly....
same like that I can't process your image to APIC WEB as to make your image good APIC WEB upscale will be gpu instensive and it may cause me to leave the game
Test 2 has a storage catch worth one more line. Cache API data is best-effort by default, so under storage pressure the browser can evict the whole origin, 176 MB model included, and the instant second run quietly turns into a fresh download. Calling
await navigator.storage.persist()asks for persistent storage, andnavigator.storage.estimate()tells you quota and usage before you start the 208 MB object remover. Checking the estimate first beats hitting aQuotaExceededErroratcache.put()after every byte already arrived. Chrome grants persist() on its own heuristics, Firefox may ask the user, so treat it as a request.Yeah you're actually right I never thought of that it is a request
It's an easy one to miss because nothing fails when it happens. One more piece:
navigator.storage.persisted()tells you whether the grant actually happened, so the app can show it instead of guessing. Safari is worth a line too. Its storage rules can clear script-written data, Cache API included, for sites the user hasn't opened in a while, unless the app is added to the home screen. So for a 176 MB model I'd treat the cache as a speedup and keep the download path tested. Do you show a 'ready offline' state anywhere in the editor?The SharedArrayBuffer / COOP-COEP trap is the one I would pin above any ONNX-in-browser checklist. Forcing
numThreads = 1on a static host is boring and correct; the silent phone hang when the "graceful" fallback never fires is the failure mode that actually burns users.The inline Web Worker via
BlobURL (Bonus) and the trilinear interpolation math for 3D LUTs (Test 6) are super clean! Doing binary search for target byte-size compression right in the browser is also such a practical trick. Great breakdown of real-world client-side constraints!Great work, Akhouri! đĽ Building all of this inside a single HTML file is impressive, especially the ONNX integration and the way you handle model downloads.
But the part that caught my attention wasnât the AI. It was the privacy claim.
âYour photos never leave your deviceâ is a powerful promise, but it also creates an interesting security question.
Even without a backend, the browser still executes JavaScript and downloads external resources. If any third-party script gets compromised, the fact that the application has no upload endpoint doesnât necessarily prevent image data from being transmitted elsewhere.
Have you considered a strict Content Security Policy, dependency integrity verification, or making the entire application work offline after the initial model download?
Iâd also be curious whether youâve tested the application with all network access disabled after loading the models.
To me, thatâs the difference between an application designed to process data locally and one whose privacy guarantees can actually be verified.
Really interesting project, bro. Keep building!
Thanks broooooo đĽ honestly this is the best question anyone asked on this post as you are the most dangerous bone of the akhouri systems a important bone...
You're right. "No upload endpoint" is only a statement about code, not a guarantee. Any script running in the page can read the canvas. I went and checked my own setup after your comment, so here is the honest list:
Broooo, now THIS is the kind of conversation I love! đđĽ
First of all, massive respect for actually going back and auditing your own implementation instead of just defending it. Thatâs exactly how good engineering happens.
And you found some interesting things!
One small distinction about the iframe: a sandboxed same-origin iframe isnât automatically unsafe. The important details are which sandbox permissions you grant, whether scripts and same-origin access are enabled together, and what the embedded content can actually do.
For the CDN dependencies, Iâd prioritize a restrictive CSP and SRI where applicable. But Iâd be particularly careful with third-party scripts running in the main document, because once they execute there, they share the pageâs privileges.
And hereâs another interesting challenge: checking DevTools Network is useful, but it only proves what happened during that particular session. It doesnât guarantee what a compromised dependency might do tomorrow.
As for APIC Web, haha, you got me! đ I spent more time reviewing the architecture than actually using the product.
Iâll give it a proper look. But you know me by now, bro. Iâll probably end up testing the security boundaries before editing a single photo. đ
Keep building, and keep questioning your own assumptions. Thatâs where the interesting discoveries happen!
that monster emoji by someone with 20-25year+ experienced person bruhh I'm cooked đ
The COOP and COEP lesson is the one I would print out. GitHub Pages cannot send those headers, so you lose SharedArrayBuffer, and the fix that stopped phones hanging was forcing numThreads to 1. That is a hosting decision showing up as a performance bug weeks later.
Switching from a fixed timeout to a stall based one also makes sense when the models range from 5 MB to over 200 MB. A slow phone that is still making progress should not be treated the same as a dead connection.
I liked OnePassApps' suggestion of a DevTools network test as proof for the "never uploads" claim. It turns a privacy promise into something a skeptical reader can check in a minute, and Harry's note about bumping the cache name is the kind of detail that saves a support thread later.
At Ascendra Ventures we try to write claims like that as acceptance tests before a build starts. Did the LaMa to MI-GAN fallback change how you describe quality to users?
Thanks broo, and a hostiing decision showing up as a performance bug weeks later is exactly how it felt đ ....
Yesss, the fallback changed how I describe quality. The dropdown (Fast / Best / Ultra) is a request, not a promise, so the UI now reports what actually ran. If LaMa fails, the status says "Ultra engine unavailable â using standard AI", and the result label says it was MI-GAN, never "Ultra". If the AI model can't load at all, it says it did a quick fill and suggests retrying. Ultra (~208 MB) is only offered on desktop-class devices, so phones just keep MI-GAN......
i haven't written those as automated acceptance tests yet I like that approach, especially for the "never uploads" claim. A DevToolss network check as a repeatable test is on my list now....
Got Python badge in this legendary post: dev.to/akhourianmolkumar/are-you-a...
Hope I'll get java script badge in this one... đĽđĽđĽ
đ
đ
Test 3's binary search is the right shape, but it has a quirk worth naming: you accept a mid only when blob.size <= targetBytes, and 7 iterations over [0.05, 0.98] leaves ~0.007 quality slack, so the returned blob can sit a few KB under the cap while a higher q would have fit â on a hard 200 KB limit I'd follow the search with one final probe at the accepted q plus a size guard, since re-encoding the same canvas at the same q can differ by a few bytes across engines and the server checks exact bytes. One question on Test 1: did you keep multi-threading behind the crossOriginIsolated check on hosts that do send COOP/COEP, and if so how much of the ONNX runtime's throughput did the single-thread path give up on desktop â wondering whether pinning numThreads=1 everywhere is costing enough that a proxy worker on a header-friendly host is worth the split code path.
Test 1 is the one that got me â "falls back gracefully" on desktop turning into a silent hang on a phone is exactly the kind of bug you only find by testing on the actual device. The stall-based timeout in test 2 is the fix I'd steal though; a fixed 30 seconds would kill every slow connection before the model even arrives. Was the phone hang the first one you hit, or did the PNG-going-black surprise land earlier?
I firstly hit actually phone hang png ones because it is more important if we compare to fixing png black issue
Pingtou is spot on about the inline Web Worker via blob URLâthat is an absolute lifesaver for keeping the UI thread from locking up during heavy processing.
Building complex AI interfaces (like the dashboard for GrantPro) taught me that the browser is often the real bottleneck, not the backend. I've run into similar memory limit crashes when handling large payloads.
Did you find WASM was consistently faster for the heavy image processing, or did the standard Canvas API hold up well enough for your use case? Great breakdown of the failures!
Thanks, Bogdan! Glad the blob-URL worker was useful. đ
To answer your question: they do different jobs, so it wasnât really a speed contest. WASM (ONNX Runtime) is how the AI models run: background removal and the object remover. Plain Canvas handles everything else, like resizing, flattening alpha, the JPEG quality search and the colour grading. Canvas holds up fine there because the browserâs own encoders do the heavy lifting.
The catch is that on a static host like GitHub Pages I canât send the COOP/COEP headers, so WASM runs single-threaded. That means itâs more âreliable on every phoneâ than âconsistently fastâ, and I havenât benchmarked the two against each other, so I wonât pretend I have numbers. đ
On memory crashes, three things helped me:
inpainting only runs on a crop around the brush instead of the full photo
ONNXâs memory arena and mem-pattern options are switched off
models are cached after the first download
What was the heavy payload in GrantPro? Curious whether you hit the limit in the browser or in the model step.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.