Node.jsパフォーマンスチューニングをDatadog APMとClaude Codeでサクッとやる

はじめに
こんにちは、kosui(@kosui_me)(id:kosui_me)です。
普段は医療系のスタートアップで認証基盤・ライセンス基盤・組織階層基盤などのプラットフォームシステムを開発・運用するチームのテックリードをしています。
日本の医療に本気で向き合う。認証・権限管理基盤チームの決意 - KAKEHASHI Tech Blog
今日は、サーバサイドTypeScriptでやっていく上で、いつか向き合うことになるパフォーマンスチューニングの話をします。
私は前職ではGoやPerlやJavaを使っていましたが、現職ではNode.js/TypeScriptを使っています。サーバサイドTypeScriptの知見は多くないと感じているので、こうして記事を書き溜めておいて、将来の自分や同じようにNode.js/TypeScriptでサーバサイド開発をしている人の助けになれば嬉しいです。
他に書いたサーバサイドTypeScriptの記事
https://kosui.me/posts/2025/05/06/142842
https://kakehashi-dev.hatenablog.com/entry/2025/08/19/110000
本題
Node.jsはシングルスレッドでイベントループを回すアーキテクチャを採用しています。そのおかげで大量の同時接続を効率的に捌いてくれますが、その一方でCPU負荷が高い処理が発生するとそこで詰まってしまい、アプリケーション全体のレスポンスが低下してしまいます。
そんな時、私たちは「推測する前に計測せよ」に従い、まず問題をきちんと発見することに注力すると思います。実際、Goを書いている人でpprofを使ったことがない人は少ない思います。
一方で、Node.jsのプロファイリングにはあまり馴染みがない人も多いのではないでしょうか。
そこで、Datadog APMでNode.jsアプリケーションのパフォーマンスを計測して、Claude Codeでボトルネックを解析し、サクサクとパフォーマンスチューニングを行う方法を紹介します。
Node.jsのパフォーマンスチューニングの勘所
多くの場合、Node.jsアプリケーションのパフォーマンスチューニングを行う理由は次のような問題が発生した時です。
-
メモリ
-
メモリリークが発生しGCが頻発している
-
そもそもヒープサイズが小さすぎる
-
CPU
-
非同期にすべき処理が同期的に実行されている
-
CPUヘビーな処理が全体のイベントループを阻害している
-
ネットワーク
-
ネットワークでKeep-Aliveが効いていない
具体的なハマりどころをいくつか紹介します。
ちなみに、以下の記事が非常に参考になりました。
https://yosuke-furukawa.hatenablog.com/entry/2017/12/05/125517
メモリ
Node.jsは世代別GCを採用しています。世代別GCでは、オブジェクトが新しく生成されたときは新世代に配置され、ある程度生存し続けると古い世代に昇格します。GCは新世代を頻繁にスキャンして不要なオブジェクトがドシドシ回収されていく一方で、古い世 代はあまり頻繁にはスキャンしません。Node.jsでは、新世代を管理するNew Spaceと、古い世代を管理するOld Spaceがあり、それぞれ最大サイズを指定できます。
そして、サーバサイドTypeScript/JavaScriptアプリケーションにおいて、リポジトリやHTTPクライアントやロガーなどをシングルトン化せずにリクエストごとに生成してしまうと、オブジェクトがNew spaceにどんどん溜まっていき、GCが頻発する原因になります。
また、利用しているライブラリ側でグローバルなキャッシュが適切にクリアされない場合も、メモリリークの原因になります。
いずれにせよ、メモリリークが発生している場合はGCが頻発し、アプリケーションのレスポンスが低下します。
また、そもそもヒープサイズが小さすぎる場合も、GCが頻発する原因になります。Node.jsアプリケーションのデフォルトのヒープサイズは比較的小さいため、大量のデータを扱うアプリケーションでは --max-old-space-size オプションでヒープサイズを増やすことが必要になる場合があります。ただ、あまりにデカいと他のプロセスが使うメモリがなくなりますので、注意して下さい。
CPUヘビーな処理
サーバサイドTypeScript/JavaScriptでは、シングルスレッドでイベントループを回しているため、非同期処理が得意な反面、CPUヘビーな処理を苦手としています。
よくあるCPUヘビーな処理の例としては、例えば次のようなケースがあります。一部の処理はどうしても同期的に実行する必要があるため、元気があればWorker Threadsを使って別スレッドで処理する方法を検討したり、CPUヘビーな処理を他のサービスに切り出すことを検討したりします。
-
大量のデータを同期的に処理する
-
デカい配列を処理する
-
argon2idやbcryptなどのCPU負荷が高いハッシュ関数を同期的に実行する -
Reactのサーバサイドレンダリングで大規模なコンポーネントツリーを同期的に描画する
Datadog APM
@datadog/pprof
実は、Node.jsでもGoと同じようにpprofを利用でき、Datadog APMもpprofをサポートしています。
プロファイラは単体で利用することもでき、@datadog/pprof (DataDog/pprof-nodejs) にて公開されています。
このプロファイラは、V8のプロファイラによって収集されたプロファイルをpprof形式にエンコードして保存し、そのプロファイルをDatadog APMに送信する仕組みになっています。
V8のプロファイラをそのまま利用しているため、どの関数でCPU時間が消費されているか、どの関数がヒープを消費しているかなどを詳細に分析できます。具体的には、次の3種類のプロファイルを収集できます。
-
CPU時間 (CPU Time)
-
ヒープサイズ (Heap Size)
-
実時間 (Wall Time)
なお、@datadog/pprof はGoogleの google/pprof-nodejs をフォークして改良しています。google/pprof-nodejsもメンテナンスされているものの、Datadog版の方が積極的に改善されている印象です。
また、DatadogはPure JavaScriptなpprofエンコーダ・デコーダDataDog/pprof-formatを開発しています。力が入っていますね。
Datadog APMの導入
導入もそれなりに簡単で、node --require dd-trace/init app.js のように --require オプションを付けて起動すれば計測できるようになります。ただし、得られたプロファイルをDatadog APMへ送信するためにはDatadogエージェントが動いている必要があります。正しい導入手順は Node.js アプリケーションのトレース を参照してください。
プロファイル収集の原理
--require オプションで渡された dd-trace/init モジュールはアプリケーションの起動時に自動でロードされます。
dd-trace/init 内の @datadog/pprof がロードされた時点でプロファイリングが開始され、同時に process.on('exit', () => { }) ハンドラが登録されて、プロセス終了時にpprof形式のプロファイルが保存されます (コード)。保存されたプロファイルは、おそらくDatadogエージェントが収集してDatadog APMに送信していると思われます。
pprof形式のファイルは、先ほども述べた通りV8のプロファイラによって収集されたプロファイルをエンコードして作られています。具体的には、v8.getHeapSnapshot() や v8.getCPUProfile() といったV8のAPIを利用してプロファイルを取得し、それをpprof形式に変換しています (コード)。
Datadog APMでのプロファイル確認
Datadog APMにプロファイルが送信されると、DatadogのAPMダッシュボードで確認できます。
-
対象サービスのAPMダッシュボードに移動する
-
「Profiles」タブをクリックする
-
「Visualize as」から「Profile List」を選択する
このボタンを見つけられず、迷うことが多い
-
どれか一つプロファイルをクリックする
-
右上に小さなダウンロードボタンがある
マジでこれを見つけられず、本当に苦労した
Claude Codeでのプロファイル解析
Datadog APMからダウンロードしたpprof形式のプロファイルは、わりとそのままClaude Codeにぶん投げて解析できます。ただ、Claude Codeは go tool pprof を使って解析することが多いので、Goをインストールしておくとよいと思います。
また、実際のアプリケーションコードと見比べながら解析してほしいので、アプリケーションのプロジェクトルートからClaude Codeを起動するのがおすすめです。
Claude Codeが表面的な解析を繰り返す時は
ただ、pprof形式のプロファイルはそれなりにサイズが大きいので、Claude Codeは一部のプロファイルだけを覗き見して、表面的な解析結果を返してくることも多いです。
例えば、メモリリークが発生してGCが頻発している場合はCPUの使用率も高くなりますが、Claude Codeがそれを見て「CPUの負荷を減らそう」と提案してくることもあります。そういう時は、ヒープのプロファイルを重点的に解析するように促すとよいでしょう。
また、Claude Codeはあまり -tree オプションを使ってくれません。そのため、ネイティブモジュールにめちゃくちゃ気を取られることもあれば、複数の関数にまたがっているボトルネックを見逃すこともあります。そういう時は、-tree オプションを使うように促すとよいでしょう。
まとめ
Node.jsアプリケーションの知見があまりない中でも、Datadog APMとClaude Codeを組み合わせることで、案外なんとかパフォーマンスの問題を特定できます。ただ、Claude Codeが表面的な解析を繰り返すことも多いので、適宜ヒーププロファイルを重点的に解析するように促したり、-tree オプションを使うように促したりして、エージェントが道に迷わないようにサポートしてあげると良いと思います。
求人
日本の医療に本気で向き合うための、強くて研ぎ澄まされた基盤システムを一緒に作ってくれる人を探しています!!!!