Instructions manual

RACK-PAGESPEED

rack-pagespeed is a Rack middleware that helps reduce the time it takes for your web page/app to load.

How does it do that? It runs the output HTML through filters that apply a series of best practices automatically for you.

Installation and usage

Install via rubygems:

$ gem install rack-pagespeed

Example usage with Sinatra (in a rackup file, a.k.a. config.ru)

require 'rack/pagespeed'
require 'myapp'
use Rack::PageSpeed, :public => "/app/public/dir" do
  store :disk => Dir.tmpdir # require 'tmpdir'
  inline_javascript :max_size => 4000
  inline_css
  combine_javascripts
end
run Sinatra::Application

Usage with Rails:

require 'rack/pagespeed' # somewhere
class Application < Rails::Application
  config.middleware.use Rack::PageSpeed, :public => Rails.public_path do
    store :disk => Dir.tmpdir # require 'tmpdir'
    inline_javascript :max_size => 4000
    inline_css
    combine_javascripts
  end
  # ...

In a nutshell: invoke the filters you want and pass them parameters. rack-pagespeed will then figure out the best order of execution and run the filters you invoked against the document.

Filters

Filters modify the document in some way. The default filters are heavily inspired (or borrowed even, if you will) from Google's modpagespeed, for the Apache web server. I believe though that, as much as this middleware is slower than it, it's useful for at least 2 scenarios: when you don't have control over the web server, or when your web server isn't Apache. I can also come up with customized excuses if you need any.

Rolling your own filter is easy: subclass Rack::PageSpeed::Filter, define a #execute! method on your class which takes a Nokogiri HTML document as parameter, and you're set. For example:

class RemovesImages < Rack::PageSpeed::Filter
  def execute! document
    document.css('img').remove if rand(99) > @options[:chance] - 1
  end
end

It doesn't matter where you define the class, as long as you do it before invoking it, perhaps in a separate file which you'll #require or even inside the block where you invoke the filters. Wherever it is, rack-pagespeed will know where to find it.

So after declaring, you can call it like so:

use Rack::PageSpeed, :public => "/app/public/dir" do
  remove_images :chance => 50
end

rack-pagespeed will create a method out of the class name that looks much like what comes out of Rails' #underscore method. Any options that you pass (do pass a Hash), if any, will be accessible via @options.

There's two statements you can declare in a filter that are of note: priority and requires_store. The former, well, defines how high priority the filter is. The filter execution order is determined by this value. For example:

class ExampleFilter < Rack::PageSpeed::Filter
  priority 5 # will run before filters with priority <= 4
  priority '5th' # also works
  ...

Calling requires_store means your filter will need at some stage to use the storage mechanism provided in the #store declaration (see next section). Upon calling it, a @store variable will be made available to the class. Like so:

class ExampleFilter < Rack::PageSpeed::Filter
  requires_store

  def execute! document
    @store['foo'] = 'bar'
  end
end

Storage

Some filters may require some kind of storage where they can keep the result of their process. For example, the combine_javascripts filter, in order to make the results of it's process available, it will:

  1. join neighbouring JavaScripts it finds.
  2. put their joined contents in storage.
  3. replace the references to each individual script file in the HTML for a special URL which rack-pagespeed can use to locate the joined result saved in storage. It looks like /rack-pagespeed-098f6bcd4621d373cade4e832627b4f6.js.

So now instead of, say, 4 separate HTTP requests (one for each script), the browser will make 1, which rack-pagespeed will intercept and serve the bundle instead.

There are 3 types of storage available by default: disk storage (file system), Memcached, and Redis. You can use them like this:

use Rack::PageSpeed, :public => "/app/public/dir" do
  store :disk # defaults to Dir.tmpdir
  # or
  store :disk => '/path/to/some/dir'
  # or
  store :memcached # defaults to localhost:11211
  # or
  store :memcached => 'address:port'
  # or
  store :redis
  # or
  store :redis => { :host => 'hostname', :port => 6379 }

Here's a neat trick: if your app can write to it's public dir, if you call store :disk => "app's public dir", requests for bundled/compressed assets won't even hit the application stack. Meaning, huge performance gains.

You can override the hash generation mechanism by specifying a hash (or unique string) that the combine_javascripts and combine_css filters can use when finding certain files. For example:

use Rack::PageSpeed, :public => "/app/public/dir" do
  combine_javascripts :hash => { 
    %w(/javascripts/jquery-1.4.4.min.js /javascripts/myapp.js) => "myjavascripts" 
  }
end

This tells the filter that when it finds 2 scripts nodes referencing those scripts, that it should bundle them together using "myjavascripts" as a hash. The bundled script file would then look like this:

<script src="/rack-pagespeed-myjavascripts.js"><script>

The above is handy when you want bundles to be referenced in a predictable fashion, so you can add the URL safely to a manifest file, for instance.

Caveats & issues

Make sure you do at least a cursory check to ensure everything's working after you include this middleware in your stack. I'll happily look into issues reported via GitHub or email.

Stuff to bear in mind:

Author

rack-pagespeed is made with by @julio_ody. All code is public domain. This page's design however, is not. Source is, as usual, hosted on GitHub.