Handling real-time data on the web

jQuery Conference San Diego 2014

Justin Ribeiro
[email protected]


@justinribeiro +Justin Ribeiro justinribeiro

Slides: http://goo.gl/tzXnHA

Demo #1: the bottom left corner

If the wifi is working data is a movin'

  • My Google Glass timer application pushing data to a broker
  • The MQTT Broker is hooked to a web socket server instance
  • We're listening in an updating things as fast as we can

What are we talking about today

  1. Why?
  2. Server Sent Events and Websockets
  3. #perfmatters: faster I say
  4. $.Callback()

But Justin what about...

Why SSE's and WebSockets?

"I've got XHR and it's awesome."

  • XHR is great for the transactional
  • SSE is great for text streaming to client
  • WebSockets are great for bi-directional low-latency text/binary

They're complimentary

It depends on your use case

No magic bullets for sale

  • The propagation latency of the data packets is the same
  • SSE and WebSockets only remove the message queuing latency
  • You can't beat the speed of light
  • Good Read: High Performance Browser Networking

Real time looks easy.

Server-Sent Event Wire Up


var myServerSentEvent = new EventSource("something.php");

myServerSentEvent.onopen = function () {
    // I'm open for business
};

myServerSentEvent.onmessage = function(event){
  // Steam me some text
};

myServerSentEvent.onerror = function (event) {
  // Call John: ask him to press reset button on rack 2
};
          

We gotta talk about IE / Android

Hey, you said you weren't going to talk about them!

  • Sure, the old IE 6/7/8 doesn't implement EventSource
  • Problem is, neither do the newer IE 9/10/11
  • Only the most recent version of Android (4.4 supports it)
  • See: Modernizr Wiki for polyfill options

SSE: the good parts

  • Can only be UTF-8 encoded text: apply app logic and go
  • event-steam = HTTP streaming = gzip it!
  • Memory-efficent: once a message is processed, it's gone.
  • Custom events

myServerSentEvent.addEventListener("someEventTag", function(event){...});
            

SSE: the not so good parts

  • Can only be UTF-8 encoded text: no binary (hint: that's what WebSockets are for)
  • Lucy, Ethel and the choclate problem: no sending back
  • The problem with the polyfill options: XHR transport overhead

WebSocket Wire Up


var myWebSocket = new WebSocket("ws://somewhere");
                
myWebSocket.onopen = function(event){
  // I'm open for business
};

myWebSocket.onclose = function(event){
  // what? I closed? What happened?
};

myWebSocket.onmessage = function(event){
  // ahhh some data
  // I think it's a cat
};

myWebSocket.onerror = function(event){
  // no one worries about icebergs
};
          

The devil is in the details

  • Are we text or binary?
  • Did we implement the compression or did the server on the frame?
  • Are we talking in a subprotocal?
  • Wait, what's in the payload?

Frame up that frame, Mr. McFrame


myWebSocket.onmessage = function(event){
  if(event.data instanceof Blob) {
    // an immutable picture of my kids
  } else {
    // some text describing my kids
  }
};
          

That Socket needs to take a hint

I don't have an immutable, just make it an arraybuffer


myWebSocket.binarytype = "arraybuffer";

myWebSocket.onmessage = function(event){
  if(event.data instanceof ArrayBuffer) {
    // let's split this buffer up
  } else {
    // some text describing my kids
  }
};
          

Where's my compression?

  • Could use Sec-WebSocket-Extensions: permessage-deflate if browser/server support
  • Could be custom compressing your data: offsets of things

myWebSocket.onmessage = function(event){
  if(event.data instanceof ArrayBuffer) {
  
    // known: byte offsets and big-endian
    var dataView = new DataView(event.data);
    var sensorId = dataView.getUint8(0);
    var temp     = dataView.getUint16(1); 
    var lux      = dataView.getUint8(3);
  
  }
};
          

The websocket rabbit hole

  • Lots of options and implementation choices to be made
  • The flexibility results in application complexity

Communication required.

Twins playing with DevTools values. :-)

Pssf, I just need JSON send it now

#perfmatters: Your bandwidth

Demo #02: Sensor A1 Reporting ° lx


{
  "topic": "office/sensors/a1/out",
  "message": {
    "degF": 71.9,
    "lux": 222.8
  }
}
          

93 bytes. A tiny perfect JSON world.

But it's not a perfect world

It's always easy when there's only one.

  • JSON can be huge
  • Resolution can be fast (~20 a second)
  • You can have lots of them!

(93 bytes * 20/second) = 1.8kb.

100 sensors = 180kb a second!

Demo #03 - Sensors

A0s0: ° A0s1: °
A2s0: ° A2s1: °
A1: ° A3: ° A4: ° A5: °
Rep 1 Dual: X1: ° X2:° P1:°
Elapsed: Remaining: Complete Lines: Total Lines:
Rep 2 Sail: X1:°
Elapsed: Remaining: Complete Lines: Total Lines:

Everything is amplified

#perfmatters: DOM DOM DOM

The magical Checklist-O-DOM-update

  1. Does this data need to update the DOM?
  2. Can I batch it with other updates?
  3. Should I be holding that DOM element for later?
  4. What's my reflow look like?

If you can, cache your update target

Bad selectors will ruin you: be specific, cache it, use it


// We'll keep it around for a while  
if (!JDR.sensors.cache[topic]) {
  var eleName = topic.replace(/\//g, "-");
  JDR.sensors.cache[topic] = $("#" + eleName);
  JDR.sensors.cache[topic]['o1'] = JDR.sensors.cache[topic].find('.o1');
  JDR.sensors.cache[topic]['o2'] = JDR.sensors.cache[topic].find('.o2');
}
          

This is not a new concept. You should be doing this even if you're not using real time data.

Update a DOM element

When in need of pure speed, textContent can be your friend.


// We'll keep it around for a while  
var myElementToUpdate = $("#glass-timer");

//... WebSocket / EventSource Wire Up

// Update that text!
myElementToUpdate[0].textContent = data.message;
          

Use Layout Boundries

You're updating the DOM, which means it's going to recalc

Limit the scope as much as you can.


/* Layout Boundary for updating Glass stat pack */
#statpack {
  left: 10px;
  bottom: 10px;
  width: 250px;
  height: 100px;
  overflow: hidden;
  position: absolute;
}
          

Great tool: paullewis/Boundarizr

Great read: Introducing 'layout boundaries'

Just the facts ma'am

When #perfmatters you can't be afraid to diverge as needed

By the numbers: JS Perf Test - $.html(), $.text(), textContent

Glass stat pack: from 2.1ms to 0.5ms

I'm not seeing a lot of jQuery here.


Let's add some.

$.Callbacks() = @#$%^ awesome

Internally used by $.ajax() and $.Deferred() components


Humm, websocket/SSE with $.Deferred() support sounds nice.


Let's do that.

jquery.websocket.callback.js

  • Wraps the WebSocket API and uses $.Callbacks()
  • Uses a pubsub / observer pattern topic system

Wire Up

Pretty straight forward


var myBrokerEcho = $.websocket({ 
    url: "ws://echo.websocket.org/"
});

myBrokerEcho.topic( "websocket.onOpen" ).subscribe( myOpenMethod );

myBrokerEcho.topic( "websocket.onMessage" ).subscribe( myMessageMethod );
          

Internally what's happening?

Pretty straight forward


// ...
ws.connection.onopen = function(event){
    ws.topic( "websocket.onOpen" ).publish( event.data );
};

ws.connection.onclose = function(event){
    ws.topic( "websocket.onClose" ).publish( event.data );
};

ws.connection.onmessage = function(event){
  // ... doing some other stuff
  ws.topic( "websocket.onMessage" ).publish( data );
};

ws.topic( "websocket.send" ).subscribe( this.send );
// ...
          

Wait what's with the send?

Internally, it's subscribing to a topic and using it's own method to send back to the open socket.


ws.topic( "websocket.send" ).subscribe( this.send );
          

How do we send to it?

Well this should work right?

Let's send some data to the socket!


var myBrokerEcho = $.websocket({ 
    url: "ws://echo.websocket.org/"
});

// Oh it's angry
myBrokerEcho.topic( "websocket.send" ).publish("Send some data");
          

It's not happy, how do we fix this?

With a $.Deferred() of course

You can't send something to the socket before the open is finished.


myBrokerEcho.topic( "websocket.onOpen" ).subscribe( myOpenMethod );

var dfd = $.Deferred();
var topic = myBrokerEcho.topic( "websocket.send" );
dfd.done( topic.publish );

function myOpenMethod( value ) {
  dfd.resolve( "I'm resolved!" );
}
          

So we create a $.Deferred() and resolve it after open

What about SSE?

I've got you covered: jquery.eventsource.callback.js


var myScript = $.eventsource({ 
    url: "sse.php"
});

myScript.topic( "eventsource.onMessage" ).subscribe( onMessage );

function onMessage( value ) {
  console.log("Incoming Message!", value);
}
          

Could use some event channel support

Early and young

  • Both libs are pretty early on in development.
  • Your mileage may vary.

jQuery Plugin Creation Totals

Me: 2


Ben Alman (@cowboy): 7634*

* Approximate: haven't checked his repo today


I am not catching up.

Wrapping up

  • Real time data is hard.
  • Look beyond your code window.
  • $.Deferred() + WebSockets = happiness

Source and what not

The End

Thank you!

@justinribeiro +Justin Ribeiro justinribeiro