Understanding Ruby and Rails: Lazy load hooks

April 7th, 2011 at 10:22 am • permalink10 comments

This article targets Rails 3. 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.run_load_hooks at the end of your Ruby class.

class HttpClient
  # ...

  ActiveSupport.run_load_hooks(:http_client, self)
end

Now you can register on_load hooks everywhere passing the name of the library used in run_load_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 run_load_hooks 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 lazy_load_hooks.rb file.

Filed in Programming • Tags: , , , ,

Comments

[...] which allows you to lazy load code in initializers. Check out Simone Carletti’s tutorial on Lazy Load Hooks for details on how this works. In short, when action_controller is loaded, our code can be placed [...]

Great info! I have been using lazy loading with Rails Engine & Railtie initializers, but I wasn’t aware that you could register your own hooks with run_load_hooks. Thanks for sharing all the details.

Hi John,
I’m glad you found it useful! :)

It should be:
instead of:

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

You’re right. Fixed.

Thanks!

Steven Shankle says:

Great post, thanks.

By your request, I want to make a correction to your English, which is very understandable.

You have written two lines:
“The run_load_hooks 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.”

Both sentences have issues issues with participles that may be fixed by the same word.
“The run_load_hooks method takes a second parameter representing the context within which the hook will be executed. It’s a common pattern to pass the class/instance to which the hooks refer.”

While grammatically correct now, the sentence structure is a bit “snobby”; it might be better to re-organize them.

You could change just the second sentence to convey the “common pattern” idea: “Passing the hook-referenced class/instance as a second parameter is a common pattern.”, but it is still a choppy construct.

Or, in this context with better flow:
“The run_load_hooks method makes use of a common pattern of taking a second parameter which is used to pass in the hook-referenced class/instance context.”

Or, perhaps this single sentence structure:
“A commonly used pattern of taking a second parameter allows the run_load_hooks method access to the the hook-referenced class/instance context.”

Or, this one:
“We use the common pattern of taking a second parameter to provide the run_load_hooks method access to the the hook-referenced class/instance context.”

Or, perhaps the “common pattern” idea is just too awkward:
“The run_load_hooks method takes a second parameter in order to pass in the hook-referenced class/instance context.”

Or, perhaps:
“The run_load_hooks method takes a second parameter which is used to pass in a class/instance context for the hooks.”

Or, perhaps if we need the “common pattern” idea:
“We use a common pattern to pass in a class/instance context for the hooks; the run_load_hooks method takes a second parameter.”

Or, perhaps better:
“Using a common pattern, the run_load_hooks method takes a second parameter to pass in a class/instance context for the hooks.”

Or, perhaps even more succinct as two sentences:
“The run_load_hooks method takes a second parameter to pass in a class/instance context for the hooks. Passing class/instance context in this manner is a common pattern.”

English constructs are nuanced and can be just nasty, but your command of the language for conveying ideas is to be applauded. Thanks again.

Hi Steven,

thank you very much for taking the time to write this long comment.
I’ll read it carefully and try to assimilate all the information.

Thanks!

Nathan B says:

Was just wondering, in the last block, should `puts “The color is #{@color}”` be `puts “The color is #{@name}”`?

Cheers

You’re right. Fixed, thank you.

Matt Brewer says:

Great explanation, helped me figure out how to hook into another gem that I wanted to do some setup on, from inside my Rails engine initializer blocks :)

Add a Comment




Follow Me
    Random Quote