保存するたびに一瞬固まる
VSCode で TypeScript のファイルを保存すると、フォーマットが走るまでに妙な間がある。
毎回ではない。ファイルによっては一瞬で終わるのに、ときどき数秒待たされる。
同時に、変数にホバーしても「読み込み中…」のまま型が出てこないことがある。しばらく待つと表示される。
犯人は tsserver、TypeScript の言語サーバーがメモリを食い切って落ちていた。
問題は「ファイル数」ではなく「1ファイルあたりのコスト」?
ホバーの「読み込み中」
ホバー時のツールチップを出しているのは Prettier ではない。tsserver だ。「読み込み中…」で止まるのは、tsserver がリクエストに返事できていない状態を意味する。
つまり tsserver が重い。
npx tsc –noEmit –extendedDiagnostics で
エディタ越しに調べるより、CLI で直接測る方が早い。
npx tsc --noEmit --extendedDiagnostics
--extendedDiagnostics を付けると、読み込んだファイル数、型の生成数、チェックにかかった時間、使用メモリが出力される。
このとき、コマンドが完走せず OOMErrorHandler を含むスタックトレースを吐いて停止した。ヒープ不足だ。
Node のデフォルトのヒープ上限は環境にもよるがおおむね 2〜4GB。tsc がそれを超えたということは、同じプロジェクトを読む tsserver も常に上限ギリギリで動いていることになる。
保存時フォーマットもホバーも「たまに」遅かった理由がこれで説明できる。たまたま重い処理が重なった瞬間に GC が走り、その間すべてが止まっていた。断続的に固まるのは、慢性的なメモリ逼迫の典型的な症状だ。
ファイル数を数える、型チェックを飛ばす
OOM で落ちるなら診断もできない。そこで型チェックを行わず、ファイル解決だけを実行する。
npx tsc --noEmit --listFilesOnly > files.txt
--listFilesOnly は依存を辿ってコンパイル対象を確定させるところまでで止まる。型チェックをしないのでメモリをほとんど使わず、落ちているプロジェクトでも完走する。
結果を数える。
wc -l files.txt
grep -c node_modules files.txt
実際の値はこうだった。
| 項目 | ファイル数 |
|---|---|
| 合計 | 4031 |
| node_modules 配下 | 3694 |
| 自前コード | 約 337 |
337 ファイル。小規模と言っていい。
この数字が切り分けを一気に進める。TypeScript がメモリを使い切る原因は大きく二つ、ファイル数の多さか、型の複雑さかだ。数万ファイル規模なら前者を疑う。337 で落ちるなら後者しかない。
pnpm を使っている場合は pnpm exec tsc ... に読み替える。モノレポでは実行するパッケージのディレクトリに cd してから叩かないと、意図しない tsconfig.json が拾われる。
ファイル数と型の複雑さは別の問題
ここは混同しやすいので分けて考えたい。
ファイル数が多い場合、症状は「起動が遅い」「初回のチェックが長い」になる。原因は include が広すぎる、exclude を書いていないので dist や coverage を拾っている、types を明示していないので node_modules/@types 配下を全部読んでいる、といったところだ。
一方、型の複雑さが原因の場合、ファイル数は少ないのにメモリと時間だけが伸びる。深い条件型、再帰的な型、巨大な union、テンプレートリテラル型の組み合わせなどで、型の展開回数が指数的に増える。--extendedDiagnostics の Instantiations が数百万を超えていたら、まずこれを疑う。
依存パッケージ側の型定義が原因の場合もある。
読み込まれている型定義の内訳は files.txt を集計すれば見えるが、パスの構造次第で集計コマンドが空振りすることがある。深追いする前に、次の切り分けを先にやった方がいい。
まず落ちない状態を作る
原因の特定には tsc を完走させる必要がある。落ちたままでは何も測れない。順序としてヒープ上限の引き上げが先に来る。
NODE_OPTIONS=--max-old-space-size=8192 pnpm exec tsc --noEmit
これで通れば、VSCode 側にも同じ設定を入れる。settings.json に一行。
"typescript.tsserver.maxTsServerMemory": 8192
保存してウィンドウをリロードすれば、ホバーの「読み込み中」も保存時フォーマットの遅さも解消する。
maxTsServerMemory は VSCode の正規の設定項目で、大規模プロジェクトでは引き上げが推奨されている。裏技ではない。
ただし対症療法であることは自覚しておきたい。自前 337 ファイルでデフォルトを超えるのは本来おかしい。数週間後にまた落ちるなら、型が増え続けているサインだ。
犯人ファイルを特定する
完走するようになったら、改めて計測する。
npx tsc --noEmit --extendedDiagnostics
見るべき数値は次の通り。
| 項目 | 意味 | 目安 |
|---|---|---|
| Files | 読み込んだファイル数 | 数千を超えると重い |
| Instantiations | 型の展開回数 | 数百万超なら型が犯人 |
| Check time | 型チェックの所要時間 | ここが支配的なら型側 |
| Memory used | 使用メモリ | 上限に近ければ危険 |
Check time が突出しているなら、トレースを取ってファイル単位まで絞り込める。
npx tsc --noEmit --generateTrace ./trace
npx @typescript/analyze-trace ./trace
analyze-trace は「どのファイルの、どの型のチェックに何ミリ秒かかったか」をホットスポット順に出力する。ここまで来れば修正対象は具体的なコード行として特定できる。
なお VSCode 側のログを直接読む手もある。
"typescript.tsserver.log": "verbose"
リロード後にコマンドパレットから TypeScript: Open TS Server Log を開くと、Project '/path/tsconfig.json' (Configured) の直後に Files (1234) という行が出る。ただしログは有効な間ずっとディスクに書き込まれ続け、それ自体が動作を遅くする。調査が終わったら必ず off に戻すこと。
切り分けの順序
同じ症状に出会ったときのために、辿った道筋を残しておく。
- エディタが遅いのか CLI が遅いのかを分ける。CLI だけなら
--cacheとファイル数の問題、エディタなら 1 ファイルあたりのコストの問題 - ホバーも同時に遅いか確認する。遅ければ Prettier ではなく tsserver
tsc --noEmit --extendedDiagnosticsを叩く。ここで OOM が出れば原因は確定- 落ちるなら
--listFilesOnlyでファイル数だけ数え、ファイル数の問題か型の問題かを判定する - ヒープ上限を上げて完走させ、
--generateTraceで犯人を特定する
Prettier の設定をいくら見直しても改善しなかったのは、そもそも見る場所が間違っていたからだった。フォーマッタが遅いという症状は、TypeScript プロジェクトの健康状態を映す鏡でもある。
VS Code 拡張機能 TypeScript 7 は解決策になるか
ここまでの話は TypeScript 6 系を前提にしている。2026年7月8日に TypeScript 7.0 が正式リリースされ、コンパイラと言語サーバーが Go にネイティブ移植された。フルビルドで 8〜12 倍という数字が出ている以上、「速いコンパイラに乗り換えれば済むのでは」という疑問は当然出る。
結論から言うと、症状には効くが原因には効かない。分けて考える必要がある。
効く: 症状のレイヤー
エディタ側の改善は大きい。公式の計測では、エラーのあるファイルを開いてから最初の診断が表示されるまでが 17.5 秒から 1.3 秒未満に短縮され、言語サーバーのクラッシュも約 60% 減っている。LSP ベースの新しい言語サーバーになったことで、補完、参照検索、ホバーの応答がまとめて速くなる。
「ホバーが読み込み中のまま返ってこない」「保存時に数秒固まる」は、この改善範囲に収まる。乗り換えれば体感は確実に良くなる。
効かない: 原因のレイヤー
一方で、この記事が辿り着いた「自前 337 ファイルで OOM するなら型の複雑さが原因」という結論には、TypeScript 7 は手を触れない。
理由は 2 つある。
1 つ目は、Go 版はゼロから書き直したものではなく既存実装から忠実に移植されたもので、型チェックのロジックは TypeScript 6.0 と構造的に同一だという点だ。1 回の型展開が速くなるだけで、展開の回数そのものは変わらない。指数的に膨らむ条件型は、Go で書き直しても指数的に膨らむ。
2 つ目はメモリ削減の実際の幅だ。解説記事には「半減」「1/3」といった数字が出回っているが、公式発表の実測値はプロジェクトによって 6〜26% 減である。2〜4GB の上限を突き抜けているプロジェクトに対して、この幅は救済にならない。さらに TypeScript 7 は型チェックを複数ワーカーで並列実行するため、設定次第ではむしろメモリが増える。
診断の役割分担で言えば、TypeScript 7 に移っても --generateTrace で犯人を特定して型を直す作業は消えない。速くなったぶん、同じ量の仕事を短い時間でこなすようになるだけだ。
本記事の手順は一部が無効になる
ここが実務上いちばん影響が大きい。TypeScript 7 に移ると、ここまでに書いた対処のいくつかがそのまま使えなくなる。
| 本記事の手順 | TypeScript 7 での扱い |
|---|---|
NODE_OPTIONS=--max-old-space-size=8192 | 完全に無効。Go バイナリなので V8 ヒープの概念自体がない |
typescript.tsserver.maxTsServerMemory | JS 版 tsserver 向けの設定。ネイティブ言語サーバーには効かない |
--listFilesOnly | そのまま使える |
--extendedDiagnostics | 使えるが出力項目が変わる。Memory allocs が増え、キャッシュサイズ系の行は減る |
--generateTrace + analyze-trace | トレース形式が変わっている可能性がある。移行時は実機で動作を確認すること |
typescript.tsserver.log | ネイティブ言語サーバー側のログ設定を確認し直す必要がある |
(新規)--checkers | 型チェックワーカー数。デフォルトは 4。増やすと速くなるがメモリ使用量も増える |
メモリのチューニング軸が --max-old-space-size から --checkers に置き換わる、というのが最大の変更点だ。CI ランナーのようにコア数もメモリも少ない環境では、むしろワーカー数を減らす方向に調整する。
そもそも移行できるかは別問題
TypeScript 7.0 にはプログラムから利用できる API が同梱されておらず、新 API の提供は 7.1 を待つ必要がある。その間、Compiler API を直接叩くツール向けには互換パッケージ @typescript/typescript6(tsc6 コマンド)が用意されており、npm エイリアスでの併用が推奨されている。
具体的に引っかかるのは以下だ。
- 型情報を使う typescript-eslint
- ts-morph、ts-patch、カスタムトランスフォーマー
- Vue / Astro / Svelte のワークフロー、Angular テンプレートの型チェック
保存時フォーマットが遅いという本記事の出発点は、Prettier と ESLint プラグインが絡む構成である可能性が高い。乗り換えを検討するなら、まずここの互換性から確認した方がいい。
結論
TypeScript 7 は、この記事の結論を置き換えるものではなく、より強力な対症療法として位置づけるのが正確だ。
- 体感の「固まる」は確実に減る。乗り換える価値はある
- しかし型の複雑さが原因の OOM は、コンパイラを替えても残る
--extendedDiagnosticsで Instantiations を見て、--generateTraceで犯人を特定し、型を直す。この作業は依然として必要
速いコンパイラは、型の設計ミスを隠すのが上手くなるだけで、消してはくれない。むしろ隠れてしまうぶん、発覚が遅れて型が増え続けるリスクがある。