One of the main draws to Node.js is its ability to respond efficiently to a large number of requests, but users of your app don’t care how much you’re squeezing out of a single core. They want stuff to show up. Now.
BigPipe
A few years ago, Facebook introduced a new technique it started using on its homepage called BigPipe. It combines the benefits of making fewer HTTP calls with being quick to the first paint. The benefits can be substantial, but BigPipe uses some unconventional approaches to dividing up CSS and markup that could be quite a departure from the common frameworks and architecture you’re probably using to build your apps. Unless you’ve achieved Facebook’s scale, the tradeoffs might not be worth it.
Half-measures with benefits
You can still get a lot of mileage out of a few simple tweaks that won’t require many changes to your standard setup. When it comes to your users’ perception of speed, nothing causes more harm than a blank white page and a busy browser icon showing no progress. Sending the first part of the response before it’s finished can have a big impact on the perceived speed of your app.
The technique is pretty simple: we’ll be using a feature of HTTP 1.1 called Chunked transfer encoding. With a standard Express.js response, you normally use res.send
or res.render
. These handy shortcuts write best-guess headers and send of the response with a single command.
The downside is that it doesn’t send anything before that. You might be holding up the whole page while you wait for one little section that’s pulling from the Twitter API.
Instead, we’ll break things up and write a few more lines of code. Let’s start with the HTML head
, our CSS and JS tags, and the opening tag of the body
element:
app.get('/test', function(req, res){
res.write('<head>
');
// res.write(... css and js tags)
res.write('');
});
Express automatically sets the HTTP ‘Transfer-Encoding’ header to ‘chunked.’ It then sends the HTML that we’ve created here. Once the browser sees the JS and CSS tags in that head
element, it can start downloading those assets while your app is finishing the body of its response.
After we’ve fetched and rendered the necessary bits for the rest of our page (database calls, external APIs, and so forth), we write those out, and then close the body
and HTML tags, and finish the response by using res.end
.
app.get('/test', function(req, res){
res.write('<head>
');
// res.write(... css and js tags)
res.write('');
// fetch data and write main body of page
// with more res.write('...') calls
res.end('</html>');
});
Streams
Another option is to use Node Streams and pipe
to the response. This requires a readable stream. For example, using the ever popular Request module, you can pipe
the incoming response from an external source straight to a response
that your app sends to the browser, like this:
app.get('/test', function(req, res){
req.pipe(request('http://mysite.com/doodle.png')).pipe(resp);
});
Beyond Express
I recently wrote an intro to Koa. Unlike the res.*
syntax of Express, Koa generally uses this.body = 'HTML HERE'
. There’s an example in the Koa repo that shows one way to stream an HTML page. If you’re not ready to live on the bleeding edge of node v0.11.x, take a look at then-jade; you’ll get some of the cool syntax of ES6 generators and the benefits of streaming chunks of HTML, all with the cozy v0.10.x environment and the awesomeness of Jade.
No chunks please
In modern browsers, the XHR object’s readyState
can have a value of 'LOADING'
(3). That allows them to handle the chunked encoding that we’re sending. However, if you’re still stuck supporting IE8 (or lower) you can use a clever addition of socket.io, nicely packaged up as Streamable. If that seems like too much of a hack, you can use Express’ content-negotiation syntax to send the response the “old fashioned” way when the request is via XHR:
app.get('/test', function(req, res){
if (req.xhr) {
// fetch data, pass to template
// response is sent in one command
res.render(data);
} else {
res.write('<head>
');
// res.write(... css and js tags)
res.write('');
// fetch data and write main body of page
// with more res.write('...') calls
res.end('</html>');
});
To go the extra mile, you might try some client-side detection for support of readyState === 3
, and pass an extra parameter in your XHR call to let the server know it can send a chunked response. But some browsers that support chunked encoding in XHR still won’t let you access the data that’s received until the request is finished, which eliminates the primary benefit.
Yak Shaving
There’s an obvious tradeoff here; we’re essentially stripping out the shortcuts and common lingo that Express gives us and reverting back to core Node response syntax and/or using obscure libraries. If your page is really simple and quick to render, the extra syntax might not be worth it.
What’s next?
- Ready to develop APIs in Node.js and get them connected to your data? We’ve made it easy to get started with LoopBack either locally or on your favorite cloud, with a simple npm install.
- What’s in the upcoming Node v0.12 release? Big performance optimizations, read Ben Noordhuis’ blog to learn more.
- Need cluster management, performance monitoring and DevOps for Node.js? Check out StrongOps!