rails-minify 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf98d2d12a8005e5ef2755cd6bba97a32122a7dd2514518e9a749d1ba367af00
4
- data.tar.gz: 878a2fab37d90963eb4b4d5924d7b3437847672741471d84ea6cefc686cb7ca1
3
+ metadata.gz: 2e018dfbd1f9df88b770cd083ba94da47867f8199f88fd885d7bff62b2efb592
4
+ data.tar.gz: 6a0ac44911b837229cd36caa47bc4571dda38296952c33198542f12b79237d66
5
5
  SHA512:
6
- metadata.gz: 2baf13ba8db10c35cfaccfaf86b6c966b11555b9b232c1710da044a37dad166f96dee80cdc2380db98321a845c612e80f3a3648dbc515b31f9402a54bc393599
7
- data.tar.gz: b26a83ac5f86fc456c18444e7eba578cb095fa897ee4bac9a508a10047a9164fc8d22197dc0ae6f7f2c7193ca32a437fe1b8fc35c7c9ab873d9091c936be57a0
6
+ metadata.gz: e10f339acb5fe096a62873b7ffc292a0c5c322854eee25f856ebd09505e812f2d97b18b8427fc7f2ad5249404ec85dc9b7f2576cebcdb32b0b3285e0ba92c90c
7
+ data.tar.gz: be2ebd0fbfae79dc3f6217d792337261f517c06f882ab506867b262c576eaa1dd17fd313931e8625dfa0ccd2ffabedfc0dcb80659817f0b6ff1d4f7cfc3e6ee6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 (2026-10-20)
4
+
5
+ Narrowed to what held up in production (Japan Voyage, each part on half the workers under the same traffic; README, "What paid off in production").
6
+
7
+ - `config.rails_minify.enabled` turns on only strict mode (`ar_minimizer.slim`), enums and ZJIT after boot. ZJIT only in production and from Ruby 4.1 (4.0's ZJIT isn't meant for production); set `config.rails_minify.zjit` yourself otherwise.
8
+ - No longer turned on by `enabled`: `ar_minimizer.views`, `.load_time`, `.small_records`, `as_minimizer.unroll_callbacks`, `ap_minimizer.route_helpers`, `.views`, and the patches. Each still works when set on its own and is documented as experimental. Patches: `config.rails_minify.patches = true` for all, or a list.
9
+ - actionpack-minimizer is no longer a dependency; rails-minify loads it, and `minify:census` covers it, when it's in the Gemfile.
10
+ - Requires activerecord-minimizer and activesupport-minimizer 0.2.
11
+
12
+ ## 0.1.3 (2026-10-18)
13
+
14
+ - `config.rails_minify.zjit = true` turns ZJIT on after boot, as `config.yjit` does for YJIT, instead of `RUBY_ZJIT_ENABLE=1` compiling boot-only code too. Japan Voyage, per process: 24% fewer compiled methods, 18 MB less JIT memory, about 25 MB less RSS, the same speed.
15
+
3
16
  ## 0.1.2 (2026-10-18)
4
17
 
5
18
  - `minify:check` also fails on callback chains longer than `callback_limit` (default 5, `ApplicationController`'s callbacks excluded) and on model callbacks that save their own record again; `callback_limit_mode` and `callback_loops` set either to `:warn`. Works without the rest of rails-minify turned on; the manifest check runs only when there is a manifest.
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # rails-minify
2
2
 
3
- Cuts what Rails (and common gems) build or redo on every request.
3
+ Fewer generated Active Record methods, ZJIT turned on after boot, and checks
4
+ for callback problems.
4
5
 
5
6
  ```ruby
6
7
  # Gemfile
@@ -10,14 +11,19 @@ gem "rails-minify"
10
11
  config.rails_minify.enabled = true
11
12
  ```
12
13
 
13
- `enabled` turns on every part below; each can still be set on its own
14
- (`config.ar_minimizer.enums = false`, `config.rails_minify.patches = [:omniauth]`).
14
+ `enabled` turns on what held up in production on a real app:
15
+
16
+ * strict mode (`config.ar_minimizer.slim`): only readers and writers per column;
17
+ * enum predicates and bang methods as plain methods (`config.ar_minimizer.enums`);
18
+ * ZJIT after boot (`config.rails_minify.zjit`), in production on Ruby 4.1+.
19
+
20
+ Each can still be set on its own (`config.ar_minimizer.enums = false`).
21
+ Everything else is [experimental](#experimental) and off unless you set it.
15
22
  Ruby 3.4+, Rails 8.0, 8.1 and main.
16
23
 
17
24
  Turn it on in tests too, so a call to a method strict mode removed fails in CI
18
25
  rather than in production. In development, strict mode alone is enough: the
19
- same errors show up while you work, and the compiling parts, which don't
20
- follow code reloads, stay off.
26
+ same errors show up while you work.
21
27
 
22
28
  ```ruby
23
29
  # config/environments/test.rb
@@ -27,7 +33,7 @@ config.rails_minify.enabled = true
27
33
  config.ar_minimizer.slim = true # strict mode, without the rest
28
34
  ```
29
35
 
30
- ## Strict by default
36
+ ## Strict mode
31
37
 
32
38
  Active Record defines, for every column, a reader, a writer and 19 more
33
39
  methods (`name?`, `name_changed?`, `saved_change_to_name?`, ...). With
@@ -46,23 +52,110 @@ rubocop-minify rewrites those calls in your code. For calls in gems
46
52
  `config.ar_minimizer.strict = false` keeps the removed methods working
47
53
  instead, generated the first time each is called.
48
54
 
49
- ## What's in it
55
+ ## ZJIT after boot
56
+
57
+ ```ruby
58
+ config.rails_minify.zjit = true # instead of RUBY_ZJIT_ENABLE=1 / --zjit
59
+ ```
60
+
61
+ `enabled` sets it in production on Ruby 4.1 and later; on 4.0, ZJIT isn't
62
+ meant for production yet, so set it yourself if you want it there. ZJIT
63
+ options still go in `RUBYOPT`, after `--zjit-disable` so they don't turn it
64
+ on at startup: `RUBYOPT="--zjit-disable --zjit-call-threshold=2"`.
65
+
66
+ `RUBY_ZJIT_ENABLE=1` turns ZJIT on before anything loads, so it also compiles
67
+ what runs only while booting: RubyGems, requiring and eager loading, and the
68
+ minimizers' own compiling. Rails turns YJIT on after boot (`config.yjit`) for
69
+ this reason, but has no such setting for ZJIT; this is that one. It works on
70
+ its own, without the rest of rails-minify.
71
+
72
+ On Japan Voyage, per process after the same 132 pages: 12,612 → 9,557
73
+ compiled methods, JIT code 45.9 → 35.3 MB, ZJIT memory 82.9 → 64.6 MB,
74
+ RSS about 25 MB less, the same CPU time per page.
75
+
76
+ ## Checks
77
+
78
+ * `bin/rails minify:check` (for CI; works whether or not the rest of
79
+ rails-minify is on) fails on:
80
+ * callbacks naming methods that don't exist;
81
+ * callback chains with more than `callback_limit` (default 5) of the app's
82
+ own callbacks (Rails' and gems' don't count; controllers per action).
83
+ Callbacks inherited from `callback_limit_exclude` (default
84
+ `["ApplicationController"]`: time zone, locale, current user, the
85
+ per-request basics) don't count either. A base class over the limit is
86
+ reported once, with the classes that inherit it;
87
+ * model callbacks that call `save`, `update`, `touch`, ... on the record
88
+ they run for, which runs the chain again;
89
+ * an out-of-date manifest, when there is one (only the experimental
90
+ compiling parts use it).
91
+
92
+ ```ruby
93
+ config.rails_minify.callback_limit = 8
94
+ config.rails_minify.callback_limit_exclude = ["ApplicationController", "Admin::BaseController"] # [] counts everything
95
+ config.rails_minify.callback_limit_mode = :warn # report, don't fail
96
+ config.rails_minify.callback_loops = :warn
97
+ config.rails_minify.allow_callback_loops = ["Trip#recalculate"] # guarded on purpose
98
+ ```
99
+
100
+ ## What paid off in production
101
+
102
+ Each gem's README has benchmark numbers. In production, on
103
+ [Japan Voyage](https://japanvoyage.jp) (Falcon, 6 workers, ZJIT, SQLite),
104
+ each part ran for an hour on half the workers while the other half ran
105
+ without it, under the same traffic (about 5,000 requests per half):
50
106
 
51
- | part | does | gem |
107
+ | part | in production | in 0.2.0 |
52
108
  |---|---|---|
53
- | attribute methods | readers and writers only (strict) | activerecord-minimizer |
54
- | enums | predicates/bang methods as plain methods (1.3-1.6x) | activerecord-minimizer |
55
- | small records | no Hashes for records that never fill them (-22% per record) | activerecord-minimizer |
56
- | callbacks | chains as generated methods (`valid?` -33..45%) | activesupport-minimizer |
57
- | route helpers | `post_path(post)` as interpolation (3-14x) | actionpack-minimizer |
58
- | views | `render`/`link_to` without their machinery (2x per page of partials) | actionpack-minimizer |
59
- | patches | below | rails-minify |
60
- | rewrites | removed-method calls in Ruby and ERB, as RuboCop autocorrections | rubocop-minify (development) |
109
+ | ZJIT after boot | HTML p50/p90/p99 75/131/231 → 70/126/208 ms, ~25 MB less per worker (an hour before against an hour after) | on with `enabled` (production, Ruby 4.1+) |
110
+ | enums | the same medians; that hour's lower mean and p99 came from a few slow requests, as it changes no allocations on these pages | on with `enabled`: harmless, no measured gain |
111
+ | strict | the same latency and memory; no calls to a removed method | on with `enabled`, so production runs what the tests run |
112
+ | small records | no difference (tens of records per request) | experimental |
113
+ | route helpers | no difference in view time | experimental |
114
+ | views (`render`/`link_to`) | no difference in view time | experimental |
115
+ | compiled call sites in views | ~22 MB more per worker, no faster | experimental |
116
+ | compiled call sites at load time | ~12% more CPU per request (not reproduced locally) | experimental |
117
+ | callbacks | no faster, ~20 MB more per worker | experimental |
118
+ | patches | no difference (view time 25.3 vs 26.8 ms) | experimental |
119
+
120
+ ZJIT after boot is the one part with a clear effect. What `enabled` turns on
121
+ is what Japan Voyage runs.
122
+
123
+ What the benchmarks speed up is per call: a method call, a Hash, a helper.
124
+ A page there spends ~30 ms in Active Record and ~30 ms in views, and those
125
+ savings were smaller than the difference between two halves of the same
126
+ workers. Parts that generate or compile code in each worker (callbacks,
127
+ compiled views) cost memory in every worker; most of these ran before the
128
+ app was preloaded, and would cost less with preloading.
129
+
130
+ Besides ZJIT after boot, what sped Japan Voyage up was outside this gem: a
131
+ preloading server warming the heap up before it forks, and one query fixed.
132
+ Together, HTML p50/p90/p99 went from 75/131/231 to 45/66/~105 ms.
133
+
134
+ ## Experimental
135
+
136
+ Each was faster in benchmarks and made no measurable difference in
137
+ production (above). They still work, each turned on by its own setting:
138
+
139
+ | part | setting | gem |
140
+ |---|---|---|
141
+ | small records: no Hashes for records that never fill them (-22% per record) | `config.ar_minimizer.small_records` | activerecord-minimizer |
142
+ | call sites compiled to what removed methods stood for | `config.ar_minimizer.views`, `.load_time` | activerecord-minimizer |
143
+ | callback chains as generated methods (`valid?` -33..45%) | `config.as_minimizer.unroll_callbacks` | activesupport-minimizer |
144
+ | route helpers as interpolation (3-14x) | `config.ap_minimizer.route_helpers` | actionpack-minimizer |
145
+ | `render`/`link_to` without their machinery (2x per page of partials) | `config.ap_minimizer.views` | actionpack-minimizer |
146
+ | patches | `config.rails_minify.patches` | rails-minify |
147
+
148
+ actionpack-minimizer is no longer a dependency: add `gem "actionpack-minimizer"`
149
+ to use it. `bin/rails minify:census` shows how much of the app the compiling
150
+ parts cover, and why the rest isn't (actionpack-minimizer's part when it's
151
+ installed).
61
152
 
62
- ## Patches
153
+ ### Patches
63
154
 
64
- For parts of Rails and common gems that redo work on every request.
65
- `config.rails_minify.patches = [...]` picks them; all by default.
155
+ For parts of Rails and common gems that redo work on every request. Off
156
+ unless `config.rails_minify.patches` is set: `true` for all of them, or a
157
+ list (`[:omniauth]`). In production they made no difference in view time;
158
+ each is on its way upstream.
66
159
 
67
160
  * `:action_text_plain_text`: `ActionText::Content#to_plain_text` parses the
68
161
  HTML with Nokogiri, and looks up every attachment's record, on every call.
@@ -84,38 +177,14 @@ For parts of Rails and common gems that redo work on every request.
84
177
  installed when OmniAuth already does this itself (it then has a public
85
178
  `Strategy#passthrough?`, as in the version proposed upstream).
86
179
 
87
- ## Numbers
180
+ ### Allocations
88
181
 
89
182
  Japan Voyage, 132 production pages, with everything on (strict mode after
90
183
  rubocop-minify's rewrites, all patches): identical HTML, and
91
184
  1,561,283 → 1,224,141 allocations per pass (−21.6%). The machine was too
92
185
  loaded for wall-clock times to mean much.
93
186
 
94
- ## Checks
95
-
96
- * `bin/rails minify:census`: how much of the app runs compiled, and why the rest doesn't.
97
- * `bin/rails minify:check` (for CI; works whether or not the rest of
98
- rails-minify is on) fails on:
99
- * callbacks naming methods that don't exist;
100
- * callback chains with more than `callback_limit` (default 5) of the app's
101
- own callbacks (Rails' and gems' don't count; controllers per action).
102
- Callbacks inherited from `callback_limit_exclude` (default
103
- `["ApplicationController"]`: time zone, locale, current user, the
104
- per-request basics) don't count either. A base class over the limit is
105
- reported once, with the classes that inherit it;
106
- * model callbacks that call `save`, `update`, `touch`, ... on the record
107
- they run for, which runs the chain again;
108
- * an out-of-date manifest.
109
-
110
- ```ruby
111
- config.rails_minify.callback_limit = 8
112
- config.rails_minify.callback_limit_exclude = ["ApplicationController", "Admin::BaseController"] # [] counts everything
113
- config.rails_minify.callback_limit_mode = :warn # report, don't fail
114
- config.rails_minify.callback_loops = :warn
115
- config.rails_minify.allow_callback_loops = ["Trip#recalculate"] # guarded on purpose
116
- ```
117
-
118
- ## Things found that are app config, not patches
187
+ ## Things found that are app config
119
188
 
120
189
  * sentry-rails' `breadcrumbs_logger = [:active_support_logger]` subscribes to
121
190
  every notification (each SQL query, render, instantiation) and takes
@@ -6,20 +6,22 @@ module RailsMinify
6
6
 
7
7
  # After config/environments/*.rb, before any initializer reads the
8
8
  # minimizers' options.
9
- config.before_initialize do |app|
10
- next unless app.config.rails_minify.enabled
9
+ config.before_initialize { |app| Railtie.apply_enabled(app.config) }
11
10
 
12
- ar = app.config.ar_minimizer
11
+ # `enabled` turns on what made no page slower in production and is worth
12
+ # having for its own sake: strict mode (fewer generated methods, production
13
+ # runs what the tests run), enums and ZJIT after boot. Everything else stays
14
+ # off unless set on its own. ZJIT, like Rails' config.yjit default, not in
15
+ # development and test, and only from Ruby 4.1: 4.0's ZJIT isn't meant for
16
+ # production yet.
17
+ def self.apply_enabled(config, env: Rails.env, ruby: RUBY_VERSION)
18
+ options = config.rails_minify
19
+ return unless options.enabled
20
+
21
+ ar = config.ar_minimizer
13
22
  ar.slim = true if ar.slim.nil?
14
- ar.views = true if ar.views.nil?
15
23
  ar.enums = true if ar.enums.nil?
16
- ar.small_records = true if ar.small_records.nil?
17
- ar.load_time = app.root.join("config/ar_minimizer.json").exist? if ar.load_time.nil?
18
- as = app.config.as_minimizer
19
- as.unroll_callbacks = true if as.unroll_callbacks.nil?
20
- ap = app.config.ap_minimizer
21
- ap.route_helpers = true if ap.route_helpers.nil?
22
- ap.views = true if ap.views.nil?
24
+ options.zjit = !env.local? && Gem::Version.new(ruby) >= Gem::Version.new("4.1") if options.zjit.nil?
23
25
  end
24
26
 
25
27
  PATCHES = {
@@ -29,10 +31,10 @@ module RailsMinify
29
31
  }.freeze
30
32
 
31
33
  initializer "rails_minify.patches" do |app|
32
- options = app.config.rails_minify
33
- next unless options.enabled || options.patches
34
+ patches = app.config.rails_minify.patches
35
+ next unless patches
34
36
 
35
- Array(options.patches || PATCHES.keys).each do |name|
37
+ (patches == true ? PATCHES.keys : Array(patches)).each do |name|
36
38
  constant, hook, install = PATCHES.fetch(name.to_sym)
37
39
  if hook
38
40
  ActiveSupport.on_load(hook) { install.call }
@@ -42,6 +44,18 @@ module RailsMinify
42
44
  end
43
45
  end
44
46
 
47
+ # ZJIT from after boot, as Rails does for YJIT (config.yjit): the code
48
+ # that only runs while booting (requiring, eager loading, the minimizers'
49
+ # compiling) isn't compiled. Japan Voyage, per process: 24% fewer
50
+ # compiled methods, 18 MB less JIT memory, the same speed.
51
+ initializer "rails_minify.zjit" do |app|
52
+ config.after_initialize do
53
+ next unless app.config.rails_minify.zjit && defined?(RubyVM::ZJIT.enable) && !RubyVM::ZJIT.enabled?
54
+
55
+ RubyVM::ZJIT.enable
56
+ end
57
+ end
58
+
45
59
  rake_tasks do
46
60
  namespace :minify do
47
61
  prepare = lambda do
@@ -60,8 +74,10 @@ module RailsMinify
60
74
  sections = {
61
75
  "Active Record (activerecord-minimizer)" => ArMinimizer::Census.report(root: app.root),
62
76
  "Callbacks (activesupport-minimizer)" => AsMinimizer::Census.report(routed: routed),
63
- "Routes and views (actionpack-minimizer)" => ApMinimizer::Census.report(app.routes, app.paths["app/views"].existent),
64
77
  }
78
+ if defined?(ApMinimizer::Census)
79
+ sections["Routes and views (actionpack-minimizer)"] = ApMinimizer::Census.report(app.routes, app.paths["app/views"].existent)
80
+ end
65
81
  sections.each do |title, result|
66
82
  puts title
67
83
  result.lines(verbose: verbose).each { puts " #{it}" }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsMinify
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/rails_minify.rb CHANGED
@@ -3,22 +3,29 @@
3
3
  require "rails_minify/version"
4
4
  require "as_minimizer"
5
5
  require "ar_minimizer"
6
- require "ap_minimizer"
6
+ begin
7
+ require "ap_minimizer" # optional: gem "actionpack-minimizer"
8
+ rescue LoadError => e
9
+ raise unless e.path == "ap_minimizer"
10
+ end
7
11
 
8
- # One switch for the minimizers:
12
+ # One switch:
9
13
  #
10
14
  # # config/environments/production.rb
11
15
  # config.rails_minify.enabled = true
12
16
  #
13
17
  # turns on, unless set otherwise on their own:
14
- # config.ar_minimizer.slim, .views, .load_time (load_time only with config/ar_minimizer.json)
15
- # config.as_minimizer.unroll_callbacks
16
- # config.ap_minimizer.route_helpers, .views
18
+ # config.ar_minimizer.slim strict mode: only readers and writers per column
19
+ # config.ar_minimizer.enums enum predicates/bang methods as plain methods
20
+ # config.rails_minify.zjit ZJIT after boot
17
21
  #
18
- # and these patches, for parts of Rails and common gems that redo work on
19
- # every request (config.rails_minify.patches = [...] to choose):
22
+ # Experimental, each off unless set: the other minimizer options
23
+ # (ar_minimizer.views, .load_time, .small_records, as_minimizer.unroll_callbacks,
24
+ # ap_minimizer.*) and the patches for parts of Rails and common gems that redo
25
+ # work on every request (config.rails_minify.patches = true, or a list):
20
26
  # :action_text_plain_text ActionText::Content#to_plain_text, once per distinct HTML
21
27
  # :omniauth OmniAuth strategies skip their per-request dup off their paths
28
+ # :request_host ActionDispatch::Request#host without a substitution per call
22
29
  module RailsMinify
23
30
  module Patches
24
31
  autoload :ActionTextPlainText, "rails_minify/patches/action_text_plain_text"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-minify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sampo Kuokkanen
@@ -15,51 +15,33 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.1'
18
+ version: '0.2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.1'
26
- - !ruby/object:Gem::Dependency
27
- name: actionpack-minimizer
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '0.1'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '0.1'
25
+ version: '0.2'
40
26
  - !ruby/object:Gem::Dependency
41
27
  name: activesupport-minimizer
42
28
  requirement: !ruby/object:Gem::Requirement
43
29
  requirements:
44
30
  - - "~>"
45
31
  - !ruby/object:Gem::Version
46
- version: '0.1'
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: 0.1.1
32
+ version: '0.2'
50
33
  type: :runtime
51
34
  prerelease: false
52
35
  version_requirements: !ruby/object:Gem::Requirement
53
36
  requirements:
54
37
  - - "~>"
55
38
  - !ruby/object:Gem::Version
56
- version: '0.1'
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- version: 0.1.1
60
- description: Turns on activerecord-, activesupport- and actionpack-minimizer, and
61
- patches parts of Rails and common gems that redo work on every request (Action Text
62
- plain text, Request#host, OmniAuth strategies).
39
+ version: '0.2'
40
+ description: 'One switch for what held up in production: Active Record strict mode
41
+ (only readers and writers per column), enum methods without closures and ZJIT turned
42
+ on after boot. minify:check finds callback problems. Experimental: patches for parts
43
+ of Rails and common gems that redo work on every request, and the other minimizer
44
+ parts.'
63
45
  executables: []
64
46
  extensions: []
65
47
  extra_rdoc_files: []
@@ -98,6 +80,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
80
  requirements: []
99
81
  rubygems_version: 4.0.20
100
82
  specification_version: 4
101
- summary: 'Precompile what Rails works out at runtime: attribute methods, callback
102
- chains'
83
+ summary: Strict Active Record attribute methods, ZJIT after boot, and checks for callback
84
+ problems
103
85
  test_files: []