Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Performance Starts at Boot

Avatar for Ben Sheldon Ben Sheldon
November 18, 2025
6

Performance Starts at Boot

Everyone can better understand how their Ruby code performs, regardless of whether they're using Rails or Hanami or just scripting with Ruby. As applications grow, I frequently see **inside-out** application performance work ignored or unacceptably tolerated ("that's just the way it is [sigh]").

Avatar for Ben Sheldon

Ben Sheldon

November 18, 2025
Tweet

Transcript

  1. Performance Starts at Boot Everyone can better understand how their

    Ruby code performs, regardless of whether they're using Rails or Hanami or just scripting with Ruby. As applications grow, I frequently see inside-out application performance work ignored or unacceptably tolerated ("that's just the way it is [sigh]"). Find a copy of these slides at speakerdeck.com/bensheldon https://island94.org [email protected]
  2. Outside - In Production customer perceived performance APM, RUM, N+1s,

    etc. Production Boot Performance ops stuff, deploy cadence, etc. Development Boot (and code reloading) Performance your lived experience as a developer writing and running code locally Inside - Out 2
  3. My goals for this talk 1. Convince you that making

    your Ruby app boot fast for development is maybe the lowest effort, reliably satisfying performance work you can do 2. Give you one big-picture principle to make Ruby development lightning fast 3. Help you identify some terribly common ways that developers will mess it up 4
  4. About me Author of GoodJob Currently, Co-Founder and CTO of

    Frontdoor Benefits Formerly, manager of GitHub's Ruby Architecture Team Based in San Francisco Looking for people to become cat fosters and forever homes 5
  5. 7

  6. Why do this work? Easy work: low permission threshold Overlooked

    work: low expectations Satisfying work: constrained but deep domain, code design, measurable Social work: work across application, teams, gems, open source Self work: you get the benefits Why not? Unfortunately, all that too ^^ 8
  7. That example again, from GitHub.com $ vernier run rails runner

    "puts true" : 4 seconds spent loading controllers 9
  8. One principle for making local development fast: Load less code

    * Also, don't do extensive file globbing expensive computations or hit an API or database during boot if you can help it. But mostly, it's the code loading. 10
  9. Background jobs in Rails 2009 — Delayed::Job 2012 — Sidekiq

    2013 — Que 2014 — ActiveJob in Rails 4.2 2020 — GoodJob That's my gem 2023 — Solid Queue 12
  10. 13

  11. Feature Request: Cron-style repeating/recurring jobs As a developer, I want

    GoodJob to enqueue Active Job jobs on a recurring basis so it can be used as a replacement for cron. Example interface: class MyRecurringJob < ApplicationJob repeat_every 1.hour # or with_cron "0 * * * *" def perform # ... ... end That sure would be a nice way to do it 14
  12. But I built it in GoodJob this way, instead #

    config/application.rb or config/initializers/good_job.rb config.good_job.cron = { recurring_job: { cron: "0 * * * *", class: "MyRecurringJob" # a String, not the constant } } The configuration doesn't live with the job # in the GoodJob gem... ActiveSupport.after_initialize do config.good_job.cron.each do |_key, config| only_on_schedule(config[:cron]) do config[:class].constantize.perform end end end 15
  13. But why? Expect an application to have a lot of

    code files. Loading files and constants, only to read configuration inside of them, is slow. We want to defer loading files and constants, because not-loading them is fast. The mechanism to defer loading is called Autoloading, which is built into Ruby ( autoload ) and also the Zeitwerk library. This all largely happens behind the scenes, and we largely don't think about it. Let's not mess it up. 16
  14. Briefly, Ruby Loading # immediately parse and run the code

    require 'some_constant.rb' And, Ruby Autoloading # defer parsing and running the code autoload :SomeConstant, 'some_constant.rb' # ... until some later code accesses the constant SomeConstant.new # => loads and runs some_constant.rb Zeitwerk can automatically set up an autoload if you follow Zeitwerks conventions for matching filenames and constants. And Zeitwerk unloads to reload constants too. 17
  15. Spotting autoloaded constants in Rails # Routes resources :posts #=>

    PostsController resources :items, controller: "posts" # Models has_many :comments has_many :comments, class_name: "PostComment" 19
  16. Break it down Configuration is require 'd during boot Behavior

    is autoloaded Development: Lazily. Only load what is needed when it's needed, ideally never. Production: Eagerly. before webserver forks or first request. 23
  17. Recall the example, from GitHub.com $ vernier run rails runner

    "puts true" : 4 seconds spent loading controllers 26
  18. Repair work Finding the culprit (temporary debugging) # config/application.rb require

    "rails" + ActiveSupport.on_load(:action_controller_base) do + puts "action_controller_base loaded" + puts caller # see what calls it + end Fixing it (and like 7 more like this) # config/initializers/asset_path.rb + ActiveSupport.on_load(:action_controller_base) do ActionController::Base.asset_host = ... + end 27
  19. ActiveSupport::LazyLoadHooks # rails/actionpack/lib/actioncontroller/base.rb ActionController::Base < Metal # ... ActiveSupport.run_load_hooks(:action_controller_base, self)

    end Most Rails framework autoloaded behavioral files will offer LazyLoadHooks ( ActiveRecord::Base , ActiveJob::Base , etc.) Gems do too, like Devise: ActiveSupport.run_load_hooks(:devise_controller, self) Use these to add configuration only when the constant is first accessed, not before. 28
  20. Also, Rails Initialization Events These won't completely defer constant loading,

    but still valuable tools to manage load order during application boot. 29
  21. A very common problem Using Devise: # config/routes.rb devise_for :users

    That twists around and then... # gems/devise/lib/devise.rb class Devise::Getter # ... def get @name.constantize # end end 31
  22. Some code for diagnosing If your application is already fast

    enough (run it twice for Bootsnap), take the win: $ time bin/rails runner "puts true" true bin/rails runner "puts true" 0.97s user 0.90s system 35% cpu 5.204 total $ time bin/rails runner "puts true" true bin/rails runner "puts true" 0.90s user 0.67s system 62% cpu 2.503 total If not, John Hawthorn's Vernier is an amazing Ruby profiler: $ vernier run bin/rails runner "puts true" starting profiler with interval 500 true #<Vernier::Result 2.768903 seconds, 18 threads, 10928 samples, 3468 unique> written to /var/folders/vm/p1vcrf3114s10pll1rm5pxbh0000gn/T/profile20240328-45567-7pu64h.vernier.json ...and then drop that profile*.json onto https://vernier.prof 37
  23. More debugging ideas # config/initializers/explore_autoloading.rb Rails.application.config.to_prepare do puts "Rails.config.to_prepare do

    ... end" end Rails.application.config.after_initialize do puts "Rails.config.after_initialize do ... end" end ActiveSupport.on_load(:active_record) do puts "ActiveSupport.on_load(:active_record) do ... end" puts caller # see what calls it end ActiveSupport.on_load(:action_controller) do puts "ActiveSupport.on_load(:action_controller) do ... end" end # ... etc for other LazyLoadHooks 38
  24. And more... # ruby find_autoloaded.rb require "./config/application" autoloaded_constants = []

    Rails.autoloaders.each do |loader| loader.on_load do |cpath, value, abspath| autoloaded_constants << [cpath, caller] end end Rails.application.initialize! autoloaded_constants.each do |x| x[1] = Rails.backtrace_cleaner.clean(x[1]).first end if autoloaded_constants.any? puts "ERROR: Autoloaded constants were referenced during during boot." puts "These files/constants were autoloaded during the boot process, which will result in"\ "inconsistent behavior and will slow down and may break development mode."\ "Remove references to these constants from code loaded at boot." w = autoloaded_constants.map(&:first).map(&:length).max autoloaded_constants.each do |name, location| puts "#{name.ljust(w)} referenced by #{location}" end fail end 39
  25. Finishing up Keep Ruby booting fast in development Design your

    code to separate configuration from behavior, and autoload the behavior. Use load hooks if you need to extend autoloaded behavior to avoid loading code prematurely or unnecessarily. Start seeing this stuff everywhere you look, and please do look into it. 40
  26. Lastly Find a copy of these slides at speakerdeck.com/bensheldon Help

    me find a foster or forever home for this sweet void: Samantha. Cats have an Instagram : @redcarpetcats https://island94.org [email protected] 41