CLI tool
bfirsh/jsnes avatar
bfirsh/jsnes

jsnes: A JavaScript NES Emulator You Embed in a Page or Drive From Node

A JavaScript NES emulator.

6,418 stars860 forksJavaScriptApache-2.0

At a glance

What is it?
jsnes is a JavaScript NES emulator published as a library for browsers and Node.js. It ships a ready-made Browser wrapper for canvas, audio and input, and a lower-level NES class when you want to own the render loop.
Who is it for?
Adopt jsnes if you want an NES core you can call frame by frame from JavaScript, whether that is a canvas embed or a Node process, and you are willing to supply your own ROM. Do not adopt it if you need a battery-included player with a ROM library, save-state UI or mobile touch controls, because the library gives you the core and the Browser wrapper but not a product.
Can I use it commercially?
Yes. Apache-2.0 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 2 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

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

Editorial analysis

What jsnes actually solves, and who it is for

The repository describes itself in one line: "A JavaScript NES emulator." The useful part is the next sentence, that it is a library working in both the browser and Node.js. That framing tells you who the intended user is. This is not a retro gaming site with a ROM catalogue. It is a 6502 and PPU implementation you call from your own code.

The practical audience is developers who already have a reason to run NES software inside a JavaScript runtime. That might be a web page that embeds a single homebrew ROM, a Node process that steps frames to produce video or test output, or a React application where the emulator is one component among many. The README points to two such frontends in the repository: a complete embedding example under example/, and a full-featured React frontend under web/.

What jsnes gives you is the emulation core plus a convenience layer. What it does not give you is a distribution channel for games. The repository has a roms/ directory, but the README never describes it as a library of playable titles, and the licensing of any particular ROM is your problem, not the library's.

The two-layer architecture: NES core and Browser wrapper

jsnes exposes two levels, and choosing between them is the main design decision you make as an integrator.

The lower level is the NES class. You construct it with callbacks, and you drive it yourself. The README's Node.js example shows the shape: onFrame receives a 256×240 pixel buffer as an Int32Array of ARGB values, onAudioSample receives left and right channel values between -1.0 and 1.0, and you call nes.frame() in a loop. The comment in that example is blunt about the contract: "Run frames at 60 fps, or as fast as you can. You are responsible for reliable timing as best you can on your platform." That sentence is the whole trade-off. You get control over rendering, audio and scheduling, and you accept the responsibility for all three.

The upper level is the Browser class. According to the README it "handles canvas rendering, audio, keyboard input, gamepad input, and frame timing automatically." You give it a container element, optionally ROM data, and it starts. The instance exposes browser.nes, browser.keyboard and browser.gamepad, so the wrapper is not a sealed box; you can reach the core underneath and remap keys or gamepad buttons at runtime through browser.keyboard.setKeys().

Input flows through constants on jsnes.Controller: BUTTON_A, BUTTON_B, BUTTON_SELECT, BUTTON_START, the four directions, and BUTTON_TURBO_A and BUTTON_TURBO_B. The turbo buttons are not separate hardware; per the README they "behave like A and B but auto-fire repeatedly while the key is held."

State is handled through nes.toJSON() and nes.fromJSON(data), which the README describes as serializing and restoring emulator state for save states. Battery-backed SRAM is separate: onBatteryRamWrite(address, value) fires when that memory is written, and the README says to use it to persist save data. Those are two different persistence problems, and the library hands both to you.

Installing jsnes and running a first frame

For a bundler or Node, the README gives one command:

bash
npm install jsnes

For a plain web page, the README uses unpkg and a script tag pointing at the minified build:

html
<script type="text/javascript" src="https://unpkg.com/jsnes@2/dist/jsnes.min.js"></script>

With the script loaded, the shortest working embed is the Browser class. You provide a container element and an error callback, then fetch the ROM and hand the bytes to the instance:

javascript
var browser = new jsnes.Browser({
  container: document.getElementById("nes"),
  onError: function (e) {
    console.error(e);
  },
});
jsnes.Browser.loadROMFromURL("my-rom.nes", function (err, data) {
  if (err) {
    console.error(err);
    return;
  }
  browser.loadROM(data);
});

If you already hold the ROM bytes, the README shows passing romData directly in the constructor instead, and notes that emulation starts automatically when romData is provided. The default keyboard mapping puts the D-pad on the arrow keys, A on X, B on Z or Y, Start on Enter and Select on Right Ctrl.

To see a complete embed rather than a fragment, the README says the example/ directory holds one and that running npx serve . in the repository root serves it at http://localhost:3000/example/nes-embed. That is the fastest way to confirm your browser and audio setup work before you write your own integration.

For a Node or custom pipeline, skip the wrapper. Construct NES with onFrame and onAudioSample, read the ROM with fs.readFileSync using binary encoding, call nes.loadROM(romData), then call nes.frame() repeatedly. Button presses are explicit too: nes.buttonDown(1, jsnes.Controller.BUTTON_A), a frame, then nes.buttonUp. Nothing happens between frames, so input timing is entirely yours.

Where jsnes stops and you have to start

The README is honest about the frame loop, and that honesty is also the limitation. In the low-level path you own timing. A dropped frame in a browser tab that has lost focus, or a Node process competing for CPU, shows up as a game that runs slow or fast. The library gives you nes.getFPS() and nes.setFramerate(rate) to observe and adjust, but it does not give you a scheduler.

The Browser wrapper removes that burden, but it also removes your control. It owns the canvas, the audio graph, the keyboard listeners and the gamepad polling. If you need a rendering pipeline the wrapper does not produce, for example compositing the frame buffer into a WebGL texture, you are back on the NES class and back to writing your own timing.

Persistence is the second gap. Save states exist as toJSON() and fromJSON(), and battery SRAM writes surface through onBatteryRamWrite, but the README does not document a storage backend. There is no built-in IndexedDB layer, no file format, no versioning of serialized state. If the shape of toJSON() output changes between releases, restoring an old save is your migration to write. The README does not document rollback or compatibility guarantees for serialized state.

Audio is the third. onAudioSample delivers samples one at a time, and the README's Node example leaves playback entirely to you, with the comment "... play audio sample" standing in for whatever buffer and output device you choose. That is a real piece of work, not a detail.

Finally, ROM legality is outside the library. jsnes emulates the hardware; it does not grant you rights to any particular game image.

How jsnes differs from EmulatorJS and Nostalgist JS

The alternatives people search for alongside jsnes take a different position on the same problem.

EmulatorJS is built around a player: you drop in a container and a ROM and you get a UI with controls, save slots and a library of supported systems. jsnes is a core with a thin wrapper and no player chrome. If your goal is to put a playable NES game on a page this afternoon, the player approach is less work. If your goal is to own the frame loop, the input mapping and the rendering path, jsnes is the closer fit, because the Browser wrapper still exposes browser.nes, browser.keyboard and browser.gamepad rather than hiding them.

Nostalgist JS is oriented toward running emulator cores in the browser through a loader, which means the emulation itself comes from a core rather than from JavaScript written for the task. jsnes is a single JavaScript implementation you install with npm install jsnes or load from unpkg, with no core-download step and no loader layer. That is simpler to reason about and simpler to bundle, and it also means the set of systems you get is exactly one.

WebNES and the older Jnes-style browser emulators occupy the same space as jsnes historically did: a page you visit to play. jsnes is the library extracted from that idea, which is why the README spends its length on API tables rather than on a game list.

Maintenance, releases and what the licence means for you

The repository is not archived, and the last push was on 2026-09-22. The most recent release listed is v2.1.0 on 2026-04-11, following v2.0.0 on 2026-03-01. That is a project with recent activity and a version 2 line that has moved at least twice this year.

Upgrade cost is dominated by the version 2 break. The npm install line in the README is unversioned, but the unpkg URL is pinned to jsnes@2, and the package.json exports map points import at ./src/index.js, require at ./dist/jsnes.js and types at ./index.d.ts. If you are on version 1, expect the Browser class and the NES options to be where the changes land, since those are what the current README documents. The package ships dist, src and index.d.ts, so TypeScript consumers get declarations without a separate types package.

On licence: the repository is Apache-2.0, and package.json declares the same. Apache-2.0 is permissive and includes an explicit patent grant, which matters for a project implementing patented-era hardware behaviour. It does not cover ROMs, BIOS files or game assets you load into the emulator; those carry their own terms. This is a description of what the licence file says, not legal advice, and if you are shipping a commercial product you should have someone qualified read it against your distribution model.

Editorial conclusion

Adopt jsnes if you want an NES core you can call frame by frame from JavaScript, whether that is a canvas embed or a Node process, and you are willing to supply your own ROM. Do not adopt it if you need a battery-included player with a ROM library, save-state UI or mobile touch controls, because the library gives you the core and the Browser wrapper but not a product. Before committing, verify three things in your own checkout: that the ROM you intend to use is one you are legally entitled to run, how you will persist battery-backed SRAM through onBatteryRamWrite, and whether your target browsers can keep up with nes.frame() at 60 fps on the frames you care about.

Frequently asked questions

How do I install jsnes?

For Node.js or a bundler the README gives npm install jsnes. In a browser you can skip the package manager and load the minified build from unpkg with a script tag pointing at https://unpkg.com/jsnes@2/dist/jsnes.min.js.

Can jsnes run in Node.js as well as the browser?

Yes. The README states it is a library that works in both the browser and Node.js, and it shows a Node example that reads a ROM with fs.readFileSync using binary encoding and drives the NES class with nes.frame().

Does jsnes handle rendering and audio for me?

The Browser class does, according to the README, which says it handles canvas rendering, audio, keyboard input, gamepad input and frame timing automatically. If you use the NES class directly instead, you receive frames through onFrame and samples through onAudioSample and are responsible for timing and playback yourself.

How do I save progress with jsnes?

The README documents nes.toJSON() and nes.fromJSON(data) for serializing and restoring emulator state, and an onBatteryRamWrite callback that fires when battery-backed SRAM is written, which it says to use to persist save data. It does not document a storage backend, so where those bytes go is up to you.

What keyboard keys does jsnes use by default?

With jsnes.Browser, player 1 uses the arrow keys for the D-pad, X for A, Z or Y for B, Enter for Start and Right Ctrl for Select, with S and A bound to Turbo A and Turbo B. The README notes bindings can be customized at runtime through browser.keyboard and setKeys().

Official sources

  1. bfirsh/jsnes on GitHub
  2. License: Apache-2.0
  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/bfirsh-jsnes.svg)](https://hysenlabs.com/projects/bfirsh-jsnes)