Serving

Caching & ETags

Fold the import map into your ETags, and keep the map fresh while you edit JavaScript in development.

A digest of the import map in your ETag#

If you use ETags generated by Rails helpers like stale? or fresh_when, the digest of the import map has to be part of the calculation. Otherwise the application keeps answering 304 Not Modified after your JavaScript changed. stale_when_importmap_changes adds the digest to the ETag for HTML requests:

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  stale_when_importmap_changes
end

Under the hood that is Rails.application.importmap.digest(resolver: helpers), a SHA1 of the rendered import map JSON, which you can use directly in an etag { … } block if you'd rather scope it yourself.

Sweeping the cache in development and test#

The import map is cached once rendered. In development, and in test when class reloading is on, a file watcher on app/javascript and vendor/javascript clears that cache before each request when a file changed, so a new controller shows up in the map without a restart. That is config.importmap.sweep_cache, on by default in those environments; the watcher is only installed when classes are reloadable, so a test environment with enable_reloading = false (the Rails default) skips it.

An engine, or an app with JavaScript elsewhere, adds its directories to the watch list. The watcher reads the list in the importmap.cache_sweeper initializer, so an engine's initializer has to run before it:

my_engine/lib/my_engine/engine.rb
initializer "my-engine.importmap", before: "importmap.cache_sweeper" do |app|
  app.config.importmap.cache_sweepers << Engine.root.join("app/assets/javascripts")
end

config/importmap.rb itself is reloaded by a separate reloader whenever any file in config.importmap.paths changes; removing a pin still needs a restart for it to disappear from the rendered map and the preloads.

Legacy browsers such as Safari on iOS 15#

For a browser without native import map support, load es-module-shims before the import map tags:

app/views/layouts/application.html.erb
<script async src="https://ga.jspm.io/npm:[email protected]/dist/es-module-shims.js" data-turbo-track="reload"></script>
<%= javascript_importmap_tags %>