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.
Rails determines which groups to require by calling Rails.groups
. By default, that’s the :default
group (anything ungrouped), the environment group coming from Rails.env
, and anything added to the RAILS_GROUPS
environment variable. With RAILS_GROUPS
we can add the assets
group to our development and testing environments.
If you are building a Docker image, set RAILS_GROUPS
to assets
with ARG
during build (while avoiding it in the final image):
ARG RAILS_GROUPS="assets"
Once that’s done, let’s also instruct Bundler to load the configuration to set the right groups for the task at hand:
$ export RAILS_ENV=production
$ export RAILS_GROUPS=assets
$ bundle config set --local without development:test
$ rails assets:precompile
The assets:precompile
task needs to include assets, but a production start later doesn’t:
$ export RAILS_ENV=production
$ export RAILS_GROUPS=
$ 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 new assets group 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. That saves memory and removes a possible attack vector. Not bad.
Get Test Driving Rails while it's in prerelease.