當 Rails 專案逐漸成長之後,最令人頭痛的就是 Rails 的 boot time 實在是很久。
有些沒辦法 reload on change 的部分(例如修改 config、安裝 gem)都需要將 server 重啟,然後發現跑一次竟然要十幾秒甚至二十秒,發現 Rails developer 人生大半的時間其實都浪費在啟動時間…
除了加強硬體規格以外,比較能做的事情就是用 preloader 預先加載 Rails 進行加速,一般常用的有 spring / zeus,加上近期的 bootsnap 又更好的提昇效率了,依照多數使用者回報的數據,幾乎都能減少一半以上的時間,這個 Gem 預先把 code compile 出來的 bytecode cache 到 tmp/cache 之下再加上把 Ruby 不太有效率的 require 也 cache 所以就更快了。
不過如果用了這些工具還是想要更快的話,那就需要一些 tricks 了,這邊分享一下我的優化過程,主要會是著重於 Test + Development 模式, Production 能加速的著實有限。
如何測試啟動時間
完全測冷啟動,不吃 preloader
$ time bundle exec rake environment
# bundle exec rake environment 22.78s user 11.76s system 75% cpu 45.601 total
會吃 preloader 的啟動
$ time (bin/rails runner nil)
# Running via Spring preloader in process 60378
# ( bin/rails runner nil; ) 0.22s user 0.10s system 31% cpu 1.017 total
測試每個 gem 的加載速度,這裡我用 bumbler
測之前記得讓 bootsnap 關閉,否則會測不出來
$ bumbler
296.76 carrierwave
378.10 ethereum
393.60 devise
410.36 authy
532.06 compass-rails
546.09 newrelic_rpm
558.73 config
575.63 fog
604.26 country_select
608.17 coffee-rails
621.22 pry
711.51 sass-rails
761.82 google-authenticator-rails
887.80 stellar-sdk
948.57 twilio-ruby
1482.54 grape
2627.09 rails
測試初始化程序加載的狀況
$ bumbler --initializers
Slow requires:
279.70 newrelic_rpm.start_plugin
383.96 compass.initialize_rails
432.23 :set_clear_dependencies_hook
589.01 active_record.initialize_database
611.57 ./config/initializers/translation.rb
753.06 :load_environment_config
800.46 :load_config_initializers
3288.36 :finisher_hook
9697.74 :set_routes_reloader_hook
現在我們大概能掌握在慢在哪裡,我們可以開始著手處理了。
移除上古遺 Gem
專案大起來的時候,有些 Gem 可能是當初為了某個功能先加的,說不定後來不再使用或是有別的 Gem 取代了。
這時候就要去考古,找一下有沒有哪些 Gem 是「完全沒在用」的。
沒在用的一定要優先拔掉,這是比較好做卻也比較花時間的選項。
檢查有沒有用到較舊且有 memory leaky issue 的 Gem
可以參考 A list of gems that have memory leaks
對環境進行 Gem 分組
進行分組的用意是要明確的執行,在每個環境下只載入需要用的 Gem,避免多餘的 require 來拖慢速度。
前端相關非啟動就需要執行的 Gem, 在 Test 環境下不該被加載,應該給予 group [:production, :development]
# Styles
group :production, :development do
gem "coffee-rails", "~> 4.2"
gem "sass-rails", "~> 5.0"
gem "bootstrap-will_paginate"
gem "bootstrap-sass"
gem "bootstrap-switch-rails"
gem "bootstrap3-datetimepicker-rails", "~> 4.14.30"
gem "font-awesome-rails"
gem "jquery-ui-rails"
gem "active_link_to"
gem "rqrcode"
end
異步執行等相關任務,沒有要進行測試也不要在 Test 環境下加載
# Backgroud Jobs
group :production, :development do
gem "sidekiq"
gem "sidekiq-statistic"
gem "sidekiq-cron"
gem "sidekiq-unique-jobs"
gem "sidekiq-status"
gem "whenever"
end
只有在 Production 有用的監測服務也不要在 Development﹑Test 等其他環境下加載
group :production do
gem "newrelic_rpm"
gem "scout_apm"
end
只有開發使用的必須要自己一組
group :development do
gem "letter_opener"
gem "rubocop", require: false
# profiling
gem "bumbler", require: false
gem "rack-mini-profiler", require: false
# Dev helpers
gem "annotate"
end
分組後可以避免加載不需要的部分,讓啟動更有效率。
require: false
在 Gem file 裡面寫 require false
是指說,bundle 的情況下一定會安裝這個 Gem, 但不在 rails 啟動時直接 require 加載進來。
只在有需要的部分 require 會更有效率。
# Web Server 不需要直接 require,因為我們是單獨啟動,並不是執行在 Rails application 內
gem "puma", "~> 3.9", require: false
# sitemap 生成器,只有執行時會用到,不需要 preload
gem "sitemap_generator", require: false
# 顯示進度條的小工具,通常用在 task 執行,也只要在 task 的檔案 require 就好
gem "ruby-progressbar", require: false
# 只有語法檢測的時候會執行,其他時候 Rails app 並不會使用到
gem "rubocop", require: false
可以看到以上的範例有一個共通點,就是這些 Gem 都是單獨執行任務的,並不是隨時隨地在 Rails application layer 內需要用到的,所以可以直接 require: false
這樣一來,可以確保執行這個專案的時候一定會安裝相依套件,但卻不會在 Rails 啟動時直接加載。
別讓 Airbrake 在 development 被 require
這個一直是我認為滿煩的問題,常常會需要用到 Airbrake 來接 error,但畢竟這服務是賣 quota 的,恨不得你在 Development 模式下也用。
Development 的情況我是一定不用的,原因如下:
- 開發模式可以自己噴 Error,真的不需要你接走,謝謝喔
- Quota 要錢的
不過雖然 Airbrake 可以設定 ignore_environments = %w(development test)
,但最煩的是,他是在每個環境下都會加載的,不信你關個 server 或是退出 console 就知道了。
$ rails console
# Running via Spring preloader in process 61400
# Loading development environment (Rails 5.0.7)
# Development [1] rocket(main)> exit
# **Airbrake: closed
看到 **Airbrake: closed
了嗎?表示他是無所不在的。
如果你直接把他從 development 和其他環境下拔除
# Gemfile
group :staging, :production do
gem "airbrake", "~> 6.1"
end
那麼就會遇到
- 啟動時直接噴錯,因為 initialize/airbrake.rb 初始化加載會找不到 gem
- 有使用
Airbrake.notify(error)
的地方會爆炸
最佳解法,在不該出現的環境做一個空殼
# config/initializers/airbrake.rb
if Rails.env.staging? || Rails.env.production?
Airbrake.configure do |c|
...
...
end
Airbrake.add_filter do |notice|
...
...
end
else
module Airbrake
extend self
def notify(exception, opts = {})
end
end
end
就可以完美解決這個問題了。
Console 要用的套件不該 preload
我相信應該滿多人裝 awesome_rails_console
之類的,進 console 除了格式漂亮以外真的很慢,而且這種算是開發人員工具的也不該直接就在各個環境下加載。
我認為最好的做法是,在啟動 console 時才加載,不需要讓整個 Rails app 提早 preload
實際作法
先用 require: false
group :staging, :development, :test do
# Dev helpers
gem "awesome_rails_console", require: false
end
再設定 development 環境下開啟 console 要使用 Pry
# config/environments/development.rb
config.console = Pry
建立一個 .pryrc
的檔案在 project 目錄下
# .pryrc
require "awesome_rails_console"
AwesomePrint.pry!
這樣在 Console 啟動時,會直接使用 Pry 啟動,然而 Pry 啟動前又會去讀取 .pryrc
的設定檔,如此一來,就只有在 console 啟動時會 require 了。
讓 routes 加速更快
我很遺憾,這是唯一一個目前想不到解法的部分,當專案的 routes 被越養越大,大型的路由文件在啟動上花費的時間真的很多,我目前手上最大的專案大概就有 4 秒在 reload routes,在 bumbler 的效能分析上,他是 :set_routes_reloader_hook initializer
,如果把這個問題也解掉,也許能在快上 30% ?