Node.jsの開発において、グローバル変数の管理はアプリケーションの安定性とパフォーマンスに直結する極めて重要な要素です。
2026年現在のモダンなJavaScript環境では、実行環境に依存しない共通のグローバルオブジェクトとしてglobalThisの利用が標準化されています。
しかし、利便性の代償として、不適切なグローバル変数の使用はメモリリークや予期せぬサイドエフェクトを引き起こすリスクを孕んでいます。
本記事では、Node.jsにおけるグローバル変数の正しい定義方法から、globalThisの具体的な活用シーン、そして大規模開発で懸念されるメモリリークを防ぐための設計思想までを詳しく解説します。
Node.jsにおけるグローバル変数の基本概念
Node.jsにおけるグローバル変数は、アプリケーション内のどのモジュールからでもアクセス可能な特殊な変数を指します。
ブラウザ環境におけるwindowオブジェクトと同様に、Node.jsには伝統的にglobalというオブジェクトが存在してきました。
Node.jsのモジュールシステム(CommonJSやES Modules)では、ファイルごとにスコープが独立しているため、通常定義した変数はそのファイル内でのみ有効です。
しかし、特定のデータをアプリケーション全体で共有したい場合には、グローバルスコープを利用することになります。
歴史的にNode.jsではglobalオブジェクトにプロパティを追加することで、どこからでも参照できる変数を定義してきました。
ただし、グローバル変数の多用はコードの可読性を著しく低下させ、デバッグを困難にする要因となります。
そのため、現代の開発では「何でもグローバルに入れる」のではなく、適切な設計に基づいて利用を制限することが求められます。
モジュールスコープとグローバルスコープの違い
Node.jsの各ファイルは独立した「モジュール」として扱われます。
例えば、ファイル内でconst x = 10;と定義しても、それは他のファイルから直接参照することはできません。
一方で、グローバルスコープに定義された変数は、明示的なインポート処理を行わずに利用可能です。
この特性は便利ですが、モジュール間の依存関係を不明瞭にするというデメリットも持ち合わせています。
globalThisの導入とその意義
JavaScriptの実行環境は、ブラウザ、Node.js、Deno、Bun、さらにはEdge Runtimeなど多岐にわたります。
以前は、ブラウザではwindow、Node.jsではglobalといった具合に、グローバルオブジェクトへのアクセス方法が環境ごとに異なっていました。
この差異を解消するために導入されたのが、共通のインターフェースであるglobalThisです。
globalThisを使用することで、環境を問わず同じコードでグローバルオブジェクトにアクセスできるようになりました。
2026年時点のプロジェクトにおいては、従来のglobalではなく、globalThisを使用することが推奨されます。
globalThisを使用した変数の定義例
実際にglobalThisを使用してグローバル変数を定義する方法を見てみましょう。
// global_example.js
// グローバル変数にアプリケーションの開始時間を記録する
globalThis.appStartTime = Date.now();
// 関数内でグローバル変数を参照する
function printStartTime() {
console.log(`Application started at: ${globalThis.appStartTime}`);
}
printStartTime();
Application started at: 1715345600000 (実行時のタイムスタンプが表示されます)
このように、globalThisにプロパティを生やすことで、どこからでもアクセス可能な状態を作ることができます。
ただし、この手法は後述する「名前の競合」のリスクを高めるため、慎重に行う必要があります。
グローバル変数が引き起こすメモリリークのメカニズム
Node.jsアプリケーション、特に長時間稼働するサーバーサイドプログラムにおいて、メモリリークは致命的な問題となります。
グローバル変数は、アプリケーションが終了するまでメモリ上に保持され続ける特性を持っています。
JavaScriptのガベージコレクション(GC)は、「どこからも参照されていないオブジェクト」をメモリから解放します。
しかし、グローバル変数は「GCルート」と呼ばれるルートオブジェクトから直接参照されているため、自動的に解放されることはありません。
もしグローバルな配列やオブジェクトに、リクエストごとのデータなどを不用意に追加し続けると、メモリ使用量は右肩上がりに増加します。
メモリリークが発生するアンチパターンの例
以下のようなコードは、典型的なメモリリークの原因となります。
// ユーザーのアクセスログをグローバルな配列に保存し続ける例
globalThis.accessLogs = [];
function handleRequest(req) {
// リクエストが来るたびに配列へ追加されるが、削除されることはない
globalThis.accessLogs.push({
timestamp: Date.now(),
url: req.url,
data: new Array(1000).fill('leak') // 大きなデータ
});
}
このコードでは、accessLogs配列が無限に肥大化し、最終的にFATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memoryというエラーでプロセスが停止します。
メモリリークを防ぐための設計指針
グローバル変数によるメモリリークを防ぐためには、状態の保持期間を明確に設計することが不可欠です。
基本的には、「グローバル変数を状態保持のために使用しない」という原則を守ることが最も効果的です。
どうしても広域でデータを共有する必要がある場合は、以下の手法を検討してください。
1. シングルトンパターンによるモジュール管理
Node.jsのモジュールは最初にロードされた際にキャッシュされます。
これを利用し、globalThisではなく専用のモジュール内でインスタンスを管理することで、スコープを限定しつつ共有データを作成できます。
// stateManager.js
class StateManager {
constructor() {
this.data = new Map();
}
set(key, value) { this.data.set(key, value); }
get(key) { return this.data.get(key); }
}
// モジュール自体をインスタンス化してエクスポート
export const stateManager = new StateManager();
この方法であれば、どのモジュールがそのデータを使用しているかがimport文によって明確になります。
2. データの明示的な破棄
グローバルな領域に一時的にデータを置く場合は、役割が終わった段階で必ずnullを代入するか、プロパティを削除してください。
delete globalThis.someHeavyData;を実行することで、GCがそのデータを回収可能な対象として認識できるようになります。
AsyncLocalStorageによるコンテキスト管理
Node.jsでの開発、特にWebサーバー開発において、「リクエストごとにグローバルなように扱える変数」が必要になることがあります。
例えば、リクエストIDやログインユーザー情報を、関数間の引数渡しなしでどこでも参照したい場合です。
これを実現するためにglobalThisを使うのは、複数のリクエストが並行して走るNode.jsでは非常に危険です(データが混ざるため)。
そこで活用すべきなのが、Node.js標準のAsyncLocalStorageクラスです。
AsyncLocalStorageの活用コード例
AsyncLocalStorageを使うと、非同期処理のスタックを越えて特定の値を保持できます。
import { AsyncLocalStorage } from 'node:async_hooks';
const requestContext = new AsyncLocalStorage();
function logWithId(message) {
const store = requestContext.getStore();
const requestId = store ? store.requestId : 'N/A';
console.log(`[RequestID: ${requestId}] ${message}`);
}
// ダミーのリクエスト処理
function handleRequest(reqId) {
requestContext.run({ requestId: reqId }, () => {
// このスコープ内では、引数として渡さなくてもreqIdを参照できる
logWithId('Processing started');
setTimeout(() => {
logWithId('Processing completed');
}, 100);
});
}
handleRequest('abc-123');
handleRequest('xyz-789');
[RequestID: abc-123] Processing started
[RequestID: xyz-789] Processing started
[RequestID: abc-123] Processing completed
[RequestID: xyz-789] Processing completed
このように、AsyncLocalStorageは「特定のリクエスト期間中だけ有効なグローバル変数」のような振る舞いを、安全に実現します。
グローバル変数使用時のセキュリティとパフォーマンス
グローバル変数の使用は、セキュリティの観点からも注意が必要です。
外部から入力された値を不用意にグローバルオブジェクトにセットすると、プロトタイプ汚染(Prototype Pollution)などの攻撃の足がかりになるリスクがあります。
また、V8エンジンにおいて、グローバル変数のルックアップはローカル変数の参照よりもわずかにオーバーヘッドが発生する場合があります。
現代のエンジンは高度に最適化されていますが、頻繁に実行されるループ内でグローバル変数を参照し続けることは避けるべきです。
パフォーマンス向上のためのローカル変数への退避
もしグローバルな設定値を大量の計算ループ内で使用する場合、一度ローカル変数にコピーすることで最適化を助けることができます。
const config = globalThis.heavyConfig; // 一度ローカルに参照を持つ
for (let i = 0; i < 1000000; i++) {
// configを使用した計算
}
2026年におけるグローバル変数管理のベストプラクティス
これまでの内容を踏まえ、現代のNode.js開発におけるグローバル変数の扱いをテーブル形式でまとめました。
| 項目 | 推奨されるアクション | 避けるべきアクション |
|---|---|---|
| アクセス方法 | globalThisを使用する | globalやwindowに依存する |
| 設定値の共有 | 環境変数(process.env)や定数モジュール | 実行時にglobalThisへ値を直接代入 |
| リクエストコンテキスト | AsyncLocalStorageを活用 | リクエストデータをグローバル配列で保持 |
| メモリ管理 | 不要になったら参照を解除(null) | 巨大なオブジェクトを無期限に保持 |
特に、「不変な定数はグローバルでも許容されるが、可変な状態はグローバルに置かない」というルールを徹底することが重要です。
アプリケーションの設定情報などは、起動時に一度だけ読み込み、その後は読み取り専用として扱う設計にしてください。
よくある質問(FAQ)
Q1: globalとglobalThisに違いはありますか?
Node.js環境においては、基本的に同じオブジェクトを指します。
しかし、globalThisは言語標準の仕様であるため、フロントエンドと共通のライブラリを作成する場合などに互換性を保つことができます。
Q2: 既存のライブラリがglobalを汚染している場合はどうすればいいですか?
古いライブラリの中には、読み込むだけでグローバルスコープを書き換えるものが存在します。
そのような場合は、ライブラリの読み込みを特定の初期化フェーズに集約し、汚染範囲を最小限に留めるようカプセル化を検討してください。
Q3: グローバル変数はいつ使うのが正解ですか?
「それがないとアプリケーション全体が動作しないほど根幹に関わるもの」であり、かつ「実行中に値が変更されないもの」に限定すべきです。
ロガーのインスタンスや、プラットフォーム固有のpolyfillなどがその候補に挙がります。
まとめ
Node.jsにおけるグローバル変数の扱いは、アプリケーションの堅牢性を左右する大きな要因です。
globalThisという標準的な手段が確立された現代では、環境に依存しない一貫したコード記述が可能になりました。
しかし、グローバル変数の本質的なリスクである「メモリリーク」と「設計の不透明化」は依然として存在します。
状態の管理にはシングルトンパターンやAsyncLocalStorageを優先し、グローバル変数は真に必要な場合にのみ、細心の注意を払って使用するようにしましょう。
適切なスコープ管理とメモリ解放の習慣を身につけることで、高負荷な本番環境でも安定して動作するNode.jsアプリケーションを構築できるはずです。
