T-CREATOR

ESLint が遅い時の処方箋:--cache/並列化/ルール絞り込みの実践

ESLint が遅い時の処方箋:--cache/並列化/ルール絞り込みの実践

プロジェクトの規模が大きくなるにつれて、ESLint の実行時間が長くなり、開発体験が悪化してしまうことはありませんか。 数秒で終わっていた Lint チェックが、気づけば数分もかかるようになり、コミット前の待ち時間がストレスになってしまいます。 本記事では、ESLint の実行速度を劇的に改善する 3 つの実践的な手法をご紹介します。

背景

ESLint の処理フロー

ESLint がコードを解析する際には、いくつかの処理ステップを踏んでいます。 このフローを理解することで、どこにボトルネックがあるのかを把握できるでしょう。

以下の図は、ESLint の基本的な処理フローを示しています。

mermaidflowchart TD
  start["ESLint 実行開始"] --> config["設定ファイル読み込み"]
  config --> files["対象ファイル収集"]
  files --> parse["ファイルごとに<br/>AST パース"]
  parse --> rules["ルール適用"]
  rules --> report["結果レポート生成"]
  report --> done["実行完了"]

このフローから分かるように、ESLint は各ファイルに対して構文解析(AST 生成)を行い、その後に全てのルールを適用していきます。 ファイル数が増えれば増えるほど、この処理が繰り返されるため、実行時間が線形に増加してしまうのです。

プロジェクト規模と実行時間の関係

TypeScript や JavaScript のプロジェクトでは、依存関係が複雑になるほどファイル数が増加します。 特にモノレポ構成や大規模な Next.js アプリケーションでは、数千ファイルを超えることも珍しくありません。

#ファイル数一般的な実行時間開発者の体感
1100 ファイル未満3〜5 秒ストレスなし
2500 ファイル前後15〜30 秒やや遅い
31000 ファイル以上1〜3 分著しく遅い
43000 ファイル以上5 分以上耐えられない

この表からも分かる通り、ファイル数が増えると ESLint の実行時間は指数関数的に増加していきます。

課題

開発体験の悪化

ESLint が遅いことで、以下のような問題が発生します。

コミット前の待ち時間

git commit の前に pre-commit フックで ESLint を実行している場合、毎回数分待たされることになります。 この待ち時間は開発のリズムを崩し、集中力を削いでしまうでしょう。

CI/CD パイプラインの遅延

継続的インテグレーション環境で ESLint を実行する場合、全体のビルド時間に大きく影響します。 デプロイまでの時間が長くなれば、フィードバックサイクルも遅くなってしまいますね。

エディタ統合時のラグ

VSCode などのエディタで ESLint をリアルタイム実行している場合、保存のたびに数秒待たされることがあります。 これは特に大規模なファイルを編集している時に顕著です。

実行時間のボトルネック

ESLint の実行時間が長くなる主な原因は 3 つあります。

mermaidflowchart LR
  slow["実行時間が遅い"] --> file_count["ファイル数の多さ"]
  slow --> no_cache["キャッシュ未使用"]
  slow --> rule_count["ルール数の多さ"]

  file_count --> parallel["★ 並列化で解決"]
  no_cache --> cache_opt["★ --cache で解決"]
  rule_count --> rule_opt["★ ルール絞り込みで解決"]

それぞれのボトルネックに対して、適切な対策を講じることで実行速度を大幅に改善できます。

解決策

--cache オプションによるキャッシュ活用

ESLint には、前回の実行結果をキャッシュする機能が備わっています。 このオプションを使うことで、変更されていないファイルの再解析をスキップできるのです。

キャッシュの仕組み

キャッシュは、各ファイルのハッシュ値と解析結果を保存します。 次回実行時にファイルが変更されていなければ、保存された結果を再利用するという仕組みですね。

mermaidflowchart TD
  start["ESLint 実行"] --> cache_check{"キャッシュ<br/>存在?"}
  cache_check -->|NO| parse["全ファイルを解析"]
  cache_check -->|YES| hash_check{"ファイル<br/>変更?"}

  hash_check -->|変更あり| parse_changed["変更ファイルのみ解析"]
  hash_check -->|変更なし| use_cache["キャッシュ結果を使用"]

  parse --> save_cache["キャッシュ保存"]
  parse_changed --> save_cache
  use_cache --> done["実行完了"]
  save_cache --> done

基本的な使い方

package.json のスクリプトに--cacheオプションを追加するだけで利用できます。

json{
  "scripts": {
    "lint": "eslint --cache ."
  }
}

このように記述することで、ESLint は.eslintcacheというファイルにキャッシュを保存します。

キャッシュファイルの場所を指定

デフォルトではプロジェクトルートにキャッシュファイルが作成されますが、場所を変更することも可能です。

json{
  "scripts": {
    "lint": "eslint --cache --cache-location .cache/.eslintcache ."
  }
}

.cacheディレクトリにキャッシュを集約することで、管理がしやすくなります。 このディレクトリは.gitignoreに追加しておきましょう。

bash# .gitignore
.cache/
.eslintcache

キャッシュ戦略の選択

ESLint 8.0 以降では、キャッシュ戦略を選択できます。

json{
  "scripts": {
    "lint": "eslint --cache --cache-strategy content ."
  }
}

--cache-strategyには 2 つのオプションがあります。

#戦略説明適用場面
1metadataファイルのメタデータで判定(デフォルト)通常の開発環境
2contentファイルの内容で判定CI 環境やタイムスタンプが信頼できない場合

並列化による高速化

ESLint は標準では単一スレッドで動作しますが、並列実行することで大幅に速度を改善できます。

eslint-parallel の導入

eslint-parallelは、複数のワーカープロセスで ESLint を並列実行するツールです。

まず、パッケージをインストールします。

bashyarn add -D eslint-parallel

次に、package.json のスクリプトを更新しましょう。

json{
  "scripts": {
    "lint": "eslint-parallel --cache ."
  }
}

これだけで、CPU のコア数に応じて並列実行されるようになります。

ワーカー数の調整

デフォルトでは CPU コア数に基づいて自動的にワーカー数が決定されますが、明示的に指定することも可能です。

json{
  "scripts": {
    "lint": "eslint-parallel --cache --max-warnings 0 --workers 4 ."
  }
}

--workersオプションで並列度を制御できます。 一般的には、CPU コア数 - 1 程度が最適でしょう。

並列化の効果

並列化による速度改善の効果を図で示します。

mermaidflowchart LR
  subgraph single["単一スレッド実行"]
    direction TB
    s1["File 1"] --> s2["File 2"]
    s2 --> s3["File 3"]
    s3 --> s4["File 4"]
  end

  subgraph parallel["並列実行 (4 ワーカー)"]
    direction TB
    p1["File 1"]
    p2["File 2"]
    p3["File 3"]
    p4["File 4"]
  end

  single -.->|"実行時間: 約 1/4 に短縮"| parallel

4 つのワーカーで並列実行すれば、理論上は実行時間が約 1/4 になります。 実際には、オーバーヘッドがあるため、完全に 1/4 にはなりませんが、それでも大幅な改善が期待できますね。

ルールの絞り込みと最適化

全ての ESLint ルールを適用すると、チェックに時間がかかります。 本当に必要なルールだけに絞り込むことで、実行速度を改善できるのです。

ルールの棚卸し

まず、現在適用されているルール数を確認しましょう。

bashyarn eslint --print-config src/index.ts | grep -c '"'

このコマンドで、特定のファイルに適用されているルール数が分かります。

重いルールの特定

ESLint には、実行時間を計測するオプションがあります。

bashTIMING=1 yarn eslint src/

この環境変数を設定して実行すると、各ルールの実行時間が表示されます。 特に時間のかかっているルールを特定できるでしょう。

extends の見直し

多くのプロジェクトでは、複数の共有設定を継承しています。

javascript// .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'next/core-web-vitals',
    'prettier',
  ],
};

このように多くの設定を継承していると、重複したルールや不要なルールが含まれている可能性があります。

本当に必要な設定だけに絞り込むことで、実行速度を改善できます。

javascript// .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'next/core-web-vitals',
  ],
  // 不要な設定は除外
  rules: {
    // プロジェクトで本当に必要なルールのみ有効化
  },
};

型チェックルールの分離

TypeScript の型情報を必要とするルールは、特に処理が重くなります。

javascript// .eslintrc.js
module.exports = {
  overrides: [
    {
      files: ['*.ts', '*.tsx'],
      parserOptions: {
        project: './tsconfig.json',
      },
      rules: {
        // 型チェックルールはTypeScriptファイルのみに適用
        '@typescript-eslint/no-floating-promises': 'error',
        '@typescript-eslint/no-misused-promises': 'error',
      },
    },
  ],
};

型チェックが必要なルールは、TypeScript ファイルにのみ適用することで、JavaScript ファイルの解析速度を改善できますね。

ignore パターンの最適化

不要なファイルを Lint 対象から除外することも重要です。

javascript// .eslintignore
node_modules/
.next/
out/
build/
dist/
*.config.js
public/
.cache/
coverage/

特にビルド成果物やライブラリコード、生成されたファイルは除外しましょう。 これらをスキャンしても、開発には役立ちませんからね。

具体例

実際のプロジェクトでの改善事例

Next.js で構築された中規模な Web アプリケーションでの改善事例をご紹介します。

プロジェクト構成

#項目内容
1フレームワークNext.js 14 (App Router)
2言語TypeScript
3ファイル数約 1,200 ファイル
4コード行数約 80,000 行
5環境MacBook Pro M1 (8 コア)

改善前の状態

最適化前は、以下のような設定で ESLint を実行していました。

json{
  "scripts": {
    "lint": "eslint ."
  }
}

シンプルな設定ですが、実行時間は約 2 分 30 秒もかかっていました。 コミット前の待ち時間としては、かなりストレスを感じる長さですね。

ステップ 1: キャッシュの導入

まず、--cacheオプションを追加します。

json{
  "scripts": {
    "lint": "eslint --cache --cache-location .cache/.eslintcache ."
  }
}

.gitignore にもキャッシュディレクトリを追加しましょう。

bash# .gitignore に追加
.cache/

この変更により、2 回目以降の実行時間が大幅に短縮されました。

#実行実行時間改善率
1初回実行2 分 30 秒-
22 回目(変更なし)3 秒98%削減
32 回目(10 ファイル変更)15 秒90%削減

ファイルを変更していない場合、ほぼ瞬時に完了するようになりました。

ステップ 2: 並列化の導入

次に、並列実行を可能にするパッケージを導入します。

bashyarn add -D eslint-parallel

package.json を更新して、並列実行を有効にします。

json{
  "scripts": {
    "lint": "eslint-parallel --cache --cache-location .cache/.eslintcache --max-warnings 0 ."
  }
}

M1 MacBook Pro は 8 コアなので、7 ワーカーで並列実行されます。

初回実行(キャッシュなし)の時間を比較してみましょう。

#実行方法実行時間改善率
1単一スレッド2 分 30 秒-
2並列実行(7 ワーカー)45 秒70%削減

並列化により、初回実行でも大幅に高速化されました。

ステップ 3: ルールの最適化

実行時間を計測して、重いルールを特定します。

bashTIMING=1 yarn lint

このコマンドの結果、以下のルールが特に時間を消費していることが分かりました。

#ルール名実行時間備考
1@typescript-eslint/no-unnecessary-type-assertion12 秒型チェック必要
2import/no-cycle8 秒依存関係解析
3@typescript-eslint/no-floating-promises7 秒型チェック必要

import​/​no-cycleは便利なルールですが、大規模プロジェクトでは非常に遅くなります。 このプロジェクトでは、循環依存のチェックをアーキテクチャレビューで行うことにし、ESLint からは除外しました。

javascript// .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'next/core-web-vitals',
  ],
  rules: {
    // 重いルールを無効化
    'import/no-cycle': 'off',
    // その他のカスタムルール
  },
};

また、.eslintignore も最適化します。

bash# .eslintignore
node_modules/
.next/
out/
build/
dist/
public/
.cache/
coverage/
**/*.config.js
**/*.d.ts

型定義ファイルやビルド設定ファイルは、Lint する必要がないため除外しました。

最終的な改善結果

3 つの最適化を組み合わせた結果、以下のような改善が得られました。

#実行パターン最適化前最適化後改善率
1初回実行(全ファイル)2 分 30 秒35 秒77%削減
22 回目(変更なし)2 分 30 秒2 秒99%削減
32 回目(10 ファイル変更)2 分 30 秒8 秒95%削減
42 回目(100 ファイル変更)2 分 30 秒18 秒88%削減

実際の開発では、全ファイルをチェックする初回実行よりも、一部のファイルだけが変更された状態での実行が圧倒的に多いでしょう。 キャッシュの効果により、日常的な開発体験が大きく改善されました。

CI 環境での設定例

CI 環境では、キャッシュの扱いが少し異なります。 GitHub Actions での設定例をご紹介しましょう。

ワークフロー定義

GitHub Actions では、キャッシュアクションを使って ESLint のキャッシュを保存・復元できます。

yamlname: Lint

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

まず、リポジトリをチェックアウトします。

次に、Node.js のセットアップを行います。

yaml- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'yarn'

Yarn のキャッシュも有効にすることで、依存関係のインストール時間も短縮できますね。

依存関係をインストールします。

yaml- name: Install dependencies
  run: yarn install --frozen-lockfile

--frozen-lockfileオプションで、yarn.lock ファイルを更新せずにインストールします。

ESLint キャッシュの復元を設定しましょう。

yaml- name: Restore ESLint cache
  uses: actions/cache@v4
  with:
    path: .cache/.eslintcache
    key: eslint-cache-${{ runner.os }}-${{ hashFiles('**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx') }}
    restore-keys: |
      eslint-cache-${{ runner.os }}-

ソースファイルのハッシュ値をキーにすることで、ファイルが変更された時だけキャッシュを更新します。

最後に、ESLint を実行します。

yaml- name: Run ESLint
  run: yarn lint

この設定により、CI 環境でもキャッシュの恩恵を受けられるようになりました。

CI 用の package.json 設定

CI 環境では、警告をエラーとして扱うことが一般的です。

json{
  "scripts": {
    "lint": "eslint-parallel --cache --cache-location .cache/.eslintcache .",
    "lint:ci": "eslint-parallel --cache --cache-location .cache/.eslintcache --max-warnings 0 ."
  }
}

lint:ciスクリプトでは、--max-warnings 0オプションを追加して、警告があればビルドを失敗させます。 これにより、コードの品質を担保できるでしょう。

ワークフローからは、このスクリプトを呼び出すように変更します。

yaml- name: Run ESLint
  run: yarn lint:ci

モノレポでの活用

モノレポ構成では、パッケージごとに ESLint を実行することで、さらに効率化できます。

ワークスペース構成

以下のようなモノレポ構成を想定します。

bashproject/
├── packages/
│   ├── app/          # Next.jsアプリ
│   ├── ui/           # UIコンポーネント
│   └── utils/        # ユーティリティ
├── package.json
└── .eslintrc.js

各パッケージにも package.json と ESLint 設定があります。

ルートの package.json

ルートの package.json では、全パッケージを並列実行する設定を行います。

json{
  "private": true,
  "workspaces": ["packages/*"],
  "scripts": {
    "lint": "yarn workspaces foreach -Ap run lint",
    "lint:changed": "yarn workspaces foreach -Ap run lint:changed"
  }
}

yarn workspaces foreachコマンドで、全ワークスペースのスクリプトを並列実行できます。

各パッケージの package.json

各パッケージでは、キャッシュと並列化を組み合わせた設定を行いましょう。

json{
  "name": "@project/app",
  "scripts": {
    "lint": "eslint-parallel --cache --cache-location ../../.cache/eslintcache-app .",
    "lint:changed": "eslint-parallel --cache --cache-location ../../.cache/eslintcache-app $(git diff --name-only --diff-filter=ACMR HEAD | grep -E '\\.(ts|tsx|js|jsx)$' | xargs)"
  }
}

lint:changedスクリプトでは、Git で変更されたファイルのみをチェックします。 これにより、プルリクエスト作成時の Lint 時間をさらに短縮できますね。

まとめ

ESLint の実行速度を改善する 3 つの手法をご紹介しました。

キャッシュの活用では、--cacheオプションを使うことで、変更されていないファイルの再解析をスキップできます。 特に、日常的な開発での 2 回目以降の実行時間を劇的に短縮できるでしょう。

並列化では、eslint-parallelを使ってマルチコア CPU の性能を最大限に引き出せます。 初回実行や CI 環境での実行時間を大幅に改善できますね。

ルールの絞り込みでは、本当に必要なルールだけを有効にすることで、無駄な処理を削減します。 重いルールを特定して、必要性を見直すことが重要です。

これら 3 つの手法を組み合わせることで、実行時間を 70〜99%削減することが可能になります。 プロジェクトの規模や構成に応じて、最適な組み合わせを見つけてみてください。

ESLint の実行速度が改善されれば、開発体験が向上し、コードの品質を保ちながら、より快適に開発を進められるでしょう。

関連リンク