Library / SDK
infinitered/nsfwjs avatar
infinitered/nsfwjs

nsfwjs: client-side NSFW image classification with TensorFlow.js

NSFW detection on the client-side via TensorFlow.js

8,997 stars598 forksTypeScriptMIT

At a glance

What is it?
nsfwjs runs a TensorFlow.js classifier in the browser, in Node.js or in React Native and returns five probability labels per image. The README claims roughly 90 percent accuracy with the small model and about 93 percent with the midsized one, and the library ships three bundled models you can also host yourself.
Who is it for?
Adopt nsfwjs when you want image triage to happen on the user's device and you can accept a five-class probability output rather than a binary verdict. Skip it if you need a hard moderation decision with an audit trail, or if your images already live on a server you control, where a server-side model removes the download cost entirely.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 58 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What nsfwjs classifies, and who ends up using it

nsfwjs labels an image with five probabilities: Drawing, Hentai, Neutral, Porn and Sexy. The README defines Drawing as safe-for-work drawings including anime, Hentai as hentai and pornographic drawings, Neutral as safe-for-work neutral images, Porn as pornographic images and sexual acts, and Sexy as sexually explicit images that are not pornography. That split matters because it separates illustrated content from photographic content, which a single binary NSFW score cannot do.

The intended audience is a front-end or React Native developer who wants a first pass over user-supplied images without sending them to a server. The README frames the library as a way to "quickly identify unseemly images; all in the client's browser", and the repository ships examples for the browser, Browserify, React Native and Node.js. The trade-off is baked into that design: the model runs where the user can inspect it, and the classification result is a probability vector, not a moderation decision.

How the load and classify pipeline actually works

There are two moving parts: a model and a classifier. nsfwjs.load() returns a model object. Calling model.classify(img) on that object returns the predictions array. The README's quick example does exactly this and logs the result.

Three models are built into the package: MobileNetV2, MobileNetV2Mid and InceptionV3. The first parameter of load selects among them, and the default is MobileNetV2. MobileNetV2 expects 224x224 input. InceptionV3 expects 299x299 and is loaded with a size option. MobileNetV2Mid is a graph model, which the README says must be loaded with type: 'graph', and it notes that the infer method is unavailable on graph models.

The package is published as ESM and CJS through an exports map with separate entry points for the main module, nsfwjs/core, and each model. That map is what makes tree-shaking possible: importing from nsfwjs/core gives you load without pulling the bundled model definitions into your bundle, and you then pass the models you want in modelDefinitions. Passing an empty array throws when you ask for a named bundled model. The bundled MobileNetV2 weights are base64-encoded inside the package, which the README says inflates that model to 3.5MB against 2.6MB for the same weights served as binary files.

Installing nsfwjs and classifying your first image

The README's Install section is the entry point; the package name is nsfwjs. Install it with npm or yarn, then load a model and classify an element from the page.

bash
npm install nsfwjs

The quick-start example from the README loads the default model and classifies an image element. Predictions is the array you log.

js
import * as nsfwjs from "nsfwjs";

const img = document.getElementById("img");
const model = await nsfwjs.load();
const predictions = await model.classify(img);
console.log("Predictions: ", predictions);

If you want the smaller bundle, import load from nsfwjs/core and register only the models you use. The README warns that an empty modelDefinitions array makes named bundled loads throw.

js
import { load } from "nsfwjs/core";
import { MobileNetV2Model } from "nsfwjs/models/mobilenet_v2";
import { MobileNetV2MidModel } from "nsfwjs/models/mobilenet_v2_mid";

const model = await load("MobileNetV2", {
  modelDefinitions: [MobileNetV2Model, MobileNetV2MidModel],
});

To serve the weights yourself, pass a URL instead of a model name. The README shows both a directory path and a direct model.json path, and notes that self-hosted binary files avoid the base64 overhead.

js
const model = nsfwjs.load("/path/to/mobilenet_v2/");

One operational detail from the README: the Cloudfront-hosted model has been moved, so if you were relying on it, follow the Host your own model section rather than pointing at the old URL.

Where the five-class output stops being enough

The README is candid that the library "isn't perfect". The stated accuracy is about 90 percent with the small model and about 93 percent with the midsized model, and the README does not break those numbers down per class. For a moderation workflow that is a meaningful gap: 90 percent accuracy on a binary decision means roughly one in ten images is mislabeled, and the cost of a false negative on Porn is very different from a false positive on Drawing.

The label set itself creates ambiguity. Sexy and Porn are separate classes, and Hentai sits apart from Porn because one is illustrated and one is photographic. If your policy treats all three as a single violation, you are writing that mapping yourself, and the README does not describe how the probabilities should be combined.

There is also a structural limit. The model runs on the client, so anything a determined user does to the page can affect the result, and the classifier only sees images you hand it. It is a triage layer, not an enforcement mechanism. And because the weights are bundled or fetched, the first classification on a cold cache pays the model download before any inference happens.

nsfwjs compared with server-side moderation APIs

The obvious alternative is a hosted moderation service that accepts an image and returns a verdict. The difference in approach is where the pixels go. With nsfwjs the image never leaves the device; the classifier runs in the browser, Node.js or React Native through TensorFlow.js, and the model files are the only thing downloaded. With a hosted API you upload the image and the vendor's model does the work.

That choice determines your failure modes. Client-side classification keeps user photos off your servers, which simplifies what you have to store and what you have to protect. It also means you cannot change the model without shipping a new bundle or a new hosted model path, you cannot audit classifications centrally unless you send the predictions somewhere, and you inherit the accuracy of whichever of the three bundled models you picked. A server-side API gives you a single place to log decisions and swap models, at the cost of uploading every image and paying per call.

Within nsfwjs itself, the three models are a comparison worth running: MobileNetV2 at 224x224 for the smallest footprint, MobileNetV2Mid as a graph model, and InceptionV3 at 299x299 for the largest input. The README gives accuracy figures for small and midsized models but not for InceptionV3.

Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-08-04, the same day as the v4.4.0 release. The release history shows a gap: v4.2.0 landed on 2024-10-11 with an ESM bundling update, v4.2.1 followed on 2024-11-11, and then nothing until v4.4.0 in August 2026. That pattern suggests releases arrive in bursts around bundling and packaging work rather than on a schedule, so pin your version and read the release notes before moving.

The upgrade surface is mostly packaging. The exports map defines nsfwjs/core and one entry per model, so an upgrade that reshuffles those paths can break imports even when the classify API is unchanged. The tree-shaking path is the newest addition and the one most likely to move.

The licence is MIT, which permits commercial use and modification provided the copyright notice and permission notice are included. That is a statement about the licence text, not legal advice; if you redistribute the bundled model weights, check the terms that apply to those weights separately.

Editorial conclusion

Adopt nsfwjs when you want image triage to happen on the user's device and you can accept a five-class probability output rather than a binary verdict. Skip it if you need a hard moderation decision with an audit trail, or if your images already live on a server you control, where a server-side model removes the download cost entirely. Before committing, load all three bundled models against your own sample set and compare the labels, because the README gives accuracy figures but no per-class breakdown, and the Cloudfront hosted model has been moved, so plan to host the model files yourself.

Frequently asked questions

What is an NSFW filter?

In the context of nsfwjs it is a classifier that assigns an image probabilities across five classes (Drawing, Hentai, Neutral, Porn and Sexy) so you can flag or block content before it is displayed. nsfwjs runs that classifier on the client through TensorFlow.js rather than on a server.

Can I use nsfwjs in a React Native app?

The repository ships a React Native example under examples/, and the README lists React Native in its run-the-examples section. The classification API is the same load and classify pair used in the browser.

Which models does nsfwjs bundle, and what input sizes do they need?

The three bundled models are MobileNetV2 (224x224, the default), MobileNetV2Mid and InceptionV3 (299x299). MobileNetV2Mid is a graph model and the README says it must be loaded with type: 'graph', which also means the infer method is unavailable.

Official sources

  1. infinitered/nsfwjs on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/infinitered-nsfwjs.svg)](https://hysenlabs.com/projects/infinitered-nsfwjs)