Building Geckoboard Lite
How we use server-side rendering to support older and low-powered devices.
Imagine this: You’ve just signed your team up for a free trial of Geckoboard. You’ve connected your Zendesk account, pulled in a handful of key metrics, and now have a pretty handsome looking dashboard. 🤵📈
All that remains is to see how it looks on the big screen.
“Easy!” you exclaim, as luck would have it there’s a Smart TV in the meeting room just next door. You wait a few minutes for the overrunning meeting to finish and vacate the room. You grab the remote, open the built-in web browser and pair the TV with Geckoboard.
It takes a few moments but finally your dashboard appears. Huzzah! 🎉
Except it looks a little… off. You can hardly read most of the numbers and some even seem to be missing entirely.
Unfortunately a number of our customers and trialists have had an experience like this. It’s demotivating for them and frankly embarrassing for us. 🤦
Geckoboard is made of modern web technology. We rely on features that are well-supported by evergreen browsers such as Chrome and Firefox, but are sadly missing on the often stagnant browsers built into Smart TVs and set-top boxes.
Here’s a photo of a dashboard running on a relatively recent Smart TV in our office. You can see that the chart axis labels (circled left) and entire Geck-O-Meter (circled right) are missing.
Inspiration
In 2015 Apple released the forth version of the Apple TV, along with tvOS and a third-party app store. This was really exciting for me as it seemed like a great platform for our product!
Unfortunately tvOS has no web browser and no UIWebView
component as found on iOS, which means no web applications. As a small team with a tight focus, we aren’t currently able to dedicate the time and resources necessary to maintaining separate native apps on each platform.
In my innovation time I began investigating the possibility of offloading widget rendering to the backend as a means of serving platforms with poor or no support for web technology. I built a small prototype using PhantomJS, which was promising but had a number of quirks making it unsuitable for real-world use.
When Chrome 59 shipped with the ability to run in headless environments I decided to revisit this idea and build another prototype. This time the results were compelling enough that we decided to dedicate the next 6-week cycle to building something for real.
Going Decaffeinated
We formed a small team (1 frontend and 1 backend engineer) and built an entirely new version of the Geckoboard frontend optimised for older and low-powered devices.
We’re serious about both coffee and puns here at Geckoboard so it seemed only right to name this app “Decaf”. ☕
With some user agent sniffing, we’re able to serve Decaf to less-capable browsers.
Decaf is a dramatically smaller JavaScript bundle as it doesn’t include any of the rendering logic or React components from Polecat (the main Geckoboard application) which means that it loads quicker on resource-constrained devices.
Widgets in Decaf are displayed using simple <img>
tags which are well-supported on almost every browser.
Exposure
The other half of Geckoboard Lite is a new backend microservice responsible for rendering widgets as images using a Headless Chrome Browser.
This service is named Exposure after the photography term (did I mention that we like puns?) 📷.
Exposure is written in Go and exposes (that pun really wasn’t intended) an HTTP interface used by Decaf.
When Decaf receives data for a widget, it makes a POST request to the /render
endpoint. If no image is found in the cache for the given payload, Exposure will communicate with Chrome using the DevTools Protocol (and Mathias Fredriksson’s excellent Go bindings) to render the React component and take a screenshot of it.
The DevTools Protocol is a largely asynchronous and event-driven API. Go’s concurrency primitives (channels and goroutines) made working with it a breeze.
Interaction with Chrome is abstracted away into a ChromeRenderer
type, which exposes a Render()
method. This method will start two goroutines: one to drive the rendering process (navigating to the internal page, evaluating JavaScript, taking the screenshot) and another to respond to events such as runtime exceptions and to halt the operation if something goes wrong.
In the diagram below, light blue boxes represent RPC calls to Chrome and yellow boxes represent events received from Chrome.
Each rendering operation happens in its own browser tab, which uses memory, CPU and other resources, so it’s important that we don’t allow an unbounded number of concurrent operations to exhaust the server’s available resources.
To this end we implemented a pooling mechanism. Rendering operations are written to a jobs channel and picked off by a fixed-size pool of goroutines (50 per instance at the time of writing). Each “worker” goroutine will forward operations on to the ChromeRenderer
.
As both ChromeRenderer
and Pool
implement the same Renderer
interface, this pooling behaviour is transparent to the caller.
type Renderer interface {
Render(context.Context, Payload) ([]byte, error)
}
Once the widget has been rendered to an image we cache the PNG bytes in Redis.
We generate the cache key from the SHA256 hash of the payload, along with the git SHA of the running Exposure process and the asset URLs. Including this extra information in the cache key ensures that the cache is busted whenever Exposure or the main frontend app is deployed.
Rather than responding to the initial request with the image directly, we instruct Decaf to make a GET request to retrieve it. This is so that we can use the Cache-Control
header to ensure the browser (and any intermediary proxies and CDNs) will cache the image on their end too, preventing wasteful use of bandwidth.
Results
We’re thrilled with how it turned out! In 6 weeks we were able to design, build, and ship a solution to a problem that has long plagued us.
It was a really fun project to work on. I’m excited about the other possibilities that Exposure opens up to us, especially with our Innovation Week fast approaching!
Finally, here’s an animated gif demonstrating what happens to our previously broken dashboard when enabling Geckoboard Lite. It’s surprisingly fast! 🐎