はじめに
フロントエンド開発をしていると、Node.jsを使う場面はかなり多いと感じます。
ただ実際には、
- npmを使うためのもの
- ビルドツールを動かすための裏方
- JavaScriptが動く環境らしい何か
といった、少し曖昧な理解のまま使っている場面が、僕は多くありました。
普段使うが対して理解を深めずに(もしくは深めずともなんとなくで扱っていけてた部分)を、
この記事では実務寄りの観点で改めて整理し、理解を深めていきます。
この記事の目的
この記事ではNode.jsを、
- どんな前提で設計されているのか
- どういう処理に向いているのか
- 無理が出やすい場所、不向きはどこか
という少し実務寄りの観点で改めて整理し、
Node.jsの前提の判断軸を整理できることをゴールにしています。
対象は、
- フロントエンドエンジニアとして日常的に開発している方
- npm/Vite/Webpack/TypeScriptなどを日常的に使っている方
- Node.jsを「雰囲気」から、もう少し構造的に理解したい方
- ブラウザJavaScriptとNode.jsの違いを整理したい方
としています。
目次
- 前提
- 何をしている実行環境なのか
- 中身をざっくり俯瞰
- 非同期処理とイベントループの考え方
- シングルスレッドの前提と影響
- ブラウザJavaScriptとNode.jsの違い
- なぜサーバーやツールに使われやすいのか
- まとめ
※ WorkerThreadsやスケーリング設計などは次回とします。
1. 前提
Node.jsを理解するうえで意識しておきたいのが、
JavaScriptを「誰が」「どこで」実行しているか
という視点。
JavaScriptという言語仕様と、それを実際に動かす実行環境は別物という理解がまず必要になります。
JavaScriptは言語としてのルールや文法を定めていますが、そのコードをどのように解釈し、動かすかは実行環境に依存していますね。
ブラウザのJavaScript環境とNode.jsは異なる実行環境であり、
それぞれ提供するAPIや動作の前提が違うため、
Node.jsを理解するときは「言語の話」と「実行環境の話」を分けて考えます。
フロントエンドではブラウザ環境を前提にJavaScriptを扱うことが多いかと思いますので、
Node.jsを使う際にDOMやwindowオブジェクトが存在しないことに戸惑います。
この違いは、後続の章で扱うAPIや非同期処理、OS操作などの理解に影響するため、意識していきましょう。
2. 何をしている実行環境なのか
Node.jsはJavaScriptを実行できますが、
JavaScriptを自前で解釈しているわけではなく、
内部でV8というJavaScriptエンジンを使っています。
V8はGoogleが開発したJavaScriptエンジンで、
Chromeなどのブラウザでも使われているものですね。
| 役割 | 担当 |
|---|---|
| JavaScriptの実行 | V8 |
| 実行環境・API提供 | Node.js |
Node.jsは、JavaScriptエンジンそのものではなく
JavaScriptエンジンを組み込んで、イベントループやOS操作を持たせた便利な実行環境
で捉えると分かりやすいです。
JSがブラウザだけでなく、サーバーサイドでも動くようになっているのはこの影響ですね。
3. Node.jsの中身をざっくり俯瞰
Node.jsの内部は複雑ですが、役割としては大きく下記の3つに分けられます。
| 役割 | 何をしているか |
|---|---|
| JavaScript実行 | V8がコードを実行する |
| 非同期処理管理 | イベントループ/libuv |
| OS操作 | fs/http/processなどのAPI |
V8によるJavaScript実行
JavaScriptのパース、実行、メモリ管理やGCはすべてV8が担当しています。
Node.jsの挙動がブラウザと似ているのはこの影響が大きいですね。
V8は、JavaScriptコードをそのまま逐次実行しているわけではありません。
内部では、コードのパースや最適化を行いながら、必要に応じて機械語へ変換して実行しています。
この仕組みのおかげでJavaScriptはスクリプト言語でありながら、比較的高速に動作します。
Node.jsがサーバーサイドやツール用途でも実用的な性能を持つ理由のひとつが、V8の存在。
一方で、V8が担っているのはあくまで「JavaScriptの実行」まで。
ファイル操作やネットワーク通信といった処理自体は、V8の外側でNode.jsやOSが担当することになります。
「どこまでがJavaScriptエンジンの仕事で、どこからがNode.jsの仕事なのか」
を理解することが大切です。
イベントループとlibuv
Node.jsの特徴は、イベントループを中心とした非同期処理モデルです。
libuvはNode.jsが使用する非同期I/Oライブラリで、イベントループの実装を担当しています。
処理の流れは下記。
- JavaScriptの実行自体は1スレッド
- I/O処理(ファイル読み書き、ネットワーク通信など)は、libuvがOSの非同期I/O機能(epoll、kqueueなど)を優先的に利用し、それが使えない場合はスレッドプールを使って裏側で進める
- 完了したらイベントループにコールバックを戻す
この仕組みによって、I/O待ちの間も他の処理を進めやすくなっています。
console.log('1: 開始');
setTimeout(() => {
console.log('3: setTimeoutのコールバック');
}, 0);
Promise.resolve().then(() => {
console.log('2: Promiseのコールバック');
});
console.log('1: 終了');
// 出力順序:
// 1: 開始
// 1: 終了
// 2: Promiseのコールバック
// 3: setTimeoutのコールバック
OS操作のためのAPI
Node.jsにはfs(ファイル操作)、http/net(通信)、process(プロセス情報)、stream(データの流れ)などのAPIが用意されています。
これらによって、JavaScriptからOS寄りの処理を扱えるようになっています、非常に便利ですね。
これらのAPIは、JavaScriptから直接OSを操作しているように見えますが、
実際にはNode.jsが内部でOSの機能を呼び出し、その結果をJavaScript側に橋渡ししています。
JavaScriptのコード自体がOSと直接やり取りしているわけではありません。
Node.jsが提供するAPIを通してOS操作を行っています。
この設計によって、
ファイル操作、ネットワーク通信、プロセス情報の取得
といった処理を、JavaScriptの文法のまま扱えるようになっています。
ただしこれらの処理はOSリソースを使用するため、
処理の仕方によってはイベントループ全体に影響を与える点は注意しましょう。
4. 非同期処理とイベントループの考え方
Node.jsはイベントループとlibuvによって非同期処理を行なっており、
async/awaitやPromiseを使うことで、コード自体も非常に直感的に書けるのも特徴です。
const fs = require('fs').promises;
async function readFile() {
const data = await fs.readFile('example.txt', 'utf8');
console.log(data);
}
readFile();
同期処理と非同期処理の違いは下記。
// 同期処理
const fs = require('fs');
const data1 = fs.readFileSync('file1.txt', 'utf8');
const data2 = fs.readFileSync('file2.txt', 'utf8');
console.log('両方のファイルを読み込み完了');
// 非同期処理
const fs = require('fs').promises;
async function readFiles() {
const [data1, data2] = await Promise.all([
fs.readFile('file1.txt', 'utf8'),
fs.readFile('file2.txt', 'utf8')
]);
console.log('両方のファイルを読み込み完了');
}
readFiles();
ただし内部的には下記の前提があります。
- JavaScriptの処理は基本的に順番に実行される(シングルスレッド)
- 重い処理があると全体に影響する
- I/O処理自体は並行して実行されるが(libuvのスレッドプールやOSの非同期I/Oにより)、JavaScriptの実行は1スレッドで順番に実行される
「非同期=全部同時に動いている」わけではなく、
I/O処理は並行するがJavaScriptの実行は順番に行われる、
シングルスレッドでの制約を意識したコード実装がとても大切です。
5. シングルスレッドの前提と影響
Node.jsはシングルスレッドが基本、という前提で考える必要があります。
制限というよりは、I/O処理を前提にした設計の結果です。
処理の向き・不向きとしては下記。
| 処理の種類 | 向き・不向き | 具体例 |
|---|---|---|
| ファイルI/O/通信 | 向いている | ファイル読み書き、HTTPリクエスト |
| 軽い計算 | 問題になりにくい | 文字列操作、簡単な数値計算 |
| 重い計算・長いループ | 影響が出やすい | 画像処理、大量データの変換、複雑な計算処理 |
CPUを長時間使う処理を書くとイベントループ全体がブロックされ、
他の処理が止まる恐れがある点は注意が必要です。
※ 下記のコードは実際の実行では時間がかかるので注意してください。
const express = require('express');
const app = express();
function heavyCalculation() {
let sum = 0;
for (let i = 0; i < 10000000000; i++) {
sum += i;
}
return sum;
}
app.get('/api/data', (req, res) => {
const result = heavyCalculation();
res.json({ result });
});
app.listen(3000);
この処理中は他のHTTPリクエストが処理されません。
const express = require('express');
const app = express();
async function heavyCalculationAsync() {
let sum = 0;
const chunkSize = 1000000;
for (let i = 0; i < 10000000000; i += chunkSize) {
for (let j = i; j < i + chunkSize && j < 10000000000; j++) {
sum += j;
}
await new Promise(resolve => setImmediate(resolve));
}
return sum;
}
app.get('/api/data', async (req, res) => {
const result = await heavyCalculationAsync();
res.json({ result });
});
app.listen(3000);
Node.jsが遅いというよりは、
設計と噛み合っていないコードになっているケースが多いように感じます。
6. ブラウザJavaScriptとの違い
同じJavaScriptでも、ブラウザ環境での前提とNode.jsでの前提は大きく異なります。
| 観点 | ブラウザ | Node.js |
|---|---|---|
| 主な役割 | UI操作、DOM操作 | I/O処理、サーバーサイド処理 |
| 実行環境 | ブラウザ内 | OS上で直接実行 |
| 主なAPI | DOM/window/fetch | fs/http/process |
| セキュリティ | サンドボックス内で実行 | OSの権限に依存 |
ブラウザ環境ではDOM操作やイベントハンドリングが中心となり、
Node.jsではファイルシステムやネットワーク処理が中心となります。
Node.jsにはUIがありませんが、
その分、リソース管理や処理効率に集中した設計になっています。
7. なぜサーバーやツールに使われやすいのか
Node.js自体はサーバーではありませんが、下記のような特徴によりサーバーやツールも多く使われています。
- I/O待ちが多い処理(ファイル読み書き、データベースアクセス、HTTPリクエストなど)に向いている
- 非同期処理により、同時に多くのリクエストを扱いやすい
- フロントエンドとサーバーサイドでJavaScriptを統一できる
HTTPのルーティングなどはExpressやFastifyといったフレームワークが担当し、
Node.jsはその土台の役割になります。
フロント側のJSコードの知識でサーバーサイドも書けるので、
フロントエンドとサーバーサイドを統一的に管理でき、
かつ学習コストが低くなるのも大きな利点ですね。
const express = require('express');
const fs = require('fs').promises;
const app = express();
app.get('/api/data', async (req, res) => {
try {
const data = await fs.readFile('data.json', 'utf8');
res.json(JSON.parse(data));
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
8. まとめ
この記事では、Node.jsを「実行基盤」という視点で整理してきました。
押さえておきたい3つのポイント
-
V8エンジンを組み込んだ実行環境
- JavaScriptエンジンそのものではなく、V8を組み込んでOS操作やイベントループを持たせた環境
- ブラウザと同じV8を使っているため挙動が似ている
-
I/O処理を前提にした設計
- シングルスレッドで、イベントループとlibuvによって非同期処理を実現している
- ファイルI/Oやネットワーク通信には向いているが、CPU集約的な処理には向いていない
-
フロントエンド開発における役割
- フロントエンド開発においては、アプリケーションを書く言語ではなく、開発を回すための実行基盤
- ビルドツールや開発サーバー、静的解析など、開発環境を支える土台
※ サーバーサイドではExpressやFastifyなどを使ってWebアプリケーションそのものも書けるようですが、フロントエンド開発では主に開発ツールの実行基盤として使われるため
実務での判断軸
Node.jsを使うかどうかを判断する際は下記。
- I/Oが中心の処理か → ファイル操作、データベースアクセス、HTTP通信など
- CPUを長時間占有しないか → 重い計算処理は避ける、または分割する
- 非同期前提で設計できるか → ブロッキング処理を避けられるか
「非同期だから速い」というより、
I/O待ちの間も他の処理を進められる設計だから扱いやすい
という理解が大切です。
おわりに
Node.jsを完全に理解するのは簡単ではありませんが、
まずは、
- 何を得意としているのか → I/O処理、非同期処理、開発ツールの実行基盤
- どこで無理が出やすいのか → CPU集約的な処理、長時間ブロッキングする処理
これらを意識するだけで、まずは書くコードや技術選定の判断ができそうですね。
Node.jsを「よくわからないけど目にするもの」から「理解して最適化を行えるもの」へ、一緒に変えていきましょう。