Moving from Socket.IO to µWebSockets.js

You don’t need decade-old hacks & quirks to build real-time apps.

µWebSockets.js is an open source WebSocket server developed since early 2016. It implements standard WebSockets — something today’s web browsers all support without polyfill or helpers. They are standard, simply put. Since inception, µWebSockets.js has established the reputation of being very optimized and fast, lean and stable. Now I want to show you how simple it can be to use!

As a reference we’ll consider the very popular Socket.IO. We’ll be looking at the basics of a collaborative drawing app, comparing the two different technologies in terms of simplicity and productivity. We want to make an app where many participants can draw on a shared canvas in real-time.

Developed for the early era when WebSockets were still an unfinished experimentation, Socket.IO is known to be slow, bloated and using excessive amounts of memory. It has a long history of relying on various hacks and quirks, hooking into crazy things like Adobe Flash sockets, HTTP timeout hacks and polling. None of this makes sense today, it adds bloat and unnecessary complexity.

But everyone is using Socket.IO so it can’t be that bad, right?

Socket.IO being a non-standard protocol, requires one particular client library to work — it is not compatible with and cannot communicate with standard WebSockets. If you expose your API over Socket.IO, you’re stuck with that. Sooner than later, especially if your business grows rapidly, you’ll need a migration plan to move away from the protocol lock-in and terrible performance that comes with Socket.IO.

I’ve seen many cases where companies get locked up with Socket.IO and desperately need a way out. Often times at a significant cost due to already public APIs being in use, needing a slow migration plan.

It would be wise not to end up like this to begin with, and instead aim for a standard where you may easily swap between implementations, if needed. Not only server side but client as well.

Client side

Let’s compare Socket.IO and standard WebSockets on the client side. For Socket.IO you obviously need the socket.io-client JavaScript library loaded by the browser. Here is a very simple example:

// establish a connection to the server
const socket = io('https://your_socketio_server.com/');
socket.on('connect', () => {
// now we are connected
socket.send('some text') // send some text to server
});
socket.on('message', (message) => {
// here we got something sent from the server
});

Simple! It’s really quite small and simple to communicate with the server using Socket.IO. However, this is roughly the standard equivalent:

// establish a connection to the server
const socket = new WebSocket('wss://your_websocket_server.com/');
socket.onopen = () => {
// now we are connected
socket.send('some text') // send some text to server
};
socket.onmessage = (message) => {
// here we got something sent from the server
};

Just as simple, right? It’s essentially the same using standard WebSockets, certainly the same overall concept. Small differences here and there, sure, but nothing you can’t easily tackle in an afternoon, especially given excellent documentation.

Standard WebSockets don’t need any client libraries and can perform up to 20x that of Socket.IO, with a far lower latency. Standard WebSockets are implemented as part of the web browser, deep down in the C++ guts. Google recently made a major performance leap for WebSockets in Chrome 78, something not possible with Socket.IO. Leave it to Google.

Server side

We are building a collaborative drawing app, a shared canvas where one or many participants may draw using the mouse cursor. What’s drawn is to be shared among all participants in real-time, over the network.

We can send one-to-one in similar ways from the server to client:

socket.send('some message'); // sends a message to a client

However, one drawing canvas can instead be seen as a “topic”, or sometimes called a “room”. Whatever happens under that “topic” is to be shared among all subscribers, all clients.

Socket.IO has support for this idea, often called pub/sub (“publish/subscribe”). We can subscribe a set of clients to one or many “rooms”. Whenever something is published to that “room”, all subscribers receive the message. This simplifies the app and makes it easier to handle groups of connections.

socket.join('drawing/canvas1'); // add this server-side socket to room "drawing/canvas1"

When a server side socket joins a room, it can later be addressed as part of the group “drawing/canvas1” when sending one-to-many:

io.in('drawing/canvas1').send('some appropriate message here'); // sends drawing message to all server-side sockets in "drawing/canvas1"

That’s the essentials of Socket.IO, skipping a few details. So let’s write the full server side example:

// listen to port 3000 server side, using Socket.IO
const io = require('socket.io')(3000);
// handle new connections
io.on('connection', (socket) => {
// handle messages from client
socket.on('message', (message) => {
// parse JSON and perform the action
let json = JSON.parse(message);
switch (json.action) {
case 'join': {
// subscribe to messages in said drawing room
socket.join(json.room);
break;
}
case 'draw': {
// draw something in drawing room
io.in(json.room).send(json.message);
break;
}
case 'leave': {
// unsubscribe from the said drawing room
socket.leave(json.room);
break;
}
}
});
});

Again, super simple! You see we added the feature to leave a room as well. Above server side code would allow clients to join a drawing room, draw something, and finally leave (or any combination of).

Standard WebSockets are implemented server side in literally hundreds and hundreds of different ways, driving competition and survival-of-the-fittest evolution. You may use any implementation, but obviously this post focuses on the very efficient µWebSockets.js.

µWebSockets.js can be seen as a mix between Express.js and Socket.IO, you define “apps” which hold URL routes. Standard WebSockets are added per route. Here is a complete counter example to the previous Socket.IO one:

// npm install uNetworking/uWebSockets.js#v16.2.0
const uWS = require('uWebSockets.js');
// uWebSockets.js is binary by default
const { StringDecoder } = require('string_decoder');
const decoder = new StringDecoder('utf8');
// an "app" is much like Express.js apps with URL routes,
// here we handle WebSocket traffic on the wildcard "/*" route
const app = uWS.App().ws('/*', {
// handle messages from client
message: (socket, message, isBinary) => {

// parse JSON and perform the action
let json = JSON.parse(decoder.write(Buffer.from(message)));
switch (json.action) {
case 'join': {
// subscribe to messages in said drawing room
socket.subscribe(json.room);
break;
}
case 'draw': {
// draw something in drawing room
app.publish(json.room, json.message);
break;
}
case 'leave': {
// unsubscribe from the said drawing room
socket.unsubscribe(json.room);
break;
}
}
}
});
// finally listen using the app on port 3000
app.listen(3000, (listenSocket) => {
if (listenSocket) {
console.log('Listening to port 3000');
}
});

Conceptually the same, µWebSockets.js gives you socket.subscribe in place of socket.join, socket.unsubscribe in place of socket.leave and app.publish in place of io.in(“”).send — all operating on entirely standard WebSockets.

Whenever a client tells the server about a drawing action, that action message is sent to all clients subscribed to the same canvas, “room”.

Full source code (wip)

With these functions in mind, it is possible to build a more complete example. Full source is available in a separate Git repo here. The repo holds versions made with both Socket.IO and µWebSockets.js to showcase the similarities.

Conclusion

Swapping from the hack that Socket.IO is, over to standardized counterparts can be easier than first imagined. There really isn’t any one killer-feature of Socket.IO that makes it impossible to do without.

Most of what you need is a way to group sockets, send one-to-one and track open/close events properly. Authentication can be the same, relying on the same as for HTTP. Technically a WebSocket route is the same as any HTTP one.

Of course, you’ll have to put in the hours, but once you have there is no lock-in holding you to any one implementation. Most of what Socket.IO gives you is available in many different implementations. But of course, the reasoning behind µWebSockets.js in particular is to be very competitive, doing anything from 5x, 10x, 100x as compared to Socket.IO (again, check the benchmarks).

So if you’re stuck with poor performance due to Socket.IO or anything similarly poor such as SocketCluster, don’t hesitate starting the move — it can be done and there is help to get. If you’re starting something new, please do the right thing.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store