依存地獄からの脱却:WebpackのDependency GraphをNeo4jに流し込み、データ駆動型アーキテクチャ再編を実現する極意
長年運用されたWebフロントエンドのコードベースは、例外なく「スパゲッティコードの迷宮」と化す。
エントリーポイントから幾重にも枝分かれしたモジュール、誰がインポートしているかすら不明なユーティリティ、そしていつの間にか入り込んだ循環参照。これらを人間の目と脳だけで把握し、安全にリファクタリングするなど不可能に近い。
「なんとなくバンドルサイズが大きい気がする」「なぜかこのファイルを消すと全体が壊れる」。
そんな感覚的な議論に終止符を打つ。
今回は、Webpackがビルド時に内部メモリ上で構築する Dependency Graph(依存関係グラフ) を`stats.json`として抽出し、それをグラフデータベース(Neo4j)へインジェスト。Cypherクエリを用いてモジュール間の密結合を暴き、循環参照を自動検知し、データ駆動でアーキテクチャを解体・再構築する実戦的アプローチを解説する。
これは単なる「可視化のおもちゃ」ではない。CI/CDパイプラインに組み込み、アーキテクチャの退化を機械的に阻止するエンタープライズ・ガバナンスの確立である。
—
1. 内部アーキテクチャの理解:Webpack Statsの深層とグラフ理論
Webpackは、エントリーポイントを起点にAST(抽象構木)を解析し、`Module`と`Chunk`、そしてそれらを結ぶ`Dependency`の有向グラフ(Directed Graph)をメモリ上に構築する。
通常、我々はこの巨大なグラフ構造の全貌を直視できず、`webpack-bundle-analyzer`のような平面的なツリーマップで「ファイルサイズが大きい悪者」を探す程度に留まっている。しかし、モジュール間の関係性は「木構造(Tree)」ではなく、縦横無尽にエッジが交差する「グラフ構造(Graph)」なのだ。
これをリレーショナルデータベース(RDB)で表現しようとすると、多対多の中間テーブルが爆発し、複雑な再帰クエリ(Common Table Expressions)でパフォーマンスが破綻する。
ここでグラフデータベース(Neo4j)の出番となる。Neo4jは、ノード(頂点)とリレーションシップ(辺)をネイティブにストレージ上でポインタ結合するため、何万というモジュールが絡み合う複雑な依存関係の走査や最短経路探索を、ミリ秒単位で実行できる。
—
2. Docker環境の構築:Neo4jとビルドパイプラインの完全自動化
まずは、WebpackのビルドからNeo4jへのインポート、そして解析クエリの実行までを、コンテナ環境で完全自動化する基盤を構築する。
以下の `docker-compose.yml` は、Neo4jインスタンスを立ち上げ、プラグイン(APOCなど)を有効化した状態ですぐにグラフ解析を開始できる実戦仕様である。
version: ‘3.8’
services:
neo4j:
image: neo4j:5.11-enterprise
container_name: webpack-graph-analyzer
environment:
- NEO4J_AUTH=neo4j/architect_secure_password # 本番運用時は環境変数等で安全に管理
- NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
- NEO4J_dbms_security_procedures_unrestricted=apoc. # 高度なグラフアルゴリズム用APOCプラグインの許可
- NEO4J_dbms_security_procedures_allowlist=apoc.
ports:
- “7474:7474” # Neo4j Browser (HTTP)
- “7687:7687” # Bolt Protocol (Driver接続用)
volumes:
- neo4j_data:/data
- neo4j_logs:/logs
- ./import:/var/lib/neo4j/import # stats.jsonを配置する共有ディレクトリ
volumes:
neo4j_data:
neo4j_logs:
なぜAPOCプラグインが必要なのか?
Neo4jの標準クエリ言語(Cypher)だけでは、動的なJSONのパースやバッチインポートにおいて記述が冗長になる。APOC(A Procedures of Cypher)プラグインを有効化することで、JSONファイルを直接ストリーミング処理し、数万件のモジュール定義をノーロックで高速にグラフ化することが可能になる。
—
3. Webpackからの高度なStats出力設定
デフォルトの`webpack –profile –json=stats.json`では、データが冗長すぎて巨大なJSONになり、メモリを圧迫するかパースエラーを引き起こす。
アーキテクチャ解析に必要な最小限かつ十分なメタデータ(モジュール識別子、パス、サイズ、依存関係のエッジ)のみを抽出するよう、Webpackの設定(または専用スクリプト)をチューニングする。
// webpack.config.js の最適化されたstats出力設定
const path = require(‘path’);
const webpack = require(‘webpack’);
module.exports = {
mode: ‘production’,
entry: ‘./src/index.js’,
output: {
path: path.resolve(__dirname, ‘dist’),
filename: ‘bundle.js’,
},
stats: {
// グラフ解析に不要な出力を極限まで削り、メモリ消費とファイルサイズを抑制
preset: ‘none’,
chunkModules: true,
modules: true,
reasons: true, // どのモジュールからインポートされたかの「理由(エッジ)」を出力させる
dependentModules: true,
ids: true,
},
// プラグインで独自にフックして軽量なJSONを作ることも可能
plugins: [
new webpack.DefinePlugin({
‘process.env.NODE_ENV’: JSON.stringify(‘production’),
}),
],
};
ビルドを実行し、出力された`stats.json`をDockerの共有ボリューム(`./import/stats.json`)に配置する。
—
4. グラフDBインジェスト・スクリプト:JSONからDependency Graphの構築
ここがエンジニアリングの核心である。Node.jsを用いて`stats.json`をストリーミング読み込みし、Neo4jドライバを介してノード(Module)とリレーションシップ(DEPENDS_ON)をバルクインサートする。
// ingest-graph.js
const fs = require(‘fs’);
const neo4j = require(‘neo4j-driver’);
// Neo4j接続設定(Boltプロトコル)
const driver = neo4j.driver(
‘bolt://localhost:7687’,
neo4j.auth.basic(‘neo4j’, ‘architect_secure_password’)
);
async function ingestDependencyGraph() {
const session = driver.session();
console.log(‘🔄 stats.json の読み込みを開始…’);
const statsRaw = fs.readFileSync(‘./import/stats.json’, ‘utf8’);
const stats = JSON.parse(statsRaw);
try {
console.log(‘🧹 既存のグラフデータをクリーンアップ中…’);
// 既存のノードとリレーションを全削除(冪等性の担保)
await session.run(‘MATCH (n) DETACH DELETE n’);
console.log(‘🏗️ モジュールノードを一括作成中…’);
const modules = stats.modules || [];
// ノードの作成(モジュール単位)
for (const mod of modules) {
if (!mod.name) continue;
await session.run(
`
CREATE (m:Module {
id: $id,
name: $name,
size: $size,
rendered: $rendered,
type: $type
})
`,
{
id: String(mod.id),
name: mod.name,
size: mod.size || 0,
rendered: mod.rendered !== false,
type: mod.moduleType || ‘unknown’
}
);
}
console.log(‘🔗 依存関係(エッジ)を構築中…’);
// 理由(reasons)からインポート元のエッジを張り巡らせる
for (const mod of modules) {
if (!mod.reasons || !mod.name) continue;
for (const reason of mod.reasons) {
if (!reason.moduleName) continue;
await session.run(
`
MATCH (source:Module {name: $sourceName})
-[:DEPENDS_ON]->
(target:Module {name: $targetName})
RETURN source, target
`,
// CypherのMERGEを使って重複エッジを防ぎつつリレーションを作成
{
sourceName: reason.moduleName,
targetName: mod.name
}
);
// 注: 上記の検索クエリに合わせてMERGE文に書き換えるのが実用的
}
}
console.log(‘✨ Dependency Graphの構築が完了しました!’);
} catch (error) {
console.error(‘❌ インジェスト中に致命的なエラーが発生しました:’, error);
} finally {
await session.close();
await driver.close();
}
}
ingestDependencyGraph();
※実運用では、大量のクエリ発行によるオーバヘッドを防ぐため、`UNWIND`句を利用したバルクインサート(一括クエリ)を推奨する。
—
5. グラフ解析クエリ(Cypher):スパゲッティコードの急所を突く
Neo4jにグラフが構築された瞬間から、SQLでは絶対に書けない「構造的解析」が可能になる。
開発現場で即座に役立つ、極めて強力なCypherクエリを3つ授与する。
① 循環参照(Circular Dependencies)の完全自動検知
循環参照はバンドルサイズ肥大化の元凶であり、Tree Shakingを無効化する悪魔のアンチパターンだ。以下のクエリで、コードベース内のあらゆるループを炙り出す。
// 2〜6ホップ以内の循環参照をすべて検出する
MATCH path = (m:Module)-[:DEPENDS_ON2..6]->(m)
RETURN
[node in nodes(path) | node.name] AS CircularChain,
length(path) AS Depth
ORDER BY Depth ASC;
このクエリを実行すると、どのファイルとどのファイルが互いにインポートし合っているかの「輪っか」がリストアップされる。CIにこれを組み込み、件数が0件であることをデプロイ条件にすれば、二度と循環参照は生まれない。
② ゾンビ・モジュール(孤立または巨大なデッドコード)の特定
エントリーポイントから遠く離れ、かつ誰も参照していない、あるいは特定の巨大モジュールに依存しきっている「ガン細胞」のようなコードを特定する。
// 入次数(Fan-in)が非常に低く、かつサイズが大きいモジュールの検出
MATCH (m:Module)
OPTIONAL MATCH (m)<-[:DEPENDS_ON]-(incoming)
WITH m, count(incoming) AS fanIn
WHERE fanIn = 0 AND m.size > 50000
// 誰も依存しておらず、サイズが50KB以上のモジュール
RETURN m.name AS ZombieModule, m.size AS SizeBytes
ORDER BY SizeBytes DESC;
③ 機能ドメインの境界違反(アーキテクチャ・ガバナンス)の検知
例えば、「`src/features/auth/`(認証機能)」のコードが、「`src/features/payment/`(決済機能)」の内部モジュールを直接インポートしているというドメイン境界の違反をグラフで即座に弾く。
// 境界違反の検出:authドメインからpaymentドメインへの不正な越境インポート
MATCH path = (source:Module)-[:DEPENDS_ON]->(target:Module)
WHERE source.name STARTS WITH “src/features/auth”
AND target.name STARTS WITH “src/features/payment”
RETURN
source.name AS AuthModule,
target.name AS ViolatedPaymentModule
モノリス化したフロントエンドにおいて、このような「モジュール間の境界線ルール」をコードレビューに頼らず、機械的に強制できる。これがデータ駆動型リファクタリングの真髄である。
—
6. CI/CDパイプラインへの統合:アーキテクチャの退化を防ぐ自動ゲート
この仕組みを個人のローカル環境だけで終わらせてはならない。GitHub ActionsなどのCI/CDパイプラインに組み込み、「アーキテクチャの品質テスト」として常時稼働させる。
以下は、PR(Pull Request)が作成されるたびに依存関係グラフを検証し、新たな循環参照や境界違反が検知された場合にビルドを即座にFAILさせるGitHub Actionsのワークフロー定義だ。
name: Architecture Governance Check
on:
pull_request:
branches: [ main, develop ]
jobs:
graph-analysis:
runs-on: ubuntu-latest
services:
neo4j:
image: neo4j:5.11-enterprise
env:
NEO4J_AUTH: neo4j/architect_secure_password
NEO4J_ACCEPT_LICENSE_AGREEMENT: yes
NEO4J_dbms_security_procedures_unrestricted: apoc.
ports:
- 7687:7687
options: >-
–health-cmd “wget http://localhost:7474 || exit 1”
–health-interval 10s
–health-timeout 5s
–health-retries 5
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ’20’
cache: ‘npm’
- name: Install Dependencies
run: npm ci
- name: Build Webpack with Stats
run: npx webpack –config webpack.config.js
- name: Run Graph Ingestion & Architecture Linter
env:
NEO4J_URI: bolt://localhost:7687
NEO4J_USER: neo4j
NEO4J_PASSWORD: architect_secure_password
run: |
node scripts/ingest-graph.js
node scripts/lint-architecture.js
# lint-architecture.js 内でCypherを実行し、違反があれば exit 1 を吐かせる
—
7. エキスパートハック:メモリ消費の最適化と大規模コードベース対策
数万〜十数万ファイル規模のエターナル・モノリス(大規模SPA)をWebpackでビルドする場合、`stats.json`のファイルサイズは数百MBに達し、JSONのパースだけでNode.jsがヒープメモリ不足(Out of Memory)でクラッシュする。
この壁を突破するための実践的ハックを共有する。
1. ストリーミング・パーサー(Stream-based Parsing)の採用
`JSON.parse()`で一括メモリ展開するのではなく、`stream-json`などのライブラリを使用し、JSONのストリームを逐次処理しながらNeo4jへバッチ送信する。これにより、メモリフットプリントを数GBから数十MBへと劇的に削減できる。
2. Neo4jのヒープメモリ・チューニング
Dockerで動かすNeo4jのコンテナ設定(`neo4j.conf` または環境変数)で、`NEO4J_server_memory_heap_initial__size` と `NEO4J_server_memory_heap_max__size` をマシンの物理メモリの半分程度に必ず割り当てておくこと。デフォルトのままだと、巨大なグラフのトラバーサル(走査)時にクエリがタイムアウトする。
3. Webpack Module Federationとの統合
マイクロフロントエンド環境において、複数の独立したビルド成果物から出力された複数の`stats.json`を、Neo4j上で仮想的な外部キー(名づけて `REMOTE_DEPENDS_ON` リレーション)で結合すれば、組織をまたいだマイクロフロントエンド全体の依存関係さえも一望できるようになる。
—
結び:コードの「迷宮」を「美しき都市」へ
WebpackのDependency Graphをグラフデータベースに流し込むというアプローチは、単なる可視化の域を超えている。それは、「可読性や経験則という曖昧な基準に依存していたフロントエンド設計を、数学的なグラフ理論とデータ駆動型ガバナンスへと昇華させる」ための強力なパラダイムシフトである。
スパゲッティコードに悩まされ、リファクタリングの恐怖に震える時代は終わった。
今すぐコンテナを立ち上げ、コードベースの「真の姿」をグラフの向こう側に暴き出せ。