Build1 publisher3 min readPublished
One registry entry gates 16 of FreeToolHub's 21 in-browser tools on a single model
FreeToolHub runs 21 AI tools with no inference server. The mechanism is device accounting: one registry row per model recording backend, dtype, download size, and the memory floor below which it will not run.
The Engineer · Build desk

What happened
- FreeToolHub ships 21 AI-powered tools with no backend, no API keys and no inference server, downloading each model into the browser and running it on the user's device.
- A single TypeScript registry is the source of truth for all 18 models, recording HF repo, dtype, backend, download size, minimum device memory and the tools that use it, and a model ID appearing elsewhere is treated as a bug.
- The 1.7B model is registered twice, at 900 MB in q4f16 for the WebGPU backend and 1,400 MB in q4 for the WASM fallback, behind a declared minMemory of 2048.
- To get SharedArrayBuffer threading the site runs Cross-Origin-Embedder-Policy: credentialless rather than require-corp, which keeps cross-origin isolation without third-party opt-in.
- @imgly/background-removal fetches its WASM and glue .mjs from its own CDN at runtime, and a CDN-side update changed the artifact under the app.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- cost Users without WebGPU pay 500 MB more for the same model, 56 percent above the q4f16 build, and that bill lands on whoever is paying for the mobile data.
- constraint credentialless only holds while nothing in the pipeline needs cookies on a cross-origin fetch, so a signed or paywalled model host would put threading back in conflict with the third-party scripts that fund a free-tool site.
- decision With 16 of 21 tools on one model and versioned IndexedDB keys, bumping a model version is a catalogue-wide re-download of up to 1.4 GB per user, which makes it a release decision rather than a config edit.
- precedent Publishing the per-model fields, backend, dtype, download size and memory floor, sets a checkable bar for anyone else claiming browser-local inference: without those numbers the claim cannot be tested against a device.
The 600,000 ms in that registry entry sets a bandwidth eligibility test written as a constant [7]. Divide the WASM build by it. 1,400 MB over 600 seconds is 2.33 MB/s, about 18.7 Mbit/s held for the full ten minutes, or the load never completes [24]. The WebGPU build asks for less: 900 MB in the same window is 1.5 MB/s, roughly 12 Mbit/s [25]. Two backends, two throughput thresholds, both implied by one field nobody reads as a threshold.
That is why the small sibling does more work than the quantization trick. SmolLM2-360M is 200 MB, 4.5 times smaller than the WebGPU build of the 1.7B and 7 times smaller than the WASM build [16][27], and the author reports it handles rewrites, captions and short generation acceptably [16].
The memory field is the other half of the accounting. minMemory for smollm2-1.7b is 2048 with no unit given, but the neighbouring field is downloadSizeMB, so read it as megabytes [6]: subtract the 1,400 MB of q4 weights and the declared device floor leaves 648 MB for the browser, the page and the runtime [26]. Roughly two thirds of the stated requirement is the file itself.
Fan-out is recorded too. The 1.7B entry's usedBy list names three routes plus thirteen more [8], which is 16 of the 21 tools on one model [9]. Cached bytes live in IndexedDB under versioned keys, and bumping a model version invalidates them deliberately [17].
The header choice is COEP: credentialless instead of require-corp. Threading needs SharedArrayBuffer, which needs cross-origin isolation [10]. The textbook route, COEP require-corp, demands that every cross-origin subresource opt in with CORP or CORS, which ad networks, consent-management scripts, analytics beacons and image CDNs do not do, so they fail silently [11]. credentialless keeps the isolation and strips credentials from cross-origin requests instead of blocking them, and no-cors subresources still load [12]. It transfers on one condition: nothing in the pipeline needs a credentialed cross-origin fetch [13]. Theirs does not, because the weights come from HuggingFace and a fallback CDN with no cookies involved [13].
The dependency graph shows the collision plainly. transformers.js pins its own onnxruntime-web, so an app that also imports ORT directly gets two versions in one bundle, fighting over the WASM glue [18]. Their package.json declares onnxruntime-web 1.21.0 and then overrides the nested copy inside @huggingface/transformers to 1.25.1 [19]. Two runtimes on purpose, because the npm version and the shipped binary are only loosely coupled, and every loader that fetches .wasm at runtime is another chance to mix them [20].
One claim here does not travel. The line about leaving a third of your users behind by shipping a single quantization is a statement about FreeToolHub's own backend split [15]. To reuse the rule you need your own WebGPU share measured; if 95 percent of your traffic has WebGPU, the second quantization is a 1.4 GB artifact maintained for a rounding error. And the piece opens by promising the tool it had to kill [22], then stops at the background-removal CDN change without naming the tool or the configuration that failed [23]. A pity, because the config that fit nothing would have been the most useful row in the registry.
What to watch
- Whether a follow-up names the killed tool and the backend, dtype and memory combination that fit no device.
- Whether transformers.js stops pinning its own onnxruntime-web, which would retire the deliberate two-version override.
- Whether @imgly ships pinned, self-hostable WASM artifacts instead of fetching them from its own CDN at runtime.