アセットパイプライン

本ガイドでは、アセットパイプライン (asset pipeline) について解説します。

このガイドの内容:

  • アセットパイプラインの概要と機能
  • アプリケーションのアセットを正しく編成する方法
  • アセットパイプラインのメリット
  • アセットパイプラインにプリプロセッサを追加する
  • アセットをgemパッケージにする

1 アセットパイプラインについて

アセットパイプライン(asset pipeline)は、JavaScriptとCSSアセットの配信を処理するためのフレームワークを提供します。これは、HTTP/2のような技術や、アセットの連結や最小化といった技術を活用することによって行われます。アプリケーションは、最終的に他のgemのアセットと自動的に結合できるようになります。

アセットパイプラインは importmap-rails gem、sprockets gem、sprockets-rails gem によって実装されており、デフォルトで有効になっています。新しいアプリケーションを作成する際に、以下のように--skip-asset-pipelineオプションを渡すとアセットパイプラインを無効にできます。

$ rails new appname --skip-asset-pipeline

本ガイドでは、CSSの処理にsprocketsを、JavaScriptの処理にimportmap-railsのみを利用するデフォルトのアセットパイプラインに重点を置いています。この2つの主な制限は、トランスパイルをサポートしていないため、BabelTypescriptSassReact JSX formatTailwindCSSといったものが使えないことです。JavaScriptやCSSのトランスパイルが必要な場合は、「別のライブラリを使う」セクションをお読みください。

1.1 主要な機能

アセットパイプラインの第1の機能は、各ファイル名にSHA256フィンガープリントを挿入し、ファイルがWebブラウザとCDNによってキャッシュされるようにすることです。このフィンガープリントは、ファイルの内容を変更すると自動的に更新され、キャッシュが無効化されます。

アセットパイプラインの第2の機能は、JavaScriptファイルの配信にimport mapsを使うことです。これにより、ESモジュール(ESM)用に作られたJavaScriptライブラリを利用する、トランスパイルやバンドリングを必要としないモダンなアプリケーションを構築できるようになり、Webpack、yarn、nodeなどのJavaScriptツールチェーンが不要になります

アセットパイプラインの第3の機能は、すべてのCSSファイルを1個のメイン.cssファイルに連結して、最小化(minify)または圧縮することです。本ガイドの後半で学ぶように、この戦略をカスタマイズして、好みの形でファイルをグループ化できます。production環境のRailsでは、各ファイル名にSHA256フィンガープリントを挿入して、ファイルがWebブラウザでキャッシュされるようにします。このフィンガープリントを変更することでキャッシュを無効にすることが可能です。フィンガープリントの変更は、ファイルの内容を変更するたびに自動的に行われます。

アセットパイプラインの第4の機能は、CSSの上位言語によるアセットコーディングを可能にすることです。

1.2 フィンガープリントと注意点

フィンガープリント(fingerprinting)は、アセットファイルの内容に応じてアセットファイル名を変更する技術です。アセットファイルの内容が少しでも変わると、アセットファイル名も必ずそれに応じて変わります。静的なコンテンツや変更頻度の低いコンテンツについては、フィンガープリントをチェックすれば内容が変更されていないかどうかを容易に確認できます。これはサーバーやデプロイ日が異なっていても有効です。

コンテンツの変更に応じてファイル名も一意に変化するようになっていれば、CDN、ISP、ネットワーク機器、Webブラウザなどあらゆる場面で有効なキャッシュをHTTPヘッダに設定できます。ファイルの内容が更新されると、フィンガープリントも必ず更新されます。これにより、リモートクライアントはコンテンツの新しいコピーをサーバーにリクエストするようになります。この手法を一般に「キャッシュ破棄(cache busting)」と呼びます。

Sprocketsがフィンガープリントを使う際には、ファイルの内容をハッシュ化したものをファイル名(通常は末尾)に追加します。たとえば、global.cssというCSSファイル名は以下のようになります。

global-908e25f4bf641868d8683022a5b62f54.css

これはRailsのアセットパイプラインの戦略として採用されています。

フィンガープリントは、development環境とproduction環境の両方でデフォルトで有効になっています。フィンガープリントは、設定のconfig.assets.digestオプションで有効または無効にできます。

1.3 import mapと注意点

import mapは、バージョンとダイジェストを持つファイルに対応する論理名を用いて、ブラウザから直接JavaScriptモジュールをインポートできます。そのため、トランスパイルやバンドリングを必要とせず、ESモジュール(ESM)用に作られたJavaScriptライブラリを用いて最新のJavaScriptアプリケーションを構築できるようになります

import mapの方法では、1個の巨大なJavaScriptファイルの代わりに、多数の小さなJavaScriptファイルを送信することになります。HTTP/2のおかげで、最初の転送時に重大なパフォーマンス上のペナルティが発生しなくなっていますし、実際、より優れたキャッシュの力学により、本質的なメリットを長期的に得られます。

2 import mapをJavaScriptアセットパイプラインとして使う

import mapは、RailsのデフォルトのJavaScriptプロセッサです。import mapを生成するロジックはimportmap-rails gemによって処理されます。

import mapはJavaScriptファイル専用であり、CSSの配信には利用できません。CSSについては、Sprocketsの利用法セクションを参照してください。

詳しい使い方はimportmap-rails gemのホームページで確認できますが、importmap-railsの基本を理解しておくことが大切です。

2.1 しくみ

import mapsは、基本的に「bare module specifiers」と呼ばれるものの文字列置換です。これにより、JavaScriptモジュールのインポート名を標準化できるようになります。

たとえば以下のインポート定義は、import mapがなければ機能しません。

import React from "react"

インポート定義を有効にするには、たとえば以下のように定義する必要があるでしょう。

import React from "https://ga.jspm.io/npm:react@17.0.2/index.js"

ここでimport mapが登場して、https://ga.jspm.io/npm:react@17.0.2/index.jsアドレスにピン留めするreact名を定義します。このような情報が提供されれば、ブラウザは簡略化されたimport React from "react"定義を受け取れるようになります。import mapは、ライブラリのソースアドレスのエイリアスのようなものと見なせます。

2.2 利用法

importmap-railsでは、ライブラリパスをpinで名前にピン留め(pinning)したimportmap設定ファイルを作成します。

# config/importmap.rb
pin "application"
pin "react", to: "https://ga.jspm.io/npm:react@17.0.2/index.js"

設定されたすべてのimport mapは、アプリケーションで<head>要素に<%= javascript_importmap_tags %>を追加することでアタッチする必要があります。javascript_importmap_tagsは、head要素で多くのスクリプトをまとめてレンダリングします。

  • import mapの設定がすべて完了しているJSON
<script type="importmap">
{
  "imports": {
    "application": "/assets/application-39f16dc3f3....js"
    "react": "https://ga.jspm.io/npm:react@17.0.2/index.js"
  }
}
</script>
  • Es-module-shims は、古いブラウザでのimport mapsサポートを保証するポリフィルとして機能します。
<script src="/assets/es-module-shims.min" async="async" data-turbo-track="reload"></script>
  • app/javascript/application.jsからのJavaScriptの読み込みのエントリポイント:
<script type="module">import "application"</script>

2.3 npmパッケージをJavaScript CDN経由で利用する

importmap-railsインストールの一部として追加される./bin/importmapコマンドを使って、import map内のnpmパッケージをpinunpin、または更新できます。binstubではCDNとしてJSPM.orgを利用しています。

このコマンドは以下のように動作します。

./bin/importmap pin react react-dom
Pinning "react" to https://ga.jspm.io/npm:react@17.0.2/index.js
Pinning "react-dom" to https://ga.jspm.io/npm:react-dom@17.0.2/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js
Pinning "scheduler" to https://ga.jspm.io/npm:scheduler@0.20.2/index.js

./bin/importmap json

{
  "imports": {
    "application": "/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js",
    "react": "https://ga.jspm.io/npm:react@17.0.2/index.js",
    "react-dom": "https://ga.jspm.io/npm:react-dom@17.0.2/index.js",
    "object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js",
    "scheduler": "https://ga.jspm.io/npm:scheduler@0.20.2/index.js"
  }
}

上のように、reactとreact-domという2つのパッケージは、jspmのデフォルトで解決すると、合計4つの依存関係に解決されます。

これで、他のモジュールと同じように、application.jsのエントリポイントでこれらを利用できるようになります。

import React from "react"
import ReactDOM from "react-dom"

pinコマンドでは、以下のようにバージョンも指定できます。

./bin/importmap pin react@17.0.1
Pinning "react" to https://ga.jspm.io/npm:react@17.0.1/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js

pinしたパッケージは、以下のようにunpinで削除できます。

./bin/importmap unpin react
Unpinning "react"
Unpinning "object-assign"

production(デフォルト)とdevelopmentでビルドが分かれているパッケージでは、以下のように--envでパッケージの環境を制御できます。

./bin/importmap pin react --env development
Pinning "react" to https://ga.jspm.io/npm:react@17.0.2/dev.index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js

また、pin実行時に、サポートされている別のCDNプロバイダー(unpkgjsdelivrなど)も指定できます。デフォルトのCDNはjspmです。

./bin/importmap pin react --from jsdelivr
Pinning "react" to https://cdn.jsdelivr.net/npm/react@17.0.2/index.js

ただし、pinをあるCDNプロバイダから別のプロバイダに切り替える場合、最初のプロバイダが追加した依存関係のうち、次のプロバイダで使われていないものを整理しなければならない場合があります。

単に./bin/importmapを実行すると、すべてのオプションが表示されます。

なお、このimportmapコマンドは、単に論理パッケージ名をCDN URLに解決するための便宜的なラッパーです。 また、CDN URLを自分で調べてpinすることも可能です。たとえば、ReactにSkypackを使いたい場合は、config/importmap.rbに以下を追加できます。

pin "react", to: "https://cdn.skypack.dev/react"

2.4 ピン留めしたモジュールをプリロードする

ウォーターフォール効果(ブラウザがネストの最も深いインポートに到達するまで次々とファイルを読み込まなければならなくなる現象)を避けるために、importmap-railsはmodulepreload linksをサポートしています。pinしたモジュールにpreload: true を追加することでプリロードできるようになります。

以下のように、アプリ内で使うライブラリやフレームワークをプリロードしておくと、早い段階でダウンロードするようブラウザに指示できます。

# config/importmap.rb
pin "@github/hotkey", to: "https://ga.jspm.io/npm:@github/hotkey@1.4.4/dist/index.js", preload: true
pin "md5", to: "https://cdn.jsdelivr.net/npm/md5@2.3.0/md5.js"

# app/views/layouts/application.html.erb
<%= javascript_importmap_tags %>

# これにより、importmapがセットアップされる前に以下のリンクがインクルードされる:
<link rel="modulepreload" href="https://ga.jspm.io/npm:@github/hotkey@1.4.4/dist/index.js">
...

最新のドキュメントについてはimportmap-railsリポジトリを参照してください。

3 Sprocketsの利用法

アプリケーションのアセットをWebで公開する素朴なアプローチは、publicフォルダのimagesstylesheetsなどのサブディレクトリにアセットを保存することでしょう。現代のWebアプリケーションは、アセットの圧縮やフィンガープリントの追加といった特定の方法で処理する必要があるため、これを手動で行うことは困難です。

Sprocketsは、設定済みディレクトリに保存されたアセットを自動的に前処理し、処理後にフィンガープリント追加、圧縮、ソースマップ生成といった設定可能な機能を使ってpublic/assetsフォルダに公開するように設計されています。

アセットを引き続きpublic階層に配置することは可能です。config.public_file_server.enabledがtrueに設定されている場合、public以下のアセットは、アプリケーションまたはWebサーバによって静的ファイルとして配信されます。配信前に何らかの前処理が必要なファイルについては、manifest.jsディレクティブを定義しておく必要があります。

Railsのproduction環境では、これらのファイルをデフォルトでpublic/assetsにプリコンパイルします。プリコンパイルされたファイルは、Webサーバで静的アセットとして配信されます。app/assetsにあるファイルそのものは、productionで直接配信されることは決してありません。

3.1 マニフェストファイルとディレクティブ

Sprocketsでアセットをコンパイルするとき、Sprocketsはどのトップレベルターゲットをコンパイルするかを決める必要があります(通常はapplication.cssと画像ファイルです)。トップレベルターゲットはSprocketsのmanifest.jsファイルで定義されます。

//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js

このファイルにはディレクティブ(directive)が含まれています。ディレクティブは、単一のCSSファイルやJavaScriptファイルをビルドするためにどのファイルが必要かをSprocketsに指示します。

上のマニフェストファイルは、./app/assets/imagesディレクトリやそのサブディレクトリにあるすべてのファイル、./app/javascriptディレクトリや./vendor/javascriptで直接JSとして認識されるすべてのファイルの内容をインクルードすることを意味しています。

このマニフェストファイルは ./app/assets/stylesheetsディレクトリにあるすべてのCSSを読み込みます(ただしサブディレクトリは含めません)。 ./app/assets/stylesheetsフォルダにapplication.cssファイルとmarketing.cssファイルがあると仮定すると、ビューに<%= stylesheet_link_tag "application" %>または<%= stylesheet_link_tag "marketing" %>と書くことでこれらのスタイルシートを読み込めるようになります。

JavaScriptファイルは、デフォルトではassetsディレクトリから読み込まれないことにお気づきでしょうか。その理由は、./app/javascriptが既にimportmap-rails gemのデフォルトのエントリポイントになっていて、vendorフォルダはダウンロードしたJSパッケージの置き場所になっているからです。

manifest.jsでは、ディレクトリ全体ではなく、特定のファイルを読み込むために linkディレクティブを指定することも可能です。linkディレクティブでは、ファイルの拡張子を明示的に指定する必要があります。

Sprocketsは、指定されたファイルを読み込んで必要に応じて処理し、1個のファイルに連結した後、(config.assets.css_compressorまたはconfig.assets.js_compressorの値に基づいて)圧縮を行います。圧縮することでファイルサイズが小さくなり、ブラウザのファイルダウンロードがより高速になります。

3.2 コントローラ固有のアセット

Railsでscaffoldやコントローラを生成すると、そのコントローラ用のCSSファイルも生成されます。scaffoldで生成する場合は、scaffolds.cssというファイルも生成されます。

たとえば、ProjectsControllerを生成すると、Railsはapp/assets/stylesheets/projects.cssというファイルも追加します。デフォルトでは、manifest.jsファイル内のlink_directoryディレクティブを使うことで、これらのファイルをアプリケーションですぐに利用可能になります。

また、以下の方法で、コントローラ固有のスタイルシートファイルを、それぞれのコントローラにのみインクルードすることも可能です。

<%= stylesheet_link_tag params[:controller] %>

ただし、この方法を使う場合は、application.cssに対してrequire_treeディレクティブを使わないでください。そうしないと、コントローラ固有のアセットが複数回インクルードされる可能性があります。

3.3 アセットの編成

パイプラインのアセットは、アプリケーション内部の3つの場所(app/assetslib/assetsvendor/assets)のいずれかに配置できます。

  • app/assets: アプリケーションが所有するアセット(カスタムの画像やスタイルシートなど)はここに配置します。

  • app/javascript: アプリケーションのJavaScriptコードはここに配置します。

  • vendor/[assets|javascript]: 外部のエンティティ(CSSフレームワークやJavaScriptライブラリなど)が所有するアセットはここに配置します。アセットパイプラインで処理される他のファイル(画像、スタイルシートなど)への参照を持つサードパーティのコードは、asset_pathなどのヘルパーを使う形に書き換える必要があることにご注意ください。

manifest.jsファイルで設定可能なその他の場所については、マニフェストファイルとディレクティブを参照してください。

3.3.1 探索パス

ファイルがマニフェストやヘルパーから参照されると、Sprocketsはmanifest.jsで指定されたすべての場所を探索してファイルを探します。探索パスは、Railsコンソールで Rails.application.config.assets.pathsを調べることで表示できます。

3.3.2 indexファイルをフォルダのプロキシとして使う

Sprocketsでは、index(および関連する拡張子)という名前のファイルを特殊な目的のために利用します。

たとえば、多数のモジュールを持つCSSライブラリがlib/assets/stylesheets/library_nameディレクトリに置かれている場合、lib/assets/stylesheets/library_name/index.cssファイルは、このライブラリ内のすべてのファイルに対するマニフェストとして機能します。このindexファイルには、必要なすべてのファイルの順序付きリストか、シンプルなrequire_treeディレクティブを含めることが可能です。

これは、/library_nameへのリクエストでpublic/library_name/index.htmlにあるファイルに到達できるのと多少似ています。つまり、インデックスファイルは直接利用できません。

ライブラリ全体としては、.cssファイルから以下のようにアクセスできます。

/* ...
*= require library_name
*/

こうすることで、関連するコードを他の場所でインクルードする前にグループ化できるようになり、設定がシンプルになってメンテナンスしやすくなります。

3.4 アセットにリンクするコードを書く

Sprocketsはアセットにアクセスするためのメソッドを特に追加しません。使い慣れているstylesheet_link_tagを引き続き使います。

<%= stylesheet_link_tag "application", media: "all" %>

Railsにデフォルトで含まれているturbo-rails gemを使う場合は、以下のようにdata-turbo-trackオプションも含めることで、アセットが更新されているかどうかをTurboがチェックし、更新されていればアセットをページに読み込むようになります。

<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>

通常のビューでは、以下のような方法でapp/assets/imagesディレクトリの画像にアクセスできます。

<%= image_tag "rails.png" %>

パイプラインが有効で、かつ現在の環境で無効になっていない場合、このファイルはSprocketsによって配信されます。ファイルがpublic/assets/rails.pngに置かれている場合、Webサーバーによって配信されます。

public/assets/rails-f90d8a84c707a8dc923fca1ca1895ae8ed0a09237f6992015fef1e11be77c023.pngなど、ファイル名にSHA256ハッシュを含むファイルへのリクエストについても同様に扱われます。ハッシュの生成法については、本ガイドのproduction環境の場合で後述します。

画像は、必要に応じてサブディレクトリで整理し、以下のようにタグでディレクトリ名を指定してアクセスすることも可能です。

<%= image_tag "icons/rails.png" %>

アセットのプリコンパイルを行っている場合(production環境の場合を参照)、存在しないアセットへのリンクを含むページを呼び出すと例外が発生します。空文字列へのリンクも同様に例外が発生します。ユーザーから提供されるデータに対してimage_tagなどのヘルパーを使う場合はご注意ください。

3.4.1 CSSとERB

アセットパイプラインは自動的にERBを評価します。たとえば、cssアセットファイルにerbという拡張子を追加すると(application.css.erbなど)、以下のようにCSS内でasset_pathなどのヘルパーが利用可能になります。

.class { background-image: url(<%= asset_path 'image.png' %>) }

ここには、参照される特定のアセットへのパスを記述します。上の例では、アセット読み込みパスのいずれかにある画像ファイル(app/assets/images/image.pngなど)が指定されたと解釈されます。この画像が既にフィンガープリント付きでpublic/assetsにあれば、このパスによる参照は有効になります。

データURIスキーム(CSSファイルにデータを直接埋め込む手法)を使いたい場合は、asset_data_uriヘルパーが利用できます。

#logo { background: url(<%= asset_data_uri 'logo.png' %>) }

上のコードは、CSSソースに正しくフォーマットされたdata URIを挿入します。

この場合、-%>でタグを閉じることはできませんのでご注意ください。

3.5 アセットが見つからない場合にエラーをraiseする

sprockets-rails 3.2.0以降を使っている場合は、アセットの探索時に何も見つからなかった場合の挙動を設定できます。以下のようにunknown_asset_fallbackfalseにすると、アセットが見つからない場合にエラーをraiseします。

config.assets.unknown_asset_fallback = false

unknown_asset_fallbacktrueにすると、エラーをraiseせずにパスを出力します。アセットのフォールバック動作はデフォルトでは無効です。

3.6 ダイジェストをオフにする

config/environments/development.rbを更新して以下を記述すると、ダイジェストをオフにできます。

config.assets.digest = false

このオプションがtrueの場合は、ダイジェストが生成されてアセットへのURLに含まれるようになります。

3.7 ソースマップをオンにする

config/environments/development.rbに以下を記述すると、ソースマップ(Source Map)を有効にできます。

config.assets.debug = true

デバッグモードを有効にすると、Sprocketsはアセットごとにソースマップを生成します。このソースマップによって、ブラウザの開発コンソールで個別のファイルをデバッグできるようになります。

アセットは、サーバー起動後に最初のリクエストを受けてコンパイルされ、キャッシュされます。 Sprocketは、以後のリクエストでコンパイルのオーバーヘッドを減らすために、Cache-Control HTTPヘッダーにmust-revalidateを設定します。ブラウザは、これらのリクエストでHTTP 304(Not Modified)レスポンスを受け取ります。

リクエストとリクエストの間にマニフェスト内のファイルが変更されると、サーバーは新たにコンパイルしたファイルを用いてレスポンスを返します。

4 production環境の場合

Sprocketsは、production環境では上述のフィンガープリントによるスキームを利用します。デフォルトでは、Railsのアセットはプリコンパイル済みかつ静的なアセットとしてWebサーバーから配信されることが前提になっています。

プリコンパイル中に、コンパイルされるファイルの内容を元にSHA256ハッシュを生成し、ディスクに保存するときにファイル名に挿入します。フィンガープリントが追加されたファイル名は、Railsヘルパーによってマニフェストファイルの代わりに使われます。

以下の例で説明します。

<%= stylesheet_link_tag "application" %>

上のコードによって以下のようなフィンガープリントが生成されます。

<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" rel="stylesheet" />

フィンガープリントの振る舞いについては、config.assets.digest初期化オプションで制御できます。デフォルトではtrueです。

通常の利用状況では、デフォルトのconfig.assets.digestオプションを変更するべきではありません。ファイル名にダイジェストがなく、期限の失効がヘッダーで遠い将来に設定されている場合、リモートクライアントはファイルの内容が変更されたときに再取得することを認識できなくなります。

4.1 アセットをプリコンパイルする

Railsには、パイプラインにあるアセットのマニフェストなどのファイルを手動でコンパイルするためのコマンドがバンドルされています。

コンパイルされたアセットは、config.assets.prefixで指定された場所に保存されます。この保存場所は、デフォルトでは/assetsディレクトリです。

デプロイ時にこのタスクをサーバー上で呼び出すと、コンパイル済みアセットをサーバー上で直接作成できます。ローカル環境でコンパイルする方法については次のセクションを参照してください。

以下がそのコマンドです。

$ RAILS_ENV=production rails assets:precompile

これにより、config.assets.prefixで指定されたフォルダがshared/assetsにリンクされます。 既にこの共有フォルダを利用している場合は、独自のデプロイ用タスクを作成する必要があります。

古いコンパイル済みアセットを参照するリモートキャッシュ済みページが、そのキャッシュ済みページの期限が切れるまで動作するには、このフォルダを複数のデプロイで共有しておくことが重要です。

常に.jsまたは.cssで終わるコンパイル済みファイル名を指定してください。

このコマンドは、.sprockets-manifest-randomhex.jsonrandomhex は16バイトのランダムな16進文字列を表す)も生成します。このJSONファイルには、すべてのアセットとそれぞれのフィンガープリントのリストが含まれます。これは、RailsヘルパーメソッドでマッピングリクエストをSprocketsに送信するのを避けるために使われます。 以下は典型的なマニフェストファイルです。

{"files":{"application-<fingerprint>.js":{"logical_path":"application.js","mtime":"2016-12-23T20:12:03-05:00","size":412383,
"digest":"<fingerprint>","integrity":"sha256-<random-string>"}},
"assets":{"application.js":"application-<fingerprint>.js"}}

実際のアプリケーションでは、マニフェストに記載されるファイルやアセットはこれよりも増え、<fingerprint><random-string>の部分も生成されます。

マニフェストのデフォルトの置き場所は、config.assets.prefixで指定された場所のルートディレクトリ)です(デフォルトは/assets)。

productionモードでプリコンパイル済みファイルが見つからない場合は、見つからないファイル名をエラーメッセージに含む例外Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledErrorが発生します。

4.1.1 遠い将来に期限が切れるヘッダー

プリコンパイル済みのアセットはファイルシステム上に置かれ、Webサーバーから直接クライアントに配信されます。これらプリコンパイル済みアセットには、いわゆる「遠い将来に失効するヘッダー(far-future headers)」はデフォルトでは含まれません。したがって、フィンガープリントのメリットを得るためには、サーバーの設定を更新してこのヘッダを含める必要があります。

Apacheの設定例:

# Expires* ディレクティブを使う場合はApacheの
# `mod_expires`モジュールを有効にする必要がある
<Location /assets/>
  # Last-Modifiedフィールドが存在する場合はETagの利用は推奨されない
  Header unset ETag
  FileETag None
  # RFCによるとキャッシュは最長1年まで
  ExpiresActive On
  ExpiresDefault "access plus 1 year"
</Location>

NGINXの設定例:

location ~ ^/assets/ {
  expires 1y;
  add_header Cache-Control public;

  add_header ETag "";
}

4.2 ローカルでプリコンパイルする

場合によっては、productionサーバーでアセットをコンパイルしたくないことがあります。たとえば、productionファイルシステムへの書き込みアクセスが制限されている場合や、アセットを変更しないデプロイが頻繁に行われる場合などが考えられます。

そのような場合は、アセットをローカルでプリコンパイルできます。つまり、production向けの最終的なコンパイル済みアセットを、production環境にデプロイする前にソースコードリポジトリに追加するということです。この方法なら、productionサーバーにデプロイするたびにproductionで別途プリコンパイルを実行する必要はありません。

以下を実行すると、production向けにプリコンパイルできます。

$ RAILS_ENV=production rails assets:precompile

ただし以下の注意点があります。

  • プリコンパイル済みのアセットが配信可能な状態になっていると、元の(コンパイルされていない)アセットと一致していなくてもプリコンパイル済みのアセットが配信されてしまいます。これはdevelopmentサーバーでも同じことが起きます

    developmentサーバーが常にアセット変更のたびにオンザフライでコンパイルし、常に最新のコードが反映されるようにするには、development環境ではproductionと異なるディレクトリにプリコンパイル済みアセットを保存する設定が必要です。そうしないと、production用のプリコンパイル済みアセットがdevelopment環境でのブラウザ表示に影響を与えてしまいます(つまりアセットを変更してもブラウザに反映されなくなります)。

    この設定は、config/environments/development.rbファイルに以下の行を追加することでできます。

    config.assets.prefix = "/dev-assets"
    
  • Capistranoなどの開発ツールで行われるアセットプリコンパイルは無効にしておく必要があります。

  • アセットの圧縮や最小化に必要なツールをdevelopment環境のシステムで利用可能にしておく必要があります。

4.3 動的コンパイル

状況によっては動的コンパイル(live compilation)を使いたいこともあります。動的コンパイルモードでは、パイプラインのアセットへのリクエストは直接Sprocketsによって扱われます。

このオプションを有効にするには以下を設定します。

config.assets.compile = true

最初のリクエストを受けると、アセットのキャッシュストアで説明したとおりにアセットがコンパイルおよびキャッシュされ、ヘルパーで使われるマニフェスト名にSHA256ハッシュが含まれるようになります。

また、SprocketsはCache-Control HTTPヘッダーmax-age=31536000に変更します。このヘッダーは、サーバーとクライアントブラウザの間にあるすべてのキャッシュ(プロキシなど)に対して「サーバーが配信するこのコンテンツは1年間キャッシュに保存してよい」と通知します。これにより、そのサーバーのアセットに対するリクエスト数を削減でき、アセットをローカルブラウザのキャッシュやその他の中間キャッシュで代替するよい機会を得られます。

このモードはデフォルトよりもメモリ消費が多くパフォーマンスも落ちるため、推奨されません。

4.4 CDN

CDN(コンテンツデリバリーネットワーク)は、全世界を対象としてアセットをキャッシュすることを主な目的として設計されています。CDNを利用すると、ブラウザからアセットをリクエストしたときに、ネットワーク上で地理的に最も「近く」にあるキャッシュのコピーが使われます。production環境のRailsサーバーから(中間キャッシュを使わずに)直接アセットを配信しているのであれば、アプリケーションとブラウザの間でCDNを利用するのがベストプラクティスです。

CDNの典型的な利用法は、productionサーバーを"origin"サーバーとして設定することです。つまり、ブラウザがCDN上のアセットをリクエストしてキャッシュが見つからない場合は、オンデマンドでサーバーからアセットファイルを取得してキャッシュします。

たとえば、Railsアプリケーションをexample.comというドメインで運用しており、mycdnsubdomain.fictional-cdn.comというCDNが設定済みであるとします。ブラウザからmycdnsubdomain.fictional-cdn.com/assets/smile.pngがリクエストされると、CDNはいったん元のサーバーのexample.com/assets/smile.pngにアクセスしてこのリクエストをキャッシュします。

CDN上の同じURLに対して次のリクエストが発生すると、キャッシュされたコピーにヒットします。CDNがアセットを直接配信可能な場合は、ブラウザからのリクエストが直接Railsサーバーに到達することはありません。CDNが配信するアセットはネットワーク上でブラウザと地理的に「近い」位置にあるので、リクエストは高速化されます。また、サーバーはアセットの送信に使う時間を節約できるので、アプリケーション本来のコードをできるだけ高速で配信することに専念できます。

4.4.1 CDNで静的なアセットを配信する

CDNを設定するには、Railsアプリケーションがインターネット上でproductionモードで運用されており、example.comなどのような一般公開されているURLでアクセス可能になっている必要があります。次に、クラウドホスティングプロバイダが提供するCDNサービスと契約を結ぶ必要もあります。その際、CDNの"origin"設定をRailsアプリケーションのWebサイトexample.comにする必要もあります。originサーバーの設定方法のドキュメントについてはプロバイダーにお問い合わせください。

利用するCDNから、アプリケーションで使うカスタムサブドメイン(例: mycdnsubdomain.fictional-cdn.com)を交付してもらう必要もあります(注: fictional-cdn.comは説明用のドメインであり、少なくとも執筆時点では本当のCDNプロバイダーではありません)。CDNサーバーの設定が終わったら、今度はブラウザに対して、Railsサーバーに直接アクセスするのではなく、CDNからアセットを取得するように通知する必要があります。これを行なうには、従来の相対パスに代えてCDNをアセットのホストサーバーとするようRailsを設定します。Railsでアセットホストを設定するには、config/environments/production.rbconfig.asset_hostを以下のように設定します。

config.asset_host = 'mycdnsubdomain.fictional-cdn.com'

ここに記述する必要があるのは「ホスト名(サブドメインとルートドメインを合わせたもの)」だけです。http://https://などのプロトコルスキームを記述する必要はありません。アセットへのリンクで使われるプロトコルスキームは、Webページヘのリクエスト発生時に、そのページへのデフォルトのアクセス方法に合わせて適切に生成されます。

この値は、以下のように環境変数でも設定できます。環境変数を使うと、stagingサーバーを実行しやすくなります。

config.asset_host = ENV['CDN_HOST']

上の設定を有効にするには、サーバーのCDN_HOST環境変数に値(この場合はmycdnsubdomain.fictional-cdn.com)を設定しておく必要があるかもしれません。

サーバーとCDNの設定が完了し、以下のアセットを持つWebページにアクセスしたとします。

<%= asset_path('smile.png') %>

この場合、http://mycdnsubdomain.fictional-cdn.com/assets/smile.pngのような完全CDN URLが生成されます(読みやすくするためダイジェスト文字は省略してあります)。

smile.pngのコピーがCDNにあれば、CDNが代わりにこのファイルをブラウザに送信します。元のサーバーはリクエストがあったことすら気づきません。ファイルのコピーがCDNにない場合は、CDNが「origin」(この場合はexample.com/assets/smile.png)を探して今後のために保存しておきます。

一部のアセットだけをCDNで配信したい場合は、アセットヘルパーのカスタム:hostオプションでconfig.action_controller.asset_hostの値セットを上書きすることも可能です。

<%= asset_path 'image.png', host: 'mycdnsubdomain.fictional-cdn.com' %>
4.4.2 CDNのキャッシュの動作をカスタマイズする

CDNは、コンテンツをキャッシュすることで動作します。CDNに保存されているコンテンツが古くなったり壊れていたりすると、メリットよりも害の方が大きくなります。本セクションでは、多くのCDNにおける一般的なキャッシュの動作について解説します。プロバイダによってはこの記述のとおりでないことがありますのでご注意ください。

4.4.2.1 CDNリクエストキャッシュ

これまでCDNがアセットをキャッシュするのに向いていると説明しましたが、実際にキャッシュされているのはアセット単体ではなくリクエスト全体です。リクエストにはアセット本体の他に各種ヘッダーも含まれています。

ヘッダーの中でもっとも重要なのはCache-Controlです。これはCDN(およびWebブラウザ)にキャッシュの取り扱い方法を通知するためのものです。たとえば、誰かが実際には存在しないアセット/assets/i-dont-exist.pngにリクエストを行い、Railsが404エラーを返したとします。このときにCache-Controlヘッダーが有効になっていると、CDNがこの404エラーページをキャッシュする可能性があります。

4.4.2.2 CDNヘッダをデバッグする

このヘッダが正しくキャッシュされているかどうかを確認する方法の1つは、curlを使う方法です。curlを使ってサーバーとCDNにそれぞれリクエストを送信し、ヘッダーが同じであるかどうかを以下のように確認できます。

$ curl -I http://www.example/assets/application-
d0e099e021c95eb0de3615fd1d8c4d83.css
HTTP/1.1 200 OK
Server: Cowboy
Date: Sun, 24 Aug 2014 20:27:50 GMT
Connection: keep-alive
Last-Modified: Thu, 08 May 2014 01:24:14 GMT
Content-Type: text/css
Cache-Control: public, max-age=2592000
Content-Length: 126560
Via: 1.1 vegur

CDNにあるコピーは以下のようになります。

$ curl -I http://mycdnsubdomain.fictional-cdn.com/application-
d0e099e021c95eb0de3615fd1d8c4d83.css
HTTP/1.1 200 OK Server: Cowboy Last-
Modified: Thu, 08 May 2014 01:24:14 GMT Content-Type: text/css
Cache-Control:
public, max-age=2592000
Via: 1.1 vegur
Content-Length: 126560
Accept-Ranges:
bytes
Date: Sun, 24 Aug 2014 20:28:45 GMT
Via: 1.1 varnish
Age: 885814
Connection: keep-alive
X-Served-By: cache-dfw1828-DFW
X-Cache: HIT
X-Cache-Hits:
68
X-Timer: S1408912125.211638212,VS0,VE0

CDNが提供するX-Cacheなどの機能やCDNが追加するヘッダなどの追加情報については、CDNのドキュメントを参照してください。

4.4.2.3 CDNとCache-Controlヘッダ

Cache-Controlヘッダーは、リクエストがキャッシュされる方法を定めたW3Cの仕様です。CDNを使わない場合は、ブラウザはこのヘッダ情報に基づいてコンテンツをキャッシュします。このヘッダのおかげで、アセットで変更が発生していない場合にブラウザがCSSやJavaScriptをリクエストのたびに再度ダウンロードせずに済むので、非常に有用です。

アセットのCache-Controlヘッダは一般に"public"にしておくものであり、RailsサーバーはCDNやブラウザに対して、そのことをこのヘッダで通知します。アセットが"public"であるということは、そのリクエストをどのキャッシュに保存してもよいということを意味します。

同様に、max-ageもこのヘッダでCDNやブラウザに通知されます。max-ageは、オブジェクトをキャッシュに保存する期間を指定します。この期間を過ぎるとキャッシュは廃棄されます。max-ageの値は秒単位で指定します。最大値は31536000であり、これは1年に相当します。

Railsでは以下の設定でこの期間を指定できます。

config.public_file_server.headers = {
  'Cache-Control' => 'public, max-age=31536000'
}

これで、production環境のアセットがアプリケーションから配信されると、キャッシュは1年間保存されます。多くのCDNはリクエストのキャッシュも保存しているので、このCache-Controlヘッダーはアセットをリクエストするすべてのブラウザ(将来登場するブラウザも含む)に渡されます。ブラウザはこのヘッダを受け取ると、次回再度リクエストが必要になったときに備えて、そのアセットを非常に長い期間キャッシュに保存してよいことを認識します。

4.4.2.4 CDNにおけるURLベースのキャッシュ無効化について

多くのCDNでは、アセットのキャッシュを完全なURLに基いて行います。たとえば以下のアセットへのリクエストがあるとします。

http://mycdnsubdomain.fictional-cdn.com/assets/smile-123.png

上のリクエストのキャッシュは、下のアセットへのリクエストのキャッシュとは完全に異なるものとして扱われます。

http://mycdnsubdomain.fictional-cdn.com/assets/smile.png

Cache-Controlmax-ageを遠い将来に設定する場合は、アセットに変更が生じた時にこれらのキャッシュが確実に無効化されるようにしてください。たとえば、ニコニコマーク画像の色を黄色から青に変更したら、サイトを訪れた人には変更後の青いニコニコマークが見えるようにしたいはずです。

RailsでCDNを併用している場合、Railsのアセットパイプライン設定config.assets.digestはデフォルトでtrueに設定されるので、アセットの内容が少しでも変更されれば必ずファイル名も変更されます。

このとき、キャッシュ内の項目を手動で削除する必要はありません。アセットファイル名が内容に応じて常に一意になるので、ユーザーは常に最新のアセットを利用できます。

5 パイプラインをカスタマイズする

5.1 CSSを圧縮する

YUIはCSS圧縮方法の1つです。YUI CSS compressorは最小化機能を提供します。

YUI圧縮は以下の記述で有効にできます。これにはyui-compressor gemが必要です。

config.assets.css_compressor = :yui

5.2 JavaScriptを圧縮する

JavaScriptの圧縮オプションには、:terser:closure:uglifier:yuiのいずれかを指定できます。それぞれ、terser gem、closure-compiler gem、uglifier gem、yui-compressor gemが必要です。

ここではterser gemを例にします。 RailsのGemfileにはデフォルトでterserが含まれています。このgemは、Node.js向けのコードをRubyでラップしたものです。terserによる圧縮は次のように行われます。ホワイトスペースとコメントを除去し、ローカル変数名を短くし、可能であればifelseを三項演算子に置き換えるなどの細かな最適化を行います。

以下の設定により、JavaScriptの圧縮にterserが使われます。

config.assets.js_compressor = :terser

terserを利用するにはExecJSをサポートするJavaScriptランタイムが必要です。macOSやWindowsを利用している場合は、OSにJavaScriptランタイムをインストールしてください。

JavaScriptの圧縮は、importmap-rails gemやjsbundling-rails gemsでアセットを読み込む場合でも有効です。

5.3 gzip圧縮されたアセットを配信する

非圧縮版のアセットに加えて、gzip圧縮されたコンパイル済みアセットもデフォルトで生成されます。gzipアセットはデータ転送を削減するのに有用です。これを指定するにはgzipフラグを設定します。

config.assets.gzip = false # gzipアセットの生成を無効にする場合

gzip形式のアセットの配信方法については、利用しているWebサーバーのドキュメントを参照してください。

5.4 独自の圧縮機能を使う

CSSやJavaScriptの圧縮設定にはあらゆるオブジェクトを渡せます。設定に与えるオブジェクトにはcompressメソッドが実装されている必要があります。このメソッドは文字列のみを引数として受け取り、圧縮結果を文字列で返す必要があります。

class Transformer
  def compress(string)
    do_something_returning_a_string(string)
  end
end

上のコードを有効にするには、application.rbの設定オプションに新しいオブジェクトを渡します。

config.assets.css_compressor = Transformer.new

5.5 アセットのパスを変更する

Sprocketsが利用するデフォルトのパブリックなパスは/assetsです。

このパスは以下のように変更可能です。

config.assets.prefix = "/他のパス"

このオプションは、アセットパイプラインを利用していない既存のプロジェクトがあり、そのプロジェクトの既存のパスを指定したり、別途新しいリソース用のパスを指定したりする場合に便利です。

5.6 X-Sendfileヘッダー

X-SendfileヘッダーはWebサーバーに対するディレクティブであり、アプリケーションからのレスポンスをブラウザに送信せずに破棄し、代わりに別のファイルをディスクから読みだしてブラウザに送信します。

このオプションはデフォルトでは無効ですが、サーバーがこのヘッダーをサポートしていれば有効にできます。このオプションをオンにすると、それらのファイル送信がWebサーバーに一任され、それによって高速化されます。 この機能の利用方法については、send_file APIドキュメントを参照してください。

ApacheとNGINXではこのオプションがサポートされており、以下のようにconfig/environments/production.rbで有効にできます。

# config.action_dispatch.x_sendfile_header = "X-Sendfile" # Apache用
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # NGINX用

既存のRailsアプリケーションをアップグレードする際にこの機能の利用を検討している場合は、このオプションの貼り付け先に十分ご注意ください。このオプションを貼り付けてよいのはproduction.rbと、production環境として振る舞わせたい他の環境ファイルだけです。application.rbではありません。

詳しくは、production環境で利用するWebサーバーのドキュメントを参照してください。 - Apache - NGINX

6 アセットのキャッシュストア

デフォルトのSprocketsは、development環境とproduction環境でtmp/cache/assetsにアセットをキャッシュします。これは以下のように変更できます。

config.assets.configure do |env|
  env.cache = ActiveSupport::Cache.lookup_store(:memory_store,
                                                { size: 32.megabytes })
end

アセットキャッシュストアを無効にするには以下のようにします。

config.assets.configure do |env|
  env.cache = ActiveSupport::Cache.lookup_store(:null_store)
end

7 アセットをGemに追加する

アセットはgemの形式で外部から持ち込むこともできます。

そのよい例はjquery-rails gemです。これは標準のJavaScriptライブラリをgemとしてRailsに提供します。このgemにはRails::Engineから継承したエンジンクラスが含まれています。このgemを導入することにより、Railsはこのgem用のディレクトリにアセットを配置可能であることを認識し、app/assetslib/assetsvendor/assetsディレクトリがSprocketsの検索パスに追加されます。

8 ライブラリやGemをプリプロセッサ化する

Sprocketsでは機能を拡張するのにProcessors、Transformers、Compressors、Exportersを使います。詳しくはSprocketsのREADME「Extending Sprockets」を参照してください。以下ではtext/css (.css)ファイルの末尾にコメントを追加するプリプロセッサを登録しています。

module AddComment
  def self.call(input)
    { data: input[:data] + "/* Hello From my sprockets extension */" }
  end
end

これで入力データを変更するモジュールができたので、続いてMIMEタイプのプリプロセッサとして登録します。

Sprockets.register_preprocessor 'text/css', AddComment

9 別のライブラリを使う

長年にわたり、アセットを処理するためのデフォルトの手法は複数ありました。Webが進化して、JavaScriptを多用するアプリケーションが増えてきました。The Rails Doctrineではメニューは"おまかせ"と考えているので、デフォルトのセットアップであるSprocketsとimport mapに重点を置きました。

私たちは、さまざまなJavaScriptフレームワークやCSSのフレームワーク、拡張機能に対して万能なソリューションが存在しないことを認識しています。Railsのエコシステムには他にもさまざまなバンドルライブラリがあり、デフォルトのセットアップでは不十分な場合に頼りにできるはずです。

9.1 jsbundling-rails

jsbundling-rails gemは、importmap-rails方式の代わりにNode.jsに依存する形を取る代替手段です。以下のいずれかをJavaScriptのバンドルに利用できます。

jsbundling-rails gemは、yarn build --watchプロセスを提供し、development環境で自動的に出力を生成します。production環境ではjavascript:buildタスクをassets:precompileタスクに自動的にフックし、パッケージの依存関係がすべてインストールされ、すべてのエントリポイントに対してJavaScriptがビルドされるようにできます。

importmap-railsの代わりに使うのがよい場合: JavaScriptコードがトランスパイルに依存している場合(例: BabelTypeScript、 React JSXフォーマット)は、jsbundling-railsが正しい方法となります。

9.2 Webpacker/Shakapacker

Webpackerは、Rails 5および6のデフォルトのJavaScriptプリプロセッサ兼バンドラでした。現在は開発が終了しています。後継としてshakapackerが存在しますが、Railsチームやプロジェクトはメンテナンスしていません。

このリストにある他のライブラリと異なり、webpacker/shakapackerはSprocketsから完全に独立していて、JavaScriptとCSSの両方のファイルを処理できます。詳しくはWebpackerガイドを参照してください。

jsbundling-railswebpacker/shakapackerの違いについては、Webpackerとの比較ドキュメントをお読みください。

9.3 cssbundling-rails

cssbundling-rails gemは、以下のいずれかを利用するCSSをバンドルおよび処理して、アセットパイプライン経由でCSSを配信します。

cssbundling-railsの動作はjsbundling-railsと似ています。development環境ではyarn build:css --watchプロセスでスタイルシートを再生成し、production環境ではassets:precompileタスクにフックしてアプリケーションにNode.js依存性を追加します。

Sprocketsとの違い: Sprockets単体ではSassをCSSにトランスパイルできないため、.sassファイルから.cssファイルを生成するためにNode.jsが必要です。.cssファイルが生成されれば、Sprocketsからクライアントに配信できるようになります。

cssbundling-railsはCSSの処理をNode.jsに依存しています。 dartsass-rails gemとtailwindcss-rails gemは、それぞれTailwind CSSとDart Sassのスタンドアロン版実行ファイルを使うので、Node.jsに依存しません。 JavaScriptをimportmap-railsで処理し、CSSをdartsass-railsまたはtailwindcss-railsで処理する形にすれば、Node依存を完全に避けられるので、よりシンプルなソリューションとなります。

9.4 dartsass-rails

アプリケーションで Sassを使いたい場合は、レガシーなsassc-rails gemの代わりにこのdartsass-rails gemが提供されています。 dartsass-rails gemは、sassc-rails gemで使われていたLibSass(2020年に非推奨化)に代えてDart Sassの実装を利用しています。

この新しいdartsass-rails gemはsassc-railsとは異なり、Sprocketsと直接統合されているわけではありません。インストールや移行の手順については、dartsass-rails gemのドキュメントを参照してください。

以前広く使われていたsassc-rails gemは、2020年に非推奨化されました。

9.5 tailwindcss-rails

tailwindcss-rails gemは、Tailwind CSS v3フレームワークのスタンドアロン実行可能版をラップしています。新しいアプリケーションを開発する際に、rails newコマンドに --css tailwindを指定することで利用できます。development環境では、Tailwindの出力を自動的に生成するためのwatchプロセスが提供されます。production環境では、assets:precompileタスクにフックします。

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加