Understanding Ruby and Rails: Lazy load hooks

This article targets Rails 3

The article was written as of Rails 3.2. The information contained in this page might not apply to different versions.

This is article is part of my series Understanding Ruby and Rails. Please see the table of contents for the series to view the list of all posts.

A small-but-interesting feature introduced in Rails 3 is the built-in support for lazy loading.

Lazy loading is a very common design pattern. The concept is to defer initialization of an object until the point at which it is needed. This design pattern decreases the time required by an application to boot by distributing the computation cost during the execution. Also, if a specific feature is never used, the computation won't be executed at all.

With Rails 3 you can now register specific hooks to be lazy-executed when the corresponding library is loaded.

class ApplicationController < ActionController::Base

  initializer "active_record.include_plugins" do
    ActiveSupport.on_load(:active_record) do
      include MyApp::ActivePlugins
    end
  end

end

In this case we register the block to be executed when the ActiveRecord library is loaded. If you read the ActiveRecord::Base source code, the very last line is a call to

ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)

This line of code executes all the hooks previously registered for ActiveRecord.

lazy-load hooks in the wild

Perhaps one of the most frequent usage of the lazy-load hooks is in Rails plugins.

For example, if your plugin needs to register some helpers, you can write a hook to include the helpers in ActionView only when ActionView is loaded. If the environment is loaded from a rake task (which doesn't necessary need ActionView), then your plugin hook won't be executed and the Rails application will boot faster.

Here's an example from the will_paginate gem.

require 'will_paginate'
require 'will_paginate/collection'

module WillPaginate
  class Railtie < Rails::Railtie
    initializer "will_paginate.active_record" do |app|
      ActiveSupport.on_load :active_record do
        require 'will_paginate/finders/active_record'
        WillPaginate::Finders::ActiveRecord.enable!
      end
    end

    initializer "will_paginate.action_dispatch" do |app|
      ActiveSupport.on_load :action_controller do
        ActionDispatch::ShowExceptions.rescue_responses['WillPaginate::InvalidPage'] = :not_found
      end
    end

    initializer "will_paginate.action_view" do |app|
      ActiveSupport.on_load :action_view do
        require 'will_paginate/view_helpers/action_view'
        include WillPaginate::ViewHelpers::ActionView
      end
    end
  end
end

Using lazy load in your libraries

So far, we only discussed about using lazy-loading to hook Rails core library. Because lazy-loading is an ActiveSupport feature, you can use it in your Rails applications but also in your own Ruby classes.

First, make sure to add a call to ActiveSupport.runloadhooks at the end of your Ruby class.

class HttpClient
  # ...

  ActiveSupport.run_load_hooks(:http_client, self)
end

Now you can register onload hooks everywhere passing the name of the library used in runload_hooks.

class Request
  # ...

  ActiveSupport.on_load :http_client do
    # do something
  end
end

class Response
  # ...

  ActiveSupport.on_load :http_client do
    # do something
  end
end

Hook context

The runloadhooks method takes a second parameter representing the context within the hook will be executed. It's a common pattern to pass the class/instance the hooks refer to.

For instance, the ActiveRecord library mentioned at the beginning of the article passes self. In that context, self references the ActiveRecord::Base class.

This is useful because, if you want to include some custom methods into ActiveRecord, you can use the following block

ActiveSupport.on_load :active_record do
  include MyPlugin::Extensions
end

instead of

ActiveSupport.on_load :active_record do
  ActiveRecord::Base.send :include, MyPlugin::Extensions
end

There's also an other interesting use case for this feature. If you want to perform some kind of lazy-initialization when an instance of a class is created, just pass the instance itself.

class Color
  def initialize(name)
    @name = name

    ActiveSupport.run_load_hooks(:instance_of_color, self)
  end
end

ActiveSupport.on_load :instance_of_color do
  puts "The color is #{@name}"
end

Color.new("yellow")
# => "The color is yellow"

Source Code

The source code of the lazy-loading feature is available in the lazyloadhooks.rb file.