Removing assets dependencies from Rails applications for runtime

Rails provides a smooth assets:precompile task to prepare application assets but keeps all required gems for assets generation as a standard part of the generated Gemfile. Let’s see if we can avoid these dependencies for runtime.

A new Rails application comes with various gems concerning assets compilation and minification:

$ cat Gemfile
...
# Use SCSS for stylesheets
gem 'sass-rails', '>= 6'
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
gem 'webpacker', '~> 5.0'

We might see other gems in older versions of Rails, like uglifier or coffee-rails.

It makes sense since the Rails’s assets:precompile tasks is usually run within the PRODUCTION environment, where the CSS concerns are defined:

$ cat config/environment/production.rb
...
  # Compress CSS using a preprocessor.
  # config.assets.css_compressor = :sass

  # Do not fallback to assets pipeline if a precompiled asset is missed.
  config.assets.compile = false

Applications without Webpacker might configure a JavaScript processor for the Asset Pipeline:

  # example of older applications
  config.assets.css_compressor = :sass
  config.assets.js_compressor = :uglifier

All of this works but includes extra gems and might have implications about system dependencies. For example, both webpacker and uglifier would need a JavaScript runtime like Node.js for running assets:precompile and possibly for starting the Rails server (Webpacker shouldn’t complain, but uglifier/execjs would).

But what if we want to handle assets outside the Rails application’s deployment, thus removing these dependencies? What if we’re going to build an optimized Docker container using the multi-stage build and not provide Node.js in the final image?

Well, we can do something we already do with development and test dependencies – omit these gems for production. We can move them into a new assets group in the Gemfile:

gem 'webpacker', '~> 5.0'

group :assets do
  gem 'sass-rails', '>= 6'
  ...
end

# or
group :assets do
  gem 'sass-rails', '>= 6'
  gem 'uglifier'
  ...
end

We could be ommiting Webpacker for production only if we don’t depend on javascript_pack_tag and other related helpers. But Webpacker doesn’t break if Node.js is not found, so it’s not an issue.

Then we can rely on Bundler configuration to set the right groups for the task at hand:

$ export RAILS_ENV=production
$ bundle config set --local without development:test
$ rails assets:precompile
$ bundle config set --local without development:test:assets
$ rails s

Setting the without option won’t load these assets gems but will fail whenever you try to use them in the configuration directly. This shouldn’t be an issue for a brand new Rails 6.1 application with Webpacker, but if your js_compressor is set to :uglifier, then omitting the gem ends up not starting the Rails server:

/home/rails-user/.rubies/ruby-2.6.5/lib/ruby/gems/2.6.0/gems/execjs-2.8.1/lib/execjs/runtimes.rb:58:in `autodetect': Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes. (ExecJS::RuntimeUnavailable)

That happens because uglifier relies on execjs which tries to autodetect a JavaScript processor. With a RubyGems’ Gem.loaded_specs, we can check if we are loading a specific gem and not set these configuration options:

...
  if Gem.loaded_specs.has_key?('uglifier')
    config.assets.js_compressor = :uglifier
  end
...

Now the uglifier is used only if we are loading it – and we only load it while running assets:precompile.

With the assets group and possibly updating the production.rb configuration, we could omit certain gems while running the Rails application server and leave out Node.js from the production server or a final container image.

← IT'S OUT NOW

I wrote a complete guide on web application deployment. Ruby with Puma, Python with Gunicorn, NGINX, PostgreSQL, Redis, networking, processes, systemd, backups, and all your usual suspects.

More →