published by | Adam Stepinski |
---|---|
in blog | Instawork Engineering |
original entry | Real-time Web Apps with Zero Lines of JS |
In a previous post, I described how Instawork doubled our web development productivity by abandoning React and embracing server-rendered pages enhanced with Intercooler.js. A standard React/Redux setup requires a lot of duplicated work; logic has to be written once on the server (as the source of truth), and a second time on the client (for local state management). By doing all UI rendering in our Django codebase, we eliminate the need to write any custom app logic in JS. Of course, pure server-rendering doesn’t deliver the smooth experience users expect from web apps. That’s where Intercooler.js comes in: by adding a few HTML attributes to our elements, we can support a wide range of interactions by swapping content on the page with AJAX. All rendering is still done server-side, but pages can come alive with interactions like infinite scrolling, tab switching, dynamic multi-step forms, etc.
I always assumed that Intercooler’s swapping mechanism wouldn’t work for some of the more complex UI interactions we planned to add to our app. In particular, I worried how we’d handle real-time features in our product, like chats or streaming notifications. Would we need to give up on the zero-JS dream to implement these features?
I’m happy to report that my worries were unfounded. We successfully shipped realtime features in our web app without writing a single line of JavaScript! The rest of the post will explain how we implemented a real-time status feature using Django, Intercooler, and Server-sent events with Mercure.
Server-sent events (SSE) is a widely-supported web standard that allows browsers to push new data to a web page at any time. Unlike WebSocket, SSE is a simple uni-directional protocol implemented using HTTP on a long-lived connection. Web pages can open a connection to get named events (and event data) using the EventSource interface in JavaScript. Responding to events is done by registering callbacks.
Intercooler has built-in support for SSE that manages the EventSource interface automatically. The syntax looks like this:
<div ic-sse-src=”/dashboard/events”>
<div ic-trigger-on=”sse:status-updated-123" ic-src=”/status/123">
Not arrived
</div>
</div>
This is just regular HTML with two special attributes:
With these two attributes, our frontend is fully capable of real-time updates! When the frontend receives the event status-updated-123, Intercooler will request content from /status/123. The response will be an HTML fragment containing the new response status. Intercooler then swaps out the status div with the new fragment. And that’s it, the UI updates in real-time in response to events, and we didn’t need to write any JavaScript to do it!
Of course, what I described is the easy part. Behind the scenes, we need to implement the long-lived SSE endpoint, as well as a mechanism to push relevant events to all clients. Web frameworks like Django don’t handle long-lived connections very well, so we looked for other options. The Django Channels project extends Django to handle asynchronous communication, but it doesn’t support SSE out of the box. We wanted something that supported SSE in a language & framework-agnostic way.
Enter Mercure. Mercure is an open-source project that provides scalable SSE communication using a centralized Hub server
We considered a few options and services for handling SSE connections, but ultimately decided on Mercure for a few reasons. As mentioned above, Mercure is built around SSE, so its language and framework agnostic. Any system that can encode JWT tokens and make HTTP requests can use Mercure. We also appreciate that Mercure is a small micro-service that runs alongside our servers, rather than as a proxy in front of our servers (the approach used by Pushpin). This means there’s less risk to our site if Mercure goes down: we lose real-time features, but the main site will continue to function. Finally, we’ve found it to be very stable and scalable. In our load tests Mercure didn’t break a sweat, even when running at 10x the loads we expect.
Our Django integration with Mercure took the form of a helper function for sending events, and a view mixin. The view mixin does two things:
I’ve shared a Gist with a complete example of our helpers, mixins, views and templates.
It’s easy to think that features like real-time UIs require a heavy client-side framework. But thanks to Intercooler’s built-in support for SSE, and the simplicity of setting up and integrating Mercure, we’ve been able to stick to our “no JS” philosophy while delivering the features our users expect. We continue to be surprised by the capabilities of Django + Intercooler + Mercure, and adding new realtime features is now a snap.
Have you added real-time updating to your web app? Are you thinking about adding it in the future? We’d love to hear about your experience and solutions, or provide more insights into Django, Intercooler, or Mercure.
Real-time Web Apps with Zero Lines of JS was originally published in Instawork Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.