
The upscale worked. That was the problem.
I run a small frontend team on an e-commerce SaaS, and our product image pipeline has a long tail of tiny legacy thumbnails. Browser-side AI upscaling looked like a cheap fix, so I spent an afternoon measuring it instead of looking at it. The tool was ImgIng (https://imging.ai/), running on an M4 Mac with 16 GB, headless Chromium 149 with WebGPU, PNG output forced so no lossy encoder would sneak into my numbers.
A 41 MB success
I fed it a 1600×1200 photo and asked for 4×. Out came 6400×4800 — 30.7 megapixels, 41 MB as PNG, 38.7 seconds. Nothing crashed, no error, the browser handed me a perfectly good Blob.
Our upload endpoint caps files at 10 MB. So the happy path here is a user waiting 38 seconds and then being told their file is too big. That is worse than not shipping the feature at all, and it is the kind of thing you only find by running the largest input you can think of rather than the one in the demo.
The 2× run is the sane one for a pipeline: 3200×2400, 11.4 MB, 28.8 seconds. Note that it is only 10 seconds faster, which brings me to the part that broke one of my assumptions.
2× is not half the work
I had written "fall back to 2× on mobile to cut the wait" into my design doc. That line is now deleted.
All five model presets are natively 4× — the backend label in the UI is always x4, x4plus or x4v3. Picking 2× means it runs the same 4× inference and then scales the result back down. Same compute. On several of my test cases 2× was actually slower than 4×. What 2× does save is output bytes and memory, not time.
Memory: three indirect signals, no real number
I wanted a VRAM curve. I did not get one.
All I could read was the JS heap: 12 MB peak on the 4× run, 98 MB on the 2× run. Those are inverted relative to output size, which tells you the number is measuring main-thread JS objects, not the tensors. The tensors live in a Worker and on the GPU, performance.memory does not expose that, and there is no usable WebGPU memory API.
So "it handles big images" rests on three indirect things: it finished, the progress bar moved in discrete steps of 1.5 to 2.25 percentage points (about 45 steps, roughly 0.9 s each — that reads like tile-by-tile work rather than one giant pass, though the UI never says so), and I could not find tile seams in the output. I scanned every column and row for spikes in mean neighbour difference; the spikes I found were spaced 1, 3587, 134, 44, 10, 53 apart — nowhere near uniform — and they land on the same source-image row when you divide by the scale factor, so they are strong edges in the photo, not seams. In this one case I found no evenly spaced tile boundaries. One photo, one preset, two scale factors. I would not generalise past that.
Presets differ by an order of magnitude
Fast preset: 1.2 to 2.1 seconds across my four samples. Top preset: 16 to 126 seconds, worst case 126.2 s. Those are not two settings of one feature, they are two different interaction designs — one can be instant-on, the other needs progress, cancellation and a story for what happens when the user navigates away.
Each preset is a separate model download, 4.65 to 154.38 MiB, 318.3 MiB for all five after de-duplication. First run of a preset took 11.3 s including the download; after a page reload, zero model requests and 2.2 s. Your first user and your returning user are effectively using two different products.
And the timings only hold for this machine. With a software-rasteriser WebGPU adapter the same case took 104 seconds versus 11.3 on Metal — a 9× spread. Any number in this post that ends in "seconds" is hardware-specific and I would not put it in a spec.
What mobile does about it
At a 390×844 viewport with DPR 3, the UI adds a line saying it defaults to the fast preset at 2× and limits the final pixel count so mobile browsers do not get killed for memory. I read the radio buttons: the two heaviest presets have disabled set to true, while all five are selectable on desktop. I like this more than anything else in the feature — rather than gambling on the device, it removes the options. I have not run it on a real phone, and the UI does not say what the pixel cap actually is, so that is as far as I can take it.
We ended up not putting upscaling in the automatic path. It is a manual step now, capped at the width the detail page needs, and the 4× output never reaches the upload endpoint. The measurement that changed the design was not a quality score — it was a file size.
Top comments (0)