Redmine 4.1 では下記のとおり sprockets が削除された。

sprockets によるアセットの gzip 自動圧縮がされなくなった。 gzipアセットはデータ転送の削減するが、この恩恵が得られなくなる。 すなわちギガを消費するようになってしまう。

であるならば、 sprockets を再導入すればいい訳だが、 消された理由からは、sprockets の再導入は難しそうであった。

経緯を確認してみると、 sprockets 4.0.0 がインストールされると Sprockets::Railtie::ManifestNeededError が発生しRedmineが起動しない問題に対して、sprockets 3.7 系で固定化する暫定対処をしていた。 (Defect #32300: Don’t use sprockets 4.0.0 in order to avoid Sprockets::Railtie::ManifestNeededError - Redmine) これに対する正式対応が sprockets を使わなくするということになった模様だ。 なお、 sprockets を自力で有効する場合についての情報は不明であった。 もっとも sprockets はかなり複雑であり、ただ単に gzip したいという要求にはそもそも向かない。

I don’t know if disabling/removing sprockets this way in the Redmine core makes enabling of the Rails asset pipeline locally more difficult than it currently is?

Sorry, I don’t know.

ところで、gzip アセットを配信する仕組みを調べてみると、actionpack の action_dispatch/middleware/static.rbActionDispatch::FileHandler で処理されていることがわかるが。 興味深いことに、ただ単に precompiled gzip ファイルさえ置いてしまえば動作するようである。

というわけでバッチでも何でも良いので、public フォルダ配下のファイルについて gzip ファイルを作成してしまえば良い。

ただsprocketsのこの仕組みはversion 3.5頃でもコロコロと変わったことがあるらしく、その頃の先人たちの記録を見ると rake task で対処している例が見つかった。

ところで、heroku でもアセットの precompile を行ってくれるのだが、 sprockets が無くとも assets:precompile task を捏造さえすれば Heroku の Slug Compiler (Google Cloud Build の heroku 版だと思えば良い) の compile phase でビルドが動くことが判明した。

As a final task in the compilation phase, the assets:precompile Rake task is executed if you have a assets:precompile Rake task defined, and don’t have a public/assets/manifest-*.json file.

…という経緯を経て、結局のところ、 gzip 実行する assets:precompile を作れば汎用的な解決策になるだろうということで、 結果下記のようなタスクを追加するに至った。

namespace :assets do
  desc "Gzip all the HTML/CSS/JS files in the public folder"
  task :precompile do
    require 'zlib'

    begin
      Rake::Task['redmine:plugins:assets'].invoke
    rescue => error
      puts "failed to execute redmine:plugins:assets task: #{error}"
    end

    Dir.glob(File.join(Rails.root, 'public', '**', '*.{html,css,js}')).each do |path|
      File.delete("#{path}.gz") if File.exist?("#{path}.gz")
      Zlib::GzipWriter.open("#{path}.gz", Zlib::BEST_COMPRESSION) do |gz|
        gz.mtime = File.mtime(path)
        gz.orig_name = File.basename(path)
        gz.puts File.open(path, 'rb') {|f| f.read }
      end
    end
  end

ポイントは redmine:plugins:assets タスクを事前に実行しておいて、 プラグインのアセットファイルも public フォルダにコピーをしてから、 gzip 処理を施すことだ。そうしないとプラグインのアセットファイルが圧縮されない。

なお zopfli などを使えばもっと圧縮率を上げれるだろうがそこまではやっていない。 そこまでやるなら brotli 使うぐらいの対処を施したいものである。