Boundaries
Published
Browser-side inference has stopped being exotic. Classification, embeddings, background removal, transcription, small language models, recommendation scoring — all of it now runs on the client, and teams doing it reach the same question everyone else reaches: can we protect this before we ship it. The answer has an unusually clean boundary, and it is not the one most people expect.
Four things ship, and one of them is yours
Open the network panel on a page doing browser-side inference and you will see four distinct categories of artifact, which is worth separating before discussing any of them.
There is a runtime library: the framework that executes the model, usually a large JavaScript file with a compiled numeric module beside it. There is the model file: the weights, in whatever format your framework consumes, typically the largest thing on the page. There is supporting data: a tokenizer, a vocabulary, a label map, normalisation constants, often plain JSON. And there is your glue code: the JavaScript you wrote to load the model, prepare inputs, run inference and turn the output into something your product uses.
A JavaScript protector operates on the fourth category and, if you choose, the first. It does not operate on the second or third in any meaningful sense. That single sentence is most of this article, and everything below is either the mechanism behind it or the consequence of it.
Why the weights are untouched, precisely
The tool parses JavaScript and emits JavaScript. A weights file is not a program; it is a numeric array in a container format, fetched over the network at run time like an image or a font. There is no parse to perform and nothing to rename, so there is no transformation available even in principle.
The build integrations make this concrete rather than theoretical. The bundler plugins filter the assets your build emits down to JavaScript files by default before deciding what to protect. An emitted model file, in any format, is not in that set. It is not skipped because of a policy decision; it never becomes a candidate. If you want to confirm this on your own project rather than take it on trust, check the protection manifest after a build — it records the files that went through the run, and your model will not be among them.
This is the same boundary that appears elsewhere on this site from different directions. A shader has to be handed to the driver as exact text. A browser build that ships intermediate language ships something a JavaScript tool does not transform. Moving code into WebAssembly moves it out of this tool's reach without moving it out of an analyst's. The rule underneath all four cases is the same: the protector's scope is JavaScript, and it is worth knowing exactly which of your shipped bytes that covers.
The base64 temptation, and what it actually costs
Somebody on every team proposes it eventually: inline the model into the bundle as a base64 string, so there is no separate download to notice and no obvious file to save. It is worth walking through what happens, because the outcome is unusually lopsided.
You now have a very large string literal in a JavaScript file. The string transformations will treat it as a string. Moving literals into a lookup table relocates it and leaves the bytes intact behind one level of indirection. The escaping the writer performs converts characters outside the ASCII range into escape sequences, which does nothing here because a base64 payload is already ASCII. Nothing about the content becomes harder to obtain, because your own code has to decode it to use it, and the decode site is where anyone extracts it in one line.
The costs, meanwhile, are real and immediate. The bundle grows by roughly a third over the binary size before any other transformation. The parse is slower, and unlike a fetched asset it is on the critical path of your first script evaluation rather than streaming in parallel. You lose ordinary caching: a fetched model file is cached separately and survives your next deployment, while an inlined one is re-downloaded every time you ship any code change, which the caching article covers as a general failure of inlining large assets. And a very large literal in the string table is the kind of thing that makes tooling behave strangely.
If you have a large literal in your bundle for any reason, the right handling is the opposite of hiding it: keep it out of the string transformations using the reserved strings mechanism, which keeps matching literals verbatim and outside both the move and the encode steps. That is the same advice the shader article gives, for the same reason. You are not gaining concealment by putting a blob through a transformation designed for source code; you are only paying for it.
The download is the loudest thing your page does
Even setting the file format aside, the delivery is conspicuous in a way that no build setting changes. A model download is frequently the largest single request an application makes, sometimes by an order of magnitude, and large requests are exactly what people notice when a page feels slow. It appears in the network panel with a URL, a content length and a response body that saves to disk with a right-click.
Protecting the JavaScript around it changes how readable the request site is. It does not change that the request happens, what it returns, or that a browser is a tool for downloading and inspecting things. This is the same structural point as the article about API keys: the network is a channel your user owns, and anything travelling over it to their machine has arrived.
Serving weights behind authentication is still worth doing, and it is worth being clear about what it accomplishes. It stops anonymous bulk collection, it gives you a per-account audit trail, and it lets you revoke. It does not stop a legitimate user from keeping the file, because by the time the model is running they have it. Treat it as access management, and price the feature assuming your customers can retain what you send them.
What is actually worth protecting here
The interesting part of this topic is not the loss, it is the reframing. Teams arrive worried about the weights and leave realising the weights were the commodity.
Consider what your glue code contains. The exact preprocessing that makes an input match what the model saw during training, which is frequently the difference between a model that works in your product and the same model performing poorly for somebody else. The thresholds you arrived at empirically. The tie-breaking and confidence handling. The rules that turn a raw score into a product decision — what gets flagged, what gets surfaced first, what gets a fallback. The routing between a small local model and a larger hosted one. The post-processing that cleans up output nobody would ship raw.
All of that is ordinary JavaScript, all of it is transformed normally, and for a great many products it is closer to the actual intellectual property than the weights are — especially if the model itself is a public pretrained one that anybody can download. A competitor with your model file and none of that pipeline has a generic capability. A competitor with your pipeline and a generic model often has your product.
Which suggests a more useful order of operations than the one people arrive with. Ask which part of the feature you would actually mind a competitor reading. If the honest answer is the weights, then the feature belongs on your server, because shipping a model to a browser is distributing it, and no build setting undoes that. If the honest answer is the pipeline, you are in the ordinary case this product is built for, and the model being publicly readable costs you nothing you were relying on.
Tampering, which is a different question
Confidentiality is only half of what teams want here. The other half is integrity: what happens when a user modifies the inference path rather than reading it. A client-side content filter that gets bypassed. A confidence score forced to a value. A model file swapped for a different one. A rate limit implemented in the same JavaScript that calls the model.
Protection genuinely helps with the cost side of this. Someone has to find the relevant code before changing it, and transformed output makes that slower and less transferable. Default per-build regeneration means a patch written against one release refers to names and offsets that will not exist in the next one, which is the same dynamic the bot detection article describes as the honest benefit in adversarial settings. The integrity options add detection: the anti monkey patching guard watches a defined list of platform functions, including the request machinery a swapped model would come through, and re-checks them on a short heartbeat.
None of that changes the underlying fact. A decision made in the browser is made on hardware the user controls, and a determined user can reach it. If a result has consequences — eligibility, pricing, moderation, access — produce it or validate it somewhere they cannot. The client-side model then becomes what it should be: a fast, cheap, private first pass whose answer is confirmed where it counts.
A note on licences
One practical thing that catches teams out. Pretrained models carry licence terms, and some of those terms are more demanding than the permissive ones common in JavaScript dependencies — conditions on redistribution, on attribution, on categories of use. Those conditions attach to the weights file, and shipping it to browsers is distribution.
Transforming your JavaScript does not change any of it. What it can do, if you are careless, is remove a required attribution notice that lived in a comment. Comments do not survive a protection step. Keep required notices in a form that survives — a separate file served alongside, or output your build appends after protection — and verify they are present in the artifact you actually ship rather than in the source you meant to ship. The article on legality covers the general shape of this for code dependencies; models simply raise the stakes.
Frequently asked questions
Does a JavaScript protector do anything to my model weights?
Nothing at all, and that is by definition rather than by omission. The tool parses JavaScript and emits JavaScript. A weights file is a separate asset that your page fetches over the network, in a numeric format that is not a program, and it passes through your build untouched. The bundler plugins make the boundary concrete: they filter emitted assets down to JavaScript files by default, so a model file is never even a candidate for the protection step. Whatever your model file cost you to produce, it ships exactly as your training run wrote it.
What if I inline the model as a base64 string inside my bundle?
Then you have a very large string literal in a JavaScript file, and the transformations that apply to strings will apply to it with no confidentiality benefit and a real cost. Moving it into a lookup table relocates it and leaves it intact. The escaping the writer performs makes output pure ASCII, which a base64 payload already is, so nothing changes there either. What you get is a bigger file, a slower parse and a slower start, in exchange for a payload that any reader can extract with one line at the point of decoding. Keep the model a separate fetched asset and exclude any large literal from the string transformations.
Can I hide the fact that a model is being downloaded?
Not in any practical sense, because a model download is the most conspicuous event on the page. It is usually the largest single request the application makes, often by an order of magnitude, and it appears in the network panel with a URL, a size and a response body that can be saved to disk. Protecting the code that issues the request changes the readability of the request site, not the existence of the request. A user who wants the file opens the network panel once.
Does serving the weights behind authentication solve it?
It changes who can get the file, which is worth doing, and it does not change what a legitimate user has once they have it. An authenticated download still arrives in a browser the user controls, still passes through the network panel, and is still cacheable on their disk. That is a meaningful control against anonymous bulk collection and no control at all against a paying customer who decides to keep a copy. Treat it as access management rather than as concealment, and price the feature on the assumption that customers have the file.
Which part of an in-browser ML feature is actually worth protecting?
The pipeline around the model, which is ordinary JavaScript and is frequently the part that took the real work. Feature extraction and normalisation, the exact preprocessing that makes your inputs match training, the thresholds and tie-breaking applied to raw output, the rules that turn a score into a product decision, and the fallbacks when confidence is low. A commodity model with your preprocessing is often much closer to your product than the model alone, and all of that is JavaScript a protector transforms normally.
Does running inference in the browser change the answer about proprietary models?
It makes the question unavoidable rather than changing it. Browser-side inference means shipping the model, and shipping the model means distributing it. If the weights are genuinely the asset you sell, then inference belongs on your server, where the model stays and the client receives only results. That is a product architecture decision rather than a build setting, and it is the same conclusion this site reaches about every other kind of value people hope to keep in a bundle.
Does protection help at all against someone tampering with the inference path?
It raises the cost of the specific work, which matters more here than for confidentiality. Someone bypassing a client-side content filter, forcing a confidence score, or swapping the model file has to locate the relevant code first, and transformed output makes that slower. Regenerated output also makes their patch perishable, since the names and offsets it depends on move at your next release. What none of it changes is that a decision made in the browser is the user's to make. If a result carries consequences, the result has to be produced or validated where the user cannot reach it.
Does obfuscating my bundle affect the licence on a pretrained model?
No, and the two are unrelated in a way worth stating explicitly. Model licences carry their own conditions on redistribution, attribution and permitted use, and those conditions follow the weights file wherever it goes. Transforming the JavaScript that loads it does not alter any of them, and removing an attribution notice during a build step is a licensing problem you have created rather than a protection you have gained. Keep required notices in the shipped output and check them after protection rather than before.
Related reading