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 アプリケーションでは、数千ファイルを超えることも珍しくありません。
| # | ファイル数 | 一般的な実行時間 | 開発者の体感 |
|---|---|---|---|
| 1 | 100 ファイル未満 | 3〜5 秒 | ストレスなし |
| 2 | 500 ファイル前後 | 15〜30 秒 | やや遅い |
| 3 | 1000 ファイル以上 | 1〜3 分 | 著しく遅い |
| 4 | 3000 ファイル以上 | 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 つのオプションがあります。
| # | 戦略 | 説明 | 適用場面 |
|---|---|---|---|
| 1 | metadata | ファイルのメタデータで判定(デフォルト) | 通常の開発環境 |
| 2 | content | ファイルの内容で判定 | 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 秒 | - |
| 2 | 2 回目(変更なし) | 3 秒 | 98%削減 |
| 3 | 2 回目(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-assertion | 12 秒 | 型チェック必要 |
| 2 | import/no-cycle | 8 秒 | 依存関係解析 |
| 3 | @typescript-eslint/no-floating-promises | 7 秒 | 型チェック必要 |
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%削減 |
| 2 | 2 回目(変更なし) | 2 分 30 秒 | 2 秒 | 99%削減 |
| 3 | 2 回目(10 ファイル変更) | 2 分 30 秒 | 8 秒 | 95%削減 |
| 4 | 2 回目(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 の実行速度が改善されれば、開発体験が向上し、コードの品質を保ちながら、より快適に開発を進められるでしょう。
関連リンク
articleESLint が遅い時の処方箋:--cache/並列化/ルール絞り込みの実践
articleESLint の内部構造を覗く:Parser・Scope・Rule・Fixer の連携を図解
articleESLint 運用ダッシュボード:SARIF/Code Scanning で違反推移を可視化
articleESLint シェアラブル設定の設計術:単一ソースで Web/Node/React をカバー
articleESLint Flat Config 速見表:files/ignores/plugins/rules/languageOptions の書き方
articleESLint を Yarn + TypeScript + React でゼロから構築:Flat Config 完全手順(macOS)
articleLangChain × Docker 最小構成:軽量ベースイメージとマルチステージビルド
articlePython UnicodeDecodeError 撲滅作戦:エンコーディング自動判定と安全読込テク
articlePrisma で「Cannot find module '@prisma/client'」が出る時の復旧チェックリスト
articlePinia ストア間の循環参照を断つ:依存分解とイベント駆動の現場テク
articlejotai × TypeScript 型推論を極める実戦のための環境設定術
articleWebLLM とは?ブラウザだけで動くローカル推論の全体像【2025 年版】
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来