Rails アセットパイプラインのプリコンパイルについて

## はじめに
Railsではアセットファイルを配信するときにアセットパイプラインを使ってプリコンパイルする必要がありますが、実際に何が行われているかよくわかっていないまま使ってたので整理を踏まえてまとめてみます。

## 目次
1. アセットパイプラインって何？
2. プリコンパイル時に何が行われているか？
2. 実行手順
1. 補足

## 1. アセットパイプラインって何？
アセットパイプラインとは、RailsにおいてJavaScriptやCSSなどの静的ファイルを管理・最適化する仕組みです。

主な機能
- ファイルの結合（複数のJSファイルを1つに）
- 圧縮（ファイルサイズを小さく）
- フィンガープリント付与（キャッシュ対策）


## 2. 何が行われるのか？

アセットパイプラインは以下の6ステップでプリコンパイルを行っています。各ステップごとに概要をまとめます。

### 2-1. アセットの探索

まず、以下の場所からアセットファイルを探索します

- `app/assets`: アプリケーション固有のアセット
  - JavaScript、CSS、画像ファイルなど
  - デフォルトで最も優先度が高い
- `lib/assets`: ライブラリ用のアセット
  - アプリケーション固有ではないが、複数のアプリケーションで共有できるアセット
- `vendor/assets`: サードパーティのアセット
  - jQuery、Bootstrap などの外部ライブラリ
- Gem の assets ディレクトリ
  - インストールされた Gem に含まれるアセット

### 2-2. 前処理

前処理段階では、開発者が書きやすくするための記法で書かれたスクリプトをブラウザが解釈できる形式に変換します。

- `.scss`、`.sass` → Sass/SCSS プリプロセッサ
- `.ts`、`.tsx` → TypeScript コンパイラ
- `.jsx` → React JSX トランスパイラ
- `.erb` → ERB テンプレートエンジン

以下がファイルとコンパイルの対応例です

- `application.scss` は Sass によって CSS にコンパイル
- `main.ts` は TypeScript によって型安全な JavaScript にコンパイル
- `components.jsx` は JSX から純粋な JavaScript にトランスパイル
- `styles.css.erb` は ERB によって動的な CSS を生成

これらの前処理によって、開発者は可読性を保ちつつ、実行効率も上げることができます。

### 2-3. 連結

連結（Concatenation）処理では、複数のアセットファイルを1つのファイルにまとめます

- マニフェストファイル（例：`application.js`）で指定された順序に従って結合
- `require` や `require_tree` ディレクティブに基づいて依存関係を解決
- 重複の排除と依存関係の適切な順序付けを実施

```javascript
// application.js
//= require jquery
//= require_tree .
```

### 2-4. 最小化

最小化（Minification）処理では、アセットファイルのサイズを削減します

JavaScript の最小化：
- 不要な空白、改行、コメントの削除
- 変数名の短縮化
- デッドコードの除去

CSS の最小化：
- 空白、改行の削除
- セレクタの最適化
- 色コードの短縮（#ffffff → #fff）

### 2-5. 圧縮

圧縮処理では、gzip形式に圧縮を行ってファイルサイズをさらに削減します

- gzip 圧縮の適用
  - テキストベースのアセット（JS、CSS）に特に効果的
  - 平均で 70-80% のサイズ削減が可能
- ブラウザがサポートする圧縮形式の自動検出
- `config.assets.compress = true` で有効化

### 2-6. フィンガープリントの付与

フィンガープリントは、アセットの内容に基づいてユニークな識別子を生成します

- ファイル名にハッシュ値を付加
  - 例：`application-908e25f4bf641868d8683022a5b62f54.css`
- キャッシュの最適化
  - ファイルの内容が変更された場合のみ、新しいフィンガープリントを生成
  - ブラウザの効率的なキャッシュ管理が可能

(フィンガープリント付与の必要性がいまいち理解できなかったので少し深堀りました)
[フィンガープリント付与の目的は？](#フィンガープリント付与の目的)

**これらを実行した結果、最適化された静的ファイルがpublic/assetsディレクトリに生成されます**

### 実行手順

プロジェクトルートにて以下のコマンドを実行するだけです。
```
bundle exec rails assets:precompile
```

開発環境でプリコンパイルの動作確認したいときのコマンドはこちら

```
RAILS_ENV=development bundle exec rails assets:precompile
```

**プリコンパイルを実行したときに生成されるファイル例**

こんな感じのcssファイルがあったとしたら
```application.css
/*
 * This is a manifest file that'll be compiled into application.css
 *= require_tree .
 *= require_self
 */

.header {
  background-color: #f8f9fa;
  padding: 20px;
}

.main-content {
  margin: 30px auto;
  max-width: 1200px;
}

.footer {
  background-color: #333;
  color: white;
  padding: 20px;
} 
```

このような圧縮されたファイルがハッシュを振り分けられた上で生成されます

```application-[hash].css
.header{background-color:#f8f9fa;padding:20px}.main-content{margin:30px auto;max-width:1200px}.footer{background-color:#333;color:#fff;padding:20px} 
```

こんなjsファイルがあったとしたら
```application.js
//= require jquery
//= require rails-ujs
//= require_tree .

document.addEventListener('DOMContentLoaded', function() {
  console.log('Application initialized');
  
  const toggleButton = document.querySelector('.toggle-menu');
  if (toggleButton) {
    toggleButton.addEventListener('click', function() {
      document.body.classList.toggle('menu-open');
    });
  }
});

//= require jquery/dist/jquery.min.js
//= require jquery-ui
//= jquery.ui.datepicker
//= require parsley.min
```

このような圧縮されたjsファイルが生成されます。

```application-[hash].js
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t){document.addEventListener("DOMContentLoaded",(function(){console.log("Application initialized");const e=document.querySelector(".toggle-menu");e&&e.addEventListener("click",(function(){document.body.classList.toggle("menu-open")}))}))}]); 
```

このままだと元のファイルとハッシュを付与されたファイルの対応関係が分からないのでマッピング情報だけまとめたマニフェストファイルが生成されます。


**マニフェストファイルの例**
```.sprockets-manifest-[hash].json
{
  "files": {
    "application-8a7b5db5c81f8f90bf8e1bb5f5c6f749.css": {
      "logical_path": "application.css",
      "mtime": "2024-03-15T10:30:00.000Z",
      "size": 234,
      "digest": "8a7b5db5c81f8f90bf8e1bb5f5c6f749",
      "integrity": "sha256-in21tcgfj5C/jhu19fxvdJ=="
    },
    "application-2d8d3b6c8c8b1e24e2f8d1d6c7f5e4a3.js": {
      "logical_path": "application.js",
      "mtime": "2024-03-15T10:30:00.000Z",
      "size": 1562,
      "digest": "2d8d3b6c8c8b1e24e2f8d1d6c7f5e4a3",
      "integrity": "sha256-LY07bIyLHiTi+NHWx/XkozZ=="
    }
  },
  "assets": {
    "application.css": "application-8a7b5db5c81f8f90bf8e1bb5f5c6f749.css",
    "application.js": "application-2d8d3b6c8c8b1e24e2f8d1d6c7f5e4a3.js"
  }
} 
```

## 終わりに
Railsのアセットパイプラインのプリコンパイルについて、その仕組みと実際の動作をまとめました。
なんのためにするのかよく分かっていませんでしたが **開発効率の向上、パフォーマンスの最適化、保守性の向上** などの様々なメリットがあること知ることができました。

## 補足
### フィンガープリント付与の目的

フィンガープリントを付与する目的は **「ユーザー側のキャッシュにより起こる予期せぬ挙動を防ぐため」** です。

フィンガープリント付与がなかったときに問題になりうるケース

1. ユーザーがapplication.jsをダウンロードしてキャッシュ
1. 開発者がapplication.jsを更新
1. ユーザーは古いキャッシュされたapplication.jsを使用し続ける

結果として、新しい機能が動作しないことやバグが発生してしまうというわけです。Railsはこの問題をフィンガープリント付与によって解決しています。

フィンガープリント付与による解決
更新前: application-123456.js
更新後: application-789abc.js

ファイル名が変わるため、ブラウザは同じファイルとは認識せず新しいものをダウンロードして表示します。