Improving Perceived Performance with Talisman for Node.js
It’s weird that so many template systems designed for the web take the time to calculate what the final byte of a response should be before transmitting the first byte.
When an application receives a request for new content, we might query our database, make our own HTTP requests, perform some computation, and then slowly assemble the string of HTML content we will send back in the response.
While we’re doing this, our user is left staring at a blank white screen, wondering if the page they want to see is ever going to come back. And when it finally does, what’s the first thing we send?
Not only is this the first thing we send, but it was always going to be the first thing we send. So why did we wait until we had finished assembling the content for the entire page before we sent it?
We could have worked to get something up in front of our user quickly, so they knew the page they had asked for was loading. And even if the total load time of the page wound up being the exactly the same, the fact that content started to appear sooner will have improved the perception of performance for that user.
We have been able to stream data like this for a long time (I’m looking at you, flush), but it’s never been a particularly easy thing to do. And as the web has matured, and developers have started to leverage frameworks and template libraries, it has only gotten harder.
Many of the template systems we have available right now insist upon computing the value of the entire response before sending any part of it.
Talisman: streaming templates for Node.js
This was the problem which started the development of what became Talisman, a streaming template system and language for Node. We wanted to get something up and in front of our users as quickly as possible, and we couldn’t find any technology we liked which could do it.
Talisman is designed to send as much content to the user as it can, as quickly as it can. It achieves this by returning a Readable stream, which the developer can pipe directly into the http.ServerResponse object in Node or Express.
When Talisman has new content ready to go, it can push it down the stream, which will deliver it to the user right away. If it encounters a Promise, it will wait for it to resolve, before pushing the resolved value to the output stream. If it encounters another Readable stream, it will pipe its contents into the output stream.
When it runs out of content, the stream is closed.
Talisman templates have a syntax superficially similar to Mustache, with variable placeholders inserted into your HTML, enclosed by double curly braces.
Loading a template file like the one above into Talisman will immediately output everything up to the end of the opening title tag — since this is static text which does not change, and does not depend on the result of any IO.
It would then look up in its variable table to see if a variable named pageTitle is available. If the value of that variable is a string, or some other scalar, it is sent to the output stream immediately… but if the value is a Promise then Talisman will wait for that Promise it resolve first.
Talisman can also handle variable values which are functions, streams, or a function which returns a string (or a stream), or a Promise which resolves to a function which returns a stream, or a Promise which resolves to a function, which returns a Promise which resolves to a string (or a stream), and on, and on...
Block and Iteration
Talisman also allows you to define a Block, which is a section of related content. Blocks can be used as an if by conditionally removing it, or as a loop by defining a section of content which should be repeated, driven by some iterator.
Talisman will repeat the section containing the <li> as many times as the assigned iterator dictates. Currently, Talisman supports two types of iterator, simple arrays and object-mode streams. This latter feature allows us to output rows as-and-when we receive them from the database. We don’t need to wait for an entire recordset to be available!
Talisman seems to improve the load time of pages, as well as improving the perceived performance of an application for a user.
In a simple example, we ported the homepage of one project from PHP to Node+Express. The database used was CouchDB.
In the PHP version of our application, the time-to-first-byte of the page was ~900ms. Porting this to Node+Express brought this down to ~200ms. Adding Talisman lowered the TTFB even further — to ~15ms!
Overall, the time to DOMContentLoaded was comparable between the two Node tests, but window.onload fired ~150ms sooner, because we had started to download assets sooner.
Like everything else in software, Talisman has some trade-offs.
Because content has already been sent to the client, including an HTTP status code of 200, we cannot then change that to a 404 or 500 if our database returns no rows, or something goes awry.
We must also somehow recover from the error mid-page, accepting the content we have already output will be part of that.
Where to get it?
$ npm install talismanjs
Comments, bug reports, and pull requests are most welcome.