osyoyu さんが企画したイベント ですね。
Rails in 1.44MB Challenge とは
Rails一式をフロッピーディスク(1.44MB)に収めてください。会場でフロッピーディスクに書き込んで動かしてプレゼントします!
Railsは結構でっかいフレームワークです。依存しているGemを全部足しあげると50MBぐらいになります。機能削減・圧縮・ズル、あらゆるテクを駆使して1/30のサイズにしてください!
ということで、50MB を何とかして 1.44MB に削るコンテストです。Kaigi on Rails 2025 のブース企画の中?いやほとんど個人企画だな?で開催されていました。
達成できたのか
$ du -sh .
1.3M .
$ ./bin/rails s
=> Booting WEBrick
=> Rails 8.0.3 application starting in development http://localhost:3000
=> Run `bin/rails server --help` for more startup options
[2025-09-29 14:04:17] INFO WEBrick 1.9.1
[2025-09-29 14:04:17] INFO ruby 3.4.5 (2025-07-16) [arm64-darwin24]
[2025-09-29 14:04:17] INFO WEBrick::HTTPServer#start: pid=27522 port=3000
Started GET "/?name=onk" for 127.0.0.1 at 2025-09-29 14:04:25 +0900
Processing by HomeController#index as HTML
Parameters: {"name" => "onk"}
Completed 200 OK in 0ms (Views: 0.0ms | GC: 0.0ms)
127.0.0.1 - - [29/Sep/2025:14:04:24 JST] "GET /?name=onk HTTP/1.1" 304 0
- -> /?name=onk
で、達成としてもヨサソウ。
改めてレギュ読んでたら「Ubuntu で動く」とあって、やっべー忘れてた、となっているのが今です。試しにビルドしたらダメっぽかったので重い C 拡張 gem を殺さなきゃ……。
「Rails が動く」の私の定義
普通の controller が動く、をゴールにしたい。
Rails.application.routes.draw do
root "home#index"
end
class HomeController < ApplicationController
def index
render plain: "Hello, #{params[:name] || "World"}!"
end
end
$ rails s
で、http://localhost:3000/ にアクセスして render されること。
Started GET "/" for 127.0.0.1 at 2025-09-29 13:47:39 +0900
Processing by HomeController#index as HTML
Completed 200 OK in 0ms (Views: 0.1ms | GC: 0.0ms)
理想的には scaffold の generator が動いて、その機能を全部網羅する、がやりたいけど、いつも通りの場所にアプリケーションコードを置けて、routing と render :plain があれば許せる。
rails s で起動させることを考えると railties も要るし、app/* を読み込むことを考えると zeitwerk も要るだろう。
状況確認
rails gem は依存多すぎるので入れない、actionpack と railties は必要として、これだけに削って、vendor/bundle を tgz で固めると達成できるのか。
source "https://rubygems.org"
gem "actionpack"
gem "railties"
$ bundle config set path "vendor/bundle" && bundle install
$ du -sh vendor/bundle/ruby/3.4.0/*
40K vendor/bundle/ruby/3.4.0/bin
0B vendor/bundle/ruby/3.4.0/build_info
7.4M vendor/bundle/ruby/3.4.0/cache
0B vendor/bundle/ruby/3.4.0/doc
3.7M vendor/bundle/ruby/3.4.0/extensions
24M vendor/bundle/ruby/3.4.0/gems
4.0K vendor/bundle/ruby/3.4.0/plugins
184K vendor/bundle/ruby/3.4.0/specifications
vendor/bundle 以下には cache/ に gem ファイルがそのまま置いてあったりするので固めるのが面倒だな。とりあえず extensions が 3.7M、gems が 24M あることが分かる。
$ rm -rf vendor/bundle/ruby/3.4.0/cache
$ tar czf vendor_bundle.tgz vendor/bundle
$ ls -hl vendor_bundle.tgz
-rw-r--r-- 1 onaka staff 6.1M 9 29 13:28 vendor_bundle.tgz
と最小の Gemfile で入ったものを tgz で固めても 6.1MB あるので、依存をもりもり削らないと達成できないことが分かる。なお試しに zstd や brotli にしても大差なかった。
この時点で C 拡張の重いヤツ:
$ du -sh vendor/bundle/ruby/3.4.0/extensions/arm64-darwin-24/3.4.0/*
172K vendor/bundle/ruby/3.4.0/extensions/arm64-darwin-24/3.4.0/bigdecimal-3.2.3
292K vendor/bundle/ruby/3.4.0/extensions/arm64-darwin-24/3.4.0/date-3.4.1
60K vendor/bundle/ruby/3.4.0/extensions/arm64-darwin-24/3.4.0/erb-5.0.2
88K vendor/bundle/ruby/3.4.0/extensions/arm64-darwin-24/3.4.0/io-console-0.8.1
2.9M vendor/bundle/ruby/3.4.0/extensions/arm64-darwin-24/3.4.0/nokogiri-1.18.10
80K vendor/bundle/ruby/3.4.0/extensions/arm64-darwin-24/3.4.0/psych-5.2.6
60K vendor/bundle/ruby/3.4.0/extensions/arm64-darwin-24/3.4.0/racc-1.8.1
88K vendor/bundle/ruby/3.4.0/extensions/arm64-darwin-24/3.4.0/stringio-3.1.7
bigdecimal, date が重いのは、いやー外して動くのか?
ruby コードとして重い gem トップ 10 はこう。
$ du -s vendor/bundle/ruby/3.4.0/gems/* | sort -rn | head
14296 vendor/bundle/ruby/3.4.0/gems/nokogiri-1.18.10
5168 vendor/bundle/ruby/3.4.0/gems/rdoc-6.14.2
3440 vendor/bundle/ruby/3.4.0/gems/railties-8.0.3
3368 vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.3
2840 vendor/bundle/ruby/3.4.0/gems/concurrent-ruby-1.3.5
2792 vendor/bundle/ruby/3.4.0/gems/actionpack-8.0.3
2272 vendor/bundle/ruby/3.4.0/gems/actionview-8.0.3
1392 vendor/bundle/ruby/3.4.0/gems/date-3.4.1
1040 vendor/bundle/ruby/3.4.0/gems/irb-1.15.2
1024 vendor/bundle/ruby/3.4.0/gems/rack-3.2.1
nokogiri や rdoc はまぁ削れるだろう (勘) けど、railties や activesupport も削る必要がありそうな予感がありますね。
依存 gem を削減したい
Bundler には以下のように fork を指定する機能がある。
gem "foo", github: "user/name", ref: "commit"
gem "bar", path: "path/to/gem"
ので、各 gem の gemspec を真面目に書き換えながらやるかなーと考えたけど、publish されている gem ってテストコード削っていたり、割と丁寧目に gem 自体が軽くなるような処理がされているンですよね。
Gem::Specification.new do |s|
...
s.files = Dir["CHANGELOG.md", "README.rdoc", "MIT-LICENSE", "lib/**/*"]
...
end
lib だけにするとかを再現するのが面倒そうなので、
- bundle install したあとのコードを直接編集していく
- bundle を介さずに gem を読み込む
という作戦にしました。
bundle を介さずに gem を読み込む
$LOAD_PATH を弄って require するのは知っていたので、claude code にに丸投げした。(Kaigi on Rails の発表も聞きたいからね……)
コードとしては Bundler 以前の世界というか、普段 rubygems 作るときも exe とか spec_helper に同じコードは書くよねーって感じですね。
gem_path = File.expand_path("../vendor/bundle/ruby/3.4.0/gems", __dir__)
Dir.glob("#{gem_path}/*").sort.each do |gem_dir|
lib_path = "#{gem_dir}/lib"
$LOAD_PATH.unshift(lib_path) if File.directory?(lib_path)
end
実際の gem 削減
vendor/bundle/ruby/3.4.0/gems/foo-x.y.z/ を消して動けばヨシ、動かなかったら require を見つけては殺す、みたいな仕事です。これも claude code にどんどん投げてる。
例えば actionview を外したかったら、https://github.com/rails/rails/blob/v8.0.3/actionpack/lib/abstract_controller/rendering.rb#L6-L7 とか https://github.com/rails/rails/blob/v8.0.3/actionpack/lib/abstract_controller/rendering.rb#L20 とかを消していく感じです。
diff --git a/vendor/bundle/ruby/3.4.0/gems/actionpack-8.0.3/lib/abstract_controller/rendering.rb b/vendor/bundle/ruby/3.4.0/gems/actionpack-8.0.3/lib/abstract_controller/rendering.rb
index 346bb4e..e9ebcc7 100644
--- a/vendor/bundle/ruby/3.4.0/gems/actionpack-8.0.3/lib/abstract_controller/rendering.rb
+++ b/vendor/bundle/ruby/3.4.0/gems/actionpack-8.0.3/lib/abstract_controller/rendering.rb
@@ -3,8 +3,8 @@
# :markup: markdown
require "abstract_controller/error"
-require "action_view"
-require "action_view/view_paths"
+# require "action_view"
+# require "action_view/view_paths"
module AbstractController
class DoubleRenderError < Error
@@ -17,7 +17,7 @@ module AbstractController
module Rendering
extend ActiveSupport::Concern
- include ActionView::ViewPaths
+ # include ActionView::ViewPaths
# Normalizes arguments and options, and then delegates to render_to_body and
# sticks the result in `self.response_body`.
railties がデフォルトで入れてくる Rack middleware が邪魔だなーって消したのもある。
https://github.com/rails/rails/blob/v8.0.3/railties/lib/rails/application/default_middleware_stack.rb#L14-L110
あと、このコード見たら分かるけど、結構 if config.respond_to?(:active_record) 等がある。下手に config.acitve_record = ActiveSupport::OrderedOptions.new 等で起動時のエラーを回避しようとすると面倒なことになるので気をつけて。なので config/environments/*.rb の各行が何をやっているのかは知っておいた方が便利と思う。
concurrent-ruby 外し
nokogiri や rdoc、actionview 辺りはどこで使っているのか明確だし外せる自信があったのでサクッと作業したけど、concurrent-ruby はちょっと悩んだ。300KB 以上あるので外すのがいいに決まっているが、外せるのか。rails 全体の中で 34 件しか require されてないならまぁ行けるのかなーと思って取りかかった。
$ rg 'require "concurrent/' -l | wc -l
34
だいたい
-Concurrent::Map.new
+Hash.new
の書き換えでいけるやろと思って作業させました。概ね行けたけど、僕まだ Concurrent::Map の実装読んだこと無いンだよな。そもそも git clone すらしてなかったと思って今回は反省した。
minifyrb
koic/minifyrb の存在を知っていたので、コレで適当にガーッと。
$ fd --type f "\.rb$" | xargs -I{} sh -c "minifyrb {} -o {}"
koic.hatenablog.com
gzip する前にコメント行を削除するのが効くといいなと思ってやりました。コメントを削るだけなら他にやりようはあったと思うけど、目の前に落ちてたのでお手軽だった。
とりあえず ActiveSupport と ActionPack 全部に掛けたけど、何ファイルかは壊れたので戻してます。(フィードバックチャンス)
railties でっけーなー、減らせるかな? と思って眺めたら、generators ディレクトリが 1MB って出たので。
$ du -hd 3 vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/
8.0K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/minitest
32K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/tasks
4.0K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/plugin
4.0K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/rackup
4.0K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/testing
28K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/test_unit
4.0K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/autoloaders
8.0K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/rack
12K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/api
44K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/templates
164K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/commands
24K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/command
1.0M vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/generators
56K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/application
8.0K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/railtie
24K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/engine
4.0K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails/console
1.6M vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib/rails
1.6M vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/lib
4.0K vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/exe
1.7M vendor/bundle/ruby/3.4.0/gems/railties-8.0.3/
もう 60% ぐらい generator じゃん。いやー、generator が rails の肝なんだなーって実感しますね。
tgz から起動
ソラで書ける自信無かったのでまるっと claude code に任せた。
tar czf vendor_bundle.tgz vendor/bundle した vendor_bundle.tgz があります。起動時に伸張するように config/boot.rb を書き換えてください
一発目は system("tar zxf ...") するコードが出てきたので、「pure ruby でできる?」って聞いたら zlib は予想通りとして、Gem::Package::TarReader が出てきた。これは初知りライブラリでした。
github.com
感想
数時間で遊べる、ちょうどいい難易度のクイズだった。osyoyu さんありがとうございます。
Gemfile.lock は普段から眺めているのでまぁまぁ何に依存しているか知っている、という前提を置けるのが良かったね。レギュB の Ruby 自体を最小化する方は普段から (ruby-build に任せずに) ビルドしまくってないと勘所が掴めなそう。
Bundler 使わずに gem 読むの 10 年ぶりに手書きしてるなーとか、actionview とこう密結合してるのかーとか、concurrent-ruby こんなでっかいの!とか、イマドキの gem は test 系の不要ファイルを本当に含んでないなとか、date や bigdecimal が gemify される前ならこんな苦労はとか、トークを聞きながら AI Agent に任せる粒度としてちょうどいいとか、面白かった点がいくつもあった。
皆さんもやってみてください :)
しかし、継続的に頭を使う必要があるとトークを聞けなくなるので、裏企画はあんまり増えないで欲しい!(>_<)
これは途中で「やばっ、話している内容が頭に残ってねぇ」と気づいて焦った顔で発言しています。
アルコールを多少控えてホテルに帰ってから手を動かせば良いのはそうなんだが……。