Connect-assets and multiple dynos

16 Feb 2013

Node.js on Heroku

Heroku is one of the many PAAS that supports Node.js applications, and a pretty popular one. It only takes 5 minutes to setup your app on a free instance, maybe 5 more if you have a database or other addons.

However, for me the transition from a free 1-dyno instance to multiple paid dynos wasn't as smooth as I thought. Heroku's load balancer being non-sticky, the transition comes with a few new things to solve, for example:

Assets and multiple dynos

Serving assets? How is that relevant?

If you use connect-assets, JS and CSS gets processed & cached when it's requested for the first time. This is all good on 1 dyno, but with a paid instance it looks a little like this:

diagram

Pre-processing assets

The fix we came up with was to pre-process all the assets when the server starts up. This also has the advantage of pre-warming the cache, and making the first requests faster.

wrench.readdirRecursive 'views', (err, entries) ->
    return unless entries
    isFile = (f) -> fs.statSync("views/#{f}").isFile()
    entries.filter(isFile).forEach (file) ->
        app.render file, {}, (err) ->
            if err then console.log "[ERR] #{file} : #{err}"

This will process all JS and CSS files that are referenced by the views, and copy the output in builtAssets if the app is running in production mode (NODE_ENV=production).

View bindings?

But wait… I noticed errors being logged!

If your views have model bindings, they won't render without the appropriate variables being passed. The render function stops at the first error, and will miss any assets included further down. Bummer.

To fix this, we decided to parse the views for js() and css() tags, and call the tag helpers manually. This is now specific to EJS, but can be adapted to any other view. If anyone has an idea to make the view parser keep going after errors, we could keep the code above and it would be even better.

The code

Anyway, with the tag parsing, the final code looks something like this.

fs = require 'fs'
wrench = require 'wrench'

server.listen app.get('port'), () ->

    processFiles = (path, fn) ->
        isFile = (f) -> fs.statSync("./#{path}/#{f}").isFile()
        wrench.readdirRecursive path, (err, entries) ->
            if entries
                entries.filter(isFile).forEach(fn)

    processEjsTags = (content) ->
        css = /<%- css\('(.*?)'\) %>/g
        js = /<%- js\('(.*?)'\) %>/g
        while matches = css.exec(content)
            connectAssets.instance.options.helperContext.css matches[1]
        while matches = js.exec(content)
            connectAssets.instance.options.helperContext.js matches[1]

    console.log 'Pre-processing views and included assets'
    processFiles 'views', (file) ->
        fs.readFile "views/#{file}", (err, content) ->
            processEjsTags(content) unless err

CDN maybe?

Of course, serving static assets straight from the dynos can be a waste of resources. They would be perfectly happy on a CDN. I guess there are 2 ways of doing this:

1: Physically copy the assets to a CDN

This option is the most complex, but sometimes the only choice… for example if your web server is read only, or if you want to keep the assets served from a separate sub-domain (ex: static.mywebsite.com).

2: Front the entire website with a CDN

This is my preferred option, when available.

Because of the expiry headers, the origin should only be hit once per asset - and because of our pre processing, it doesn't matter which dyno responds to the request! In this case no need for any extra steps, and all asset references can stay as relative paths!

Comments