eventemitter3, events, mitt, and pubsub-js are all libraries designed to handle event-driven architecture in JavaScript applications. They allow components to communicate without direct dependencies by subscribing to events and publishing data when changes occur. eventemitter3 and events follow the Node.js EventEmitter pattern, offering features like context binding and listener management. mitt is a minimalistic alternative focused on small bundle size, while pubsub-js implements a classic Publish-Subscribe pattern with token-based unsubscription. Choosing the right one depends on your need for Node.js compatibility, bundle size constraints, and specific API preferences like context binding or wildcard events.
When building interactive web applications, components often need to talk to each other without being tightly coupled. Event emitters and PubSub libraries solve this by allowing you to subscribe to messages and publish them when needed. While eventemitter3, events, mitt, and pubsub-js all handle this core task, they differ in API design, features, and environment optimization. Let's break down how they handle common engineering scenarios.
The first thing you notice is how you create an instance and send messages. eventemitter3 and events use a class-based approach where you instantiate an emitter object. mitt is similar but lighter. pubsub-js uses a static global object by default, removing the need to manage an instance.
eventemitter3 requires you to create an instance.
import EventEmitter from 'eventemitter3';
const emitter = new EventEmitter();
emitter.emit('update', { id: 1 });
events also uses a class instance, mirroring Node.js core.
import EventEmitter from 'events';
const emitter = new EventEmitter();
emitter.emit('update', { id: 1 });
mitt creates a lightweight instance.
import mitt from 'mitt';
const emitter = mitt();
emitter.emit('update', { id: 1 });
pubsub-js uses a static global API.
import PubSub from 'pubsub-js';
// No instance needed
PubSub.publish('update', { id: 1 });
Managing subscriptions is critical to prevent memory leaks. The way you remove listeners varies significantly. eventemitter3, events, and mitt require you to pass the same function reference to stop listening. pubsub-js returns a token when you subscribe, which you use to unsubscribe later.
eventemitter3 needs the original handler function.
const handler = (data) => console.log(data);
emitter.on('update', handler);
// Later
emitter.off('update', handler);
events works the same way, using off or removeListener.
const handler = (data) => console.log(data);
emitter.on('update', handler);
// Later
emitter.off('update', handler);
mitt also requires the handler reference.
const handler = (data) => console.log(data);
emitter.on('update', handler);
// Later
emitter.off('update', handler);
pubsub-js uses a subscription token.
const token = PubSub.subscribe('update', (msg, data) => console.log(data));
// Later
PubSub.unsubscribe(token);
In object-oriented code, you often need this to refer to your class instance inside a callback. eventemitter3 and events allow you to bind context directly in the subscription method. mitt and pubsub-js do not support this natively, requiring you to use .bind() or arrow functions manually.
eventemitter3 accepts a context argument.
class Component {
constructor() {
emitter.on('update', this.handleUpdate, this);
}
handleUpdate(data) { /* this refers to Component */ }
}
events does not have a built-in context argument in the standard API.
class Component {
constructor() {
// Must bind manually
emitter.on('update', this.handleUpdate.bind(this));
}
handleUpdate(data) { /* this refers to Component */ }
}
mitt has no context support.
class Component {
constructor() {
// Must bind manually
emitter.on('update', this.handleUpdate.bind(this));
}
handleUpdate(data) { /* this refers to Component */ }
}
pubsub-js has no context support.
class Component {
constructor() {
// Must bind manually
PubSub.subscribe('update', this.handleUpdate.bind(this));
}
handleUpdate(msg, data) { /* this refers to Component */ }
}
Sometimes you only want to listen for an event once, like initializing a resource. eventemitter3 and events provide a once method. mitt and pubsub-js do not have this built-in, so you must manually unsubscribe after the first call.
eventemitter3 has a dedicated once method.
emitter.once('ready', () => {
console.log('Ready only once');
});
events also includes once.
emitter.once('ready', () => {
console.log('Ready only once');
});
mitt requires manual removal.
const handler = () => {
console.log('Ready only once');
emitter.off('ready', handler);
};
emitter.on('ready', handler);
pubsub-js requires manual removal.
const token = PubSub.subscribe('ready', (msg) => {
console.log('Ready only once');
PubSub.unsubscribe(token);
});
For advanced debugging or global logging, you might want to listen to all events at once. eventemitter3 supports wildcard listeners using *. The other three libraries do not support this feature out of the box.
eventemitter3 supports wildcards.
emitter.on('*', (event, data) => {
console.log(`All events: ${event}`, data);
});
events does not support wildcards.
// No native wildcard support
// You would need to wrap the emitter or manually emit to a log topic
mitt does not support wildcards.
// No native wildcard support
// You would need to manually emit to a log topic
pubsub-js does not support wildcards.
// No native wildcard support
// You would need to manually publish to a log topic
| Feature | eventemitter3 | events | mitt | pubsub-js |
|---|---|---|---|---|
| API Style | Instance-based | Instance-based | Instance-based | Static Global |
| Unsubscribe | Pass handler | Pass handler | Pass handler | Use token |
| Context | Built-in support | Manual bind | Manual bind | Manual bind |
| Once | Built-in once | Built-in once | Manual | Manual |
| Wildcards | β Supported | β No | β No | β No |
| Primary Use | Frontend Apps | Isomorphic/Node | Tiny Bundles | Decoupled Systems |
eventemitter3 is the strongest all-rounder for frontend development. It gives you the powerful Node.js API you might already know, but it strips away the heavy Node.js dependencies. The built-in context binding and wildcard support save you from writing boilerplate code.
events is essential if you are writing a library that must run in Node.js and the browser without changes. It ensures compatibility with the Node.js ecosystem, but it can be heavier than needed for simple browser tasks.
mitt is the choice for performance-critical applications where bundle size is the main concern. It trades features for speed and size. If you just need to pass messages and don't need this binding or once, it is a great fit.
pubsub-js offers a different mental model with token-based unsubscription. This can be cleaner in some architectures where you don't want to keep references to your handler functions around just to clean them up later.
Final Thought: For most modern React or Vue applications, eventemitter3 offers the best balance of features and performance. If you are building a tiny widget, grab mitt. If you are polyfilling Node core, stick with events.
Choose eventemitter3 if you want the familiar Node.js EventEmitter API but optimized for the browser. It supports context binding, wildcards, and has a smaller footprint than the full events polyfill. It is ideal for React applications or complex frontend state management where you need robust listener control without Node.js dependencies.
Choose events if you are building isomorphic libraries that must run identically in Node.js and the browser without modification. It is the exact polyfill for the Node.js core module. Use this if you depend on other Node.js streams or modules that explicitly require the native EventEmitter interface.
Choose mitt if bundle size is your top priority and you need a simple global event bus. It lacks advanced features like context binding or once listeners, making it best for straightforward publish-subscribe needs in small projects or micro-frontends where every kilobyte counts.
Choose pubsub-js if you prefer a decoupled PubSub pattern where unsubscription is handled via tokens rather than passing the original callback. It is well-suited for legacy codebases or architectures where you want to avoid holding references to handler functions for cleanup.
EventEmitter3 is a high performance EventEmitter. It has been micro-optimized for various of code paths making this, one of, if not the fastest EventEmitter available for Node.js and browsers. The module is API compatible with the EventEmitter that ships by default with Node.js but there are some slight differences:
throw an error when you emit an error event and nobody is
listening.newListener and removeListener events have been removed as they
are useful only in some uncommon use-cases.setMaxListeners, getMaxListeners, prependListener and
prependOnceListener methods are not available.fn.bind.removeListener method removes all matching listeners, not only the
first.It's a drop in replacement for existing EventEmitters, but just faster. Free performance, who wouldn't want that? The EventEmitter is written in EcmaScript 3 so it will work in the oldest browsers and node versions that you need to support.
$ npm install --save eventemitter3
Recommended CDN:
https://unpkg.com/eventemitter3@latest/dist/eventemitter3.umd.min.js
After installation the only thing you need to do is require the module:
var EventEmitter = require('eventemitter3');
And you're ready to create your own EventEmitter instances. For the API documentation, please follow the official Node.js documentation:
http://nodejs.org/api/events.html
We've upgraded the API of the EventEmitter.on, EventEmitter.once and
EventEmitter.removeListener to accept an extra argument which is the context
or this value that should be set for the emitted events. This means you no
longer have the overhead of an event that required fn.bind in order to get a
custom this value.
var EE = new EventEmitter()
, context = { foo: 'bar' };
function emitted() {
console.log(this === context); // true
}
EE.once('event-name', emitted, context);
EE.on('another-event', emitted, context);
EE.removeListener('another-event', emitted, context);
To run tests run npm test. To run the benchmarks run npm run benchmark.
Tests and benchmarks are not included in the npm package. If you want to play
with them you have to clone the GitHub repository. Note that you will have to
run an additional npm i in the benchmarks folder before npm run benchmark.