A few years ago, I have a post talking about Autoloading. In recent days, my colleague has some problems with Autoloading and Reloading.
Therefore I decided to review the Autoloading mechanism in Rails 5 and 6.
Before we starting to discuss Autoloading or Reloading, I want to spend some time to think about it.
In the C, C++, or Java which is compiled language. They didn’t need to autoload because the compiler will include the necessary parts to the binary. And we usually use
import to include the related symbols to reference the codes we are required.
But in the Ruby, PHP, or Node.js which is interpreted in the runtime. That means our code didn’t preprocess before we execute it. And our code didn’t know other code until we
include them into our main program file.
These two types of languages are trying to split codes into small files. But for the interpreted language we cannot skip unnecessary files to loaded if we require each file.
In Ruby, we have a keyword
autoload that allows us to define “when the constant not defined, load the specify file.” to implement load required files.
It may reduce memory usage when we load a large amount of code in our application. But I more believe autoloading is used to help us to find codes in a large application that has many files.
The require method
In my code review, I ask for my colleague to use
require 'middleware/domain_rewriter' instead
require_relative '../lib/middleware/domain_rewriter' to include our extra middleware in Rails in
But it doesn’t work correctly, we have to use
require_relative in this case.
The question is “why we can use
require in the non-relatives path?”
In Ruby, we have a global variable named
$LOAD_PATH if we use
pp to print it, we can find our Ruby install path is listed inside it. It will let Ruby find files inside these paths when we try to require something.
If we have the
Gemfile in the project folder, the gems install path will be added to this list. This is why we can require gems only to add them to the Gemfile.
After we know the
require search paths, the reason why
config/application.rb cannot directly require
lib/ is easier to recognize.
The Rails is a Rack-based application and it usually boots from
1# frozen_string_literal: true 2 3# This file is used by Rack-based servers to start the application. 4 5require_relative "config/environment" 6 7run Rails.application
This file requires the
config/environment.rb and we can find it require the
1# frozen_string_literal: true 2 3# Load the Rails application. 4require_relative "application" 5 6# Initialize the Rails application. 7Rails.application.initialize!
It is obvious the Rails didn’t add
lib/ into the
$LOAD_PATH and we cannot require them directly.
Since we can load library elegant by use
require but we still need to require the application code via
require_relative and it makes us feel annoyed when our codebase is growing.
Since Rails 6, it starts to use the
Zeitwerk as the code loader, we will use it as an example in this post to reduce complex behaviors behind it.
According to Zeitwerk’s readme, we can know the basic logic is below shows:
lib/my_gem.rb -> MyGem lib/my_gem/foo.rb -> MyGem::Foo lib/my_gem/bar_baz.rb -> MyGem::BarBaz lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo
It is very similar to we put the files inside
app/models because Rails register these directories for us.
This is mean it is not required to use
_controlleras postfix in your
app/controllerfolder, but it will cause other hard to recognize the file usage.
The Zeitwerk uses Ruby’s
autoload to load classes when we configure the autoload paths, it will scan all files and add them to the related classes’ autoload list.
In my memory, the older version Rails has its autoloading implementation via override some Kernel methods and rescue NameError to find the actual file path to load it.
I think this part is the most Junior developer feeling confuse when they try to
require something but it breaks when they update some code.
In Zeitwerk we have
#enable_reloading options can grant permission to call the
#reload method. The reloading feature is helpful in the development environment when we change something but not required to restart the server.
For the compiled language, it is necessary to recompile and reopen it. But there have other methods can prevent it.
But why we can
#unload the interpreted code? This usually depends on the language feature, in the Ruby the constant variable is changeable and allows to be removed.
When we call the
#reload method, the Zeitwerk will
#unload constants which are loaded. And load all classes again to put the new codes into memory.
That is means when we have a top-level constant is unloaded, the children will be unloaded together.
This is a common mistake when we defined a child class in the same file with parent class, but directly use it in other file but didn’t load its parent.
It may not happen in newer Rails, the loader will try to load its parent before load it.
In the same case, a similar mistake is we define the
API namespace under autoload managed folder (eg.
app/) and define the in the not-managed folder (eg.
When we change some files under
app/ folder and it will unload the
API namespace, after reload the
API will be unloaded and never go back.
The reason is the
require recognize the file is loaded, therefore Ruby thinks it didn’t load again but it is unloaded for reloading.
Below codes is an example:
1# frozen_string_literal: true 2 3require_relative 'api' 4pp defined?(API) 5# => "constant" 6 7Object.send(:remove_const, 'API') 8require_relative 'api' 9pp defined?(API) 10# => nil 11 12load "api.rb" 13pp defined?(API) 14# => "constant"
require can prevent we load the same file twice but the
load didn’t check for it. The Zeitwerk also overrides the
#require method to provide a similar feature which managed by Zeitwerk loader.
Depends on the above example, we can have an outline of the Rails’ autoloading and reloading mechanism to help us use them more reasonably.
In the end, I have on more thing I want to talk about. The
lib/ is managed by Rails, too. But it can be used after Rails is booted, this is the reason we cannot use them in the
config/application.rb before boot.
The autoloading and reloading are useful when we develop our application and Zeitwerk allows us to add it to our project easier. If you have projects not based on Rails, I suggest you try to add it and learn more from practice.