What a 5 MB super-resolution model can (and can't) do inside a browser tab
I enlarged a 563×449 photo to 1576×1257 without uploading it anywhere. The file was 147 KB before and 515.6 KB after, and it took 14.2 seconds. The interesting part isn't the result, it's the three questions it raises:
I enlarged a 563×449 photo to 1576×1257 without uploading it anywhere. The file was 147 KB before and 515.6 KB after, and it took 14.2 seconds. The interesting part isn't the result, it's the three questions it raises:
- Why did the file get 3.5× bigger if no new information exists?
- Why is the work split into "part 10 of 20"?
- Why do these tools cap the input at about 2 megapixels?
Let's answer them with a bit of arithmetic and some small snippets.
Resizing vs. super-resolution
A normal resize (bilinear, bicubic, Lanczos) computes every new pixel as a weighted average of its neighbours. It can only blend what is already there, so edges get softer as you enlarge.
// The baseline every "AI upscaler" should be compared against
function resizeBaseline(img, scale) {
const c = document.createElement('canvas');
c.width = Math.round(img.naturalWidth * scale);
c.height = Math.round(img.naturalHeight * scale);
const ctx = c.getContext('2d');
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, c.width, c.height);
return c;
}
A super-resolution network such as Real-ESRGAN (Wang et al., 2021, open source, BSD-licensed) does something different. It was trained on pairs of degraded and clean images, so it has learned what a plausible edge, a hair or a brick texture looks like, and it paints those in. The result looks sharper, but the extra detail is a statistically likely guess, not recovered truth. That's why a face can get crisp skin texture it never had, and why the same model is the wrong tool for a screenshot of text, where a "plausible" letter shape can be a wrong letter.
Rule of thumb I use: photos, artwork, product shots → try the network. Screenshots, charts, scans of text → plain resizing.
Why the file got bigger
Compression ratio depends on how much high-frequency detail an image has. The network adds texture, texture is expensive for JPEG, and the pixel count grew too:
563 × 449 = 252,787 px
1576 × 1257 = 1,981,032 px (≈ 7.8× more pixels, scale ≈ 2.8× per side)
147 KB → 515.6 KB (≈ 3.5× more bytes)
Bytes grew less than pixels because JPEG handles smooth areas cheaply. The takeaway: a bigger file is not a better file, and you should expect to run a compressor afterwards.
Why "part 10 of 20"
The model itself is small. The tool I tested says its model is about 5 MB. The tensors are the problem. Back-of-the-envelope, for a worst-case 2 MP input enlarged 4× (that's 32 MP of output):
output RGBA bytes: 32e6 * 4 ≈ 128 MB
float32 RGB activations: 32e6 * 3 * 4 ≈ 384 MB (for the final layer alone)
and intermediate feature maps are many channels wide, so the real peak is far higher. A browser tab on a laptop or a phone will not survive that, so the image is processed in chunks. The generic recipe looks like this (a sketch of the idea, not any particular tool's code):
const T = 128, PAD = 8, SCALE = 4; // tile size, overlap, model factor
for (let y = 0; y < h; y += T) {
for (let x = 0; x < w; x += T) {
// read a little more than the tile so the seams have context
const sx = Math.max(0, x - PAD), sy = Math.max(0, y - PAD);
const ex = Math.min(w, x + T + PAD), ey = Math.min(h, y + T + PAD);
const up = await runModel(cropRGB(src, sx, sy, ex, ey)); // SCALE× bigger
// keep only the inner part, drop the padded border
blit(dst, up,
(x - sx) * SCALE, (y - sy) * SCALE,
Math.min(T, w - x) * SCALE, Math.min(T, h - y) * SCALE,
x * SCALE, y * SCALE);
onProgress(++done / total); // "part 10 of 20" comes from here
}
}
The overlap matters. Without it, every tile boundary becomes a faint grid line because the network has no context beyond the edge of its input.
That also explains the caps you see in these tools: the one I tried accepts JPG/PNG/WebP up to 16 MB and about 2 MP, and limits the output's longest side to 4096 px. Those are not arbitrary; they keep the memory estimate above inside what a normal device can take.
WebGPU or not
The same model can run on the GPU through WebGPU or fall back to the CPU. Detecting it is cheap:
async function pickBackend() {
if ('gpu' in navigator) {
try {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) return 'webgpu';
} catch { /* fall through */ }
}
return 'cpu';
}
The difference is large. In my test the status line read Enhancing (WebGPU) — part 10 of 20 · about 8 s left. The page itself warns that without WebGPU a 1 MP photo "can take a minute or two". If you build something like this, show the backend and a time estimate in the UI; people forgive a slow job far more easily than a silent one.
What I'd do in a real project
-
Compare against the baseline. Always render
resizeBaselinenext to the network output. If you can't see a difference at 100% zoom, skip the network and save the battery. - Don't promise recovery. "Adds detail" is honest, "restores your photo" is not. Very blurry or heavily compressed sources improve only a little.
- Cap the input, cap the output. Do the memory arithmetic first, then pick the limits.
- Cache the model. A one-time 5 MB download is fine; downloading it on every visit is not.
- Ship the tiling progress. It is the cheapest way to make a 15-second job feel acceptable.
If you want to try the browser-only setup yourself without writing it, the tool I used for the numbers above is here. Whatever you use, test it with one photo and one screenshot. The contrast between the two tells you more than any benchmark.
What's the largest image you've pushed through a model in the browser before the tab gave up? I'd like to know where your limit was.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.