Enhance page speed for your Middleman websites on Heroku

Middleman is a great tool for developing static websites with a bunch of tools like template engine, SASS or CoffeeScript. Jordan Elver has a great post about how to deploy a Middleman to Heroku as a Rack app serving your static files.

If you want to follow the suggestions from Google's PageSpeed Insights to increase your website performance, you'll need some additional steps.

The most basic part will be minifying your CSS and JavaScript. You can even minify your HTML to make it smaller.

There are some more things you can do:

Optimize images

The Middleman documentation suggest using middleman-imageoptim to optimize your images on build, but my previous experience was pretty painful–especially when you have many images in your website. It can make your every deploy really long, since it'll try to optimize every image after build.

Screenshot for ImageOptim.app

If you're using a Mac, I'll suggest to do manual image optimization with the ImageOptim app. You just have to do it once, and drap your new images to the app before adding to your repository. All the images in your site are now optimized forever, so you don't have to wait for your images being re-optimized every time you deploy.

Enable gzip and leverage caching, the easy way

The easiest way to turn both things on is using Rack::Deflater with Rack::StaticCache. The later comes from the rack-contrib project, which also hosts the Rack::TryStatic middleware.

Just add these lines to your config.ru:

require 'rack/contrib/static_cache'

use Rack::Deflater
use Rack::StaticCache, :urls => ['/images', '/stylesheets', '/javascripts', '/fonts'],
                       :root => "build"

And it'll work perfectly for you.

Serve pre-compiled gzip assets

The Rack::Deflater middleware works great on providing gzipped files, but it's a real-time compression for every single request. It can cost extra CPU resource on your server.

Middleman has built-in support to generate pre-gzipped assets on build, so it'll be great if you can use them directly.

First, you'll need to turn this on in your config.rb:

configure :build do
  activate :gzip
end

Then I use the rack-zippy Rack to replace the Rack::Deflater middleware. This middleware simply check if there's a gzipped version for the file you're requesting, and serve it if found. Add the gem to your Gemfile and run bundle:

gem "rack-zippy"

then add this to your config.ru file:

require 'rack-zippy'

# replace `Rack::Deflater` with this line

use Rack::Zippy::AssetServer, 'build'

Modify Rack::StaticCache to work with rack-zippy

Then a problem comes: rack-zippy does not work well with Rack::StaticCache. The two middleware both try to serve a qualified file directly, rather than pass that file to another middleware.

If you put use Rack::Zippy::AssetServer before use Rack::StaticCache, you'll lose the extended expire header; and if you put it after, you'll lose the gzip file serving.

To make the two middlewares work properly, I need the StaticCache to call Zippy::AssetServer for file to serve, but still modify the header for extended expire dates. I've made a modified version of that, and pushed it to GitHub.

You can try that out by adding gem 'zippy_static_cache' to your Gemfile, than replace the Rack::StaticCache part in your config.ru with this:

require 'zippy_static_cache'

# put it before `use Rack::Zippy::AssetServer`

use ZippyStaticCache, :urls => ['/images', '/stylesheets', '/javascripts', '/fonts']

And it should work with rack-zippy now. But use at your own risk!

Final congifuration

This is my final config.ru:

config.ru
require 'rack'
require 'rack/contrib/try_static'
require 'rack-zippy'
require 'zippy_static_cache'

use ZippyStaticCache, :urls => ['/images', '/stylesheets', '/javascripts', '/fonts']
use Rack::Zippy::AssetServer, 'build'
use Rack::TryStatic,
  root: 'build',
  urls: %w[/],
  try: ['.html', 'index.html', '/index.html']

run lambda{ |env|
  four_oh_four_page = File.expand_path("../build/404/index.html", __FILE__)
  [ 404, { 'Content-Type'  => 'text/html'}, [ File.read(four_oh_four_page) ]]
}
comments powered by Disqus