ソースマップは、ビルド後のJavaScriptやCSSの「何行目の何桁目」が変換前のどのファイルのどの位置から来たのかを記録したJSONファイルです。拡張子は通常 .js.map や .css.map で、ブラウザの開発者ツールはこれを読んで、圧縮された1行のコードではなく元のソースを表示します。この記事ではECMA-426で確定した.mapファイルの中身、実際のmappings文字列をデコードした対応付け、esbuild・TypeScript・webpackでの生成設定の差、本番公開時にどこまで出してよいかの判断基準までを扱います。
まとめ:.mapファイルの必須3フィールドと本番公開で先に決める2点
ソースマップは2024年12月に ECMA-426「Source map format specification」1st edition として標準化されました。必須フィールドは version・sources・mappings の3つだけで、version は整数の3固定。中核の mappings はBase64 VLQで符号化された相対値の羅列で、デコードすると「生成後の桁」と「元ファイルの行・桁・識別子名」の対応表になります。実務の判断は2点に絞られます。1点目は本番で sourcesContent(元コード全文)を出すかどうかで、webpackなら hidden-source-map(.map自体を公開しない)か nosources-source-map(元コードだけ落とす)を選ぶ形。2点目は開発者ツールのコンソールに出る404警告で、これは依存パッケージが.mapを同梱していないだけのことが大半であり、アプリの動作には影響しません。
ソースマップの正体とECMA-426で確定した.mapファイルの中身
仕様は長らくGoogle Docsに置かれたリビジョン3の文書が事実上の標準でしたが、TC39にTG4(Task Group 4)が設置されて規格化されました。適用範囲は「JavaScript、WebAssembly、CSSにコンパイルされたコードのデバッグ体験を向上させる」ことと明記されており、JavaScript専用の仕組みではありません。
必須3フィールドと任意5フィールドの内訳
| フィールド | 必須 | 内容 |
|---|---|---|
| version | 必須 | 整数の3固定。他の値は拒否されうる |
| sources | 必須 | 変換前ファイルのパス配列 |
| mappings | 必須 | Base64 VLQで符号化した位置対応 |
| file | 任意 | この.mapが対応する生成ファイル名 |
| sourceRoot | 任意 | sourcesの各パスに前置する基準パス |
| sourcesContent | 任意 | 変換前ソースの本文そのもの |
| names | 任意 | 圧縮前の識別子名の配列 |
| ignoreList | 任意 | 第三者コードとみなすsourcesの添字 |
注意したいのは sourcesContent が任意である点。ここに元コードが丸ごと入るため、.mapを無防備に公開すると変換前のソースが誰でも読める状態になります。本番配信で最初に検討すべきはこのフィールドの扱いです。
sourceMappingURLコメントとsourcemapヘッダーによる紐付け
生成ファイルと.mapファイルは、生成ファイル末尾のコメントで結び付きます。ECMA-426は「生成側は //# のみを出力しなければならず、消費側は //@ も受理しなければならない」と規定。古い記事に登場する //@ 形式は読み取り互換のために残っているだけで、これから出力する側が選ぶ表記ではありません。
// JavaScript の場合(生成ファイルの末尾)
//# sourceMappingURL=out.js.map
/* CSS はブロックコメントのみ */
/*# sourceMappingURL=style.css.map */
コメントを使わずHTTPレスポンスヘッダーで示す方法もあり、規格が定めるヘッダー名は sourcemap(以前推奨された x-sourcemap は非推奨)。生成ファイル自体に痕跡が残らないので、社内ネットワークからのみ.mapを配信するといった制御と相性が良い方式です。SassやPostCSSが出す.css.mapも同じ仕組みで、CSSはブロックコメントしか使えない点だけが異なります。
mappingsをデコードして分かる対応付けの実体
mappings はセミコロンで生成後の行を区切り、カンマでセグメント(対応点)を区切ります。1セグメントは先頭から順に「生成後の桁」「sourcesの添字」「元の行」「元の桁」「namesの添字」を並べたもので、長さは1・4・5のいずれか。1フィールドだけのセグメントは、元ソースに対応点を持たない生成コードを示します。重要なのはすべて直前の値からの相対値である点で、この差分符号化のおかげで巨大なバンドルでも.mapが現実的なサイズに収まります。
以下は Node.js v26.5.0 上の esbuild 0.28.2 で、4行のTypeScriptをビルドした結果です。
$ esbuild greet.ts --bundle --minify --sourcemap --outfile=out.js
// 変換前 greet.ts
export function greet(name: string): string {
const greeting = `Hello, ${name}!`;
return greeting;
}
// 生成された out.js
(()=>{function n(e){return`Hello, ${e}!`}})();
//# sourceMappingURL=out.js.map
// out.js.map の中身
{
"version": 3,
"sources": ["greet.ts"],
"sourcesContent": [ ... 元コード105文字がそのまま ... ],
"mappings": "MAAO,SAASA,EAAMC,EAAsB,CAE1C,MADiB,UAAUA,CAAI,GAEjC",
"names": ["greet", "name"]
}
この mappings をBase64 VLQとしてデコードし、相対値を累積すると次の対応表になります。行と桁はいずれも規格どおりの0始まりで、エディタが表示する行番号より1小さい値です。
セグメント 相対値 生成後 行:桁 元 行:桁 name その位置の文字
MAAO [6, 0, 0, 7] 0:6 0:7 function <- function
SAASA [9, 0, 0, 9, 0] 0:15 0:16 greet n <- greet
EAAMC [2, 0, 0, 6, 1] 0:17 0:22 name e <- name
EAAsB [2, 0, 0, 22] 0:19 0:44 { <- {
CAE1C [1, 0, 2, -42] 0:20 2:2 return <- return
MADiB [6, 0, -1, 17] 0:26 1:19 Hello <- Hello
UAAUA [10, 0, 0, 10, 0] 0:36 1:29 name e <- name
CAAI [1, 0, 0, 4] 0:37 1:33 } <- }
GAEjC [3, 0, 2, -33] 0:40 3:0 } <- }
2行目の SAASA が5フィールドの典型例です。生成後の15桁目、つまり圧縮後の関数名 n の位置が元ファイル0行16桁目の greet に対応し、names の0番目から本来の名前を復元できると読めます。開発者ツールのスタックトレースに n ではなく greet と表示されるのは、この5番目のフィールドが効いているため。
5行目の CAE1C には -42 という負の値が現れます。直前が元ファイル0行44桁目だったので、そこから42桁戻って2行2桁目を指す形。VLQは最下位ビットを符号ビットに使うため、負の差分も1〜2文字で表現できます。生成後のコードは1行でも元ファイル側の参照位置は前後に跳ぶ、という事実がこの負値から読み取れます。
生成設定の実測差分:esbuild・tsc・webpack devtool
ソースマップは「出す・出さない」の2択ではなく、品質とビルド速度と情報漏えいリスクのトレードオフを設定値で選ぶものです。
webpack devtoolの選び分けと本番可否
| devtool | ビルド速度 | 再ビルド | 本番可 | 品質 |
|---|---|---|---|---|
| (none) | 最速 | 最速 | 可 | バンドルのみ |
| eval | 速い | 最速 | 不可 | 生成コード |
| eval-source-map | 最遅 | 普通 | 不可 | 元コード |
| eval-cheap-module-source-map | 遅い | 速い | 不可 | 元コードの行のみ |
| cheap-module-source-map | 遅い | 遅い | 不可 | 元コードの行のみ |
| source-map | 最遅 | 最遅 | 可 | 元コード |
| hidden-source-map | 最遅 | 最遅 | 可 | 元コード(参照コメントなし) |
| nosources-source-map | 最遅 | 最遅 | 可 | 元コードの位置のみ |
公式の品質表から実務で選ぶ値だけを抜き出したものです。開発中は eval-source-map か eval-cheap-module-source-map、本番は source-map 系かソースマップなしを選びます。eval を含む値が本番不可なのは、モジュールを eval() でラップするため、CSPで unsafe-eval を許可しない限り実行できないから。cheap が付く値は桁を落として行単位に丸めるので再ビルドは速くなりますが、圧縮後の1行に複数の文が並ぶ本番ビルドでは桁がないと位置を特定できません。バンドラの比較検討中なら、Rollupとは|JavaScriptバンドラーのnpm導入から使い方・Webpack/Viteとの違いまでやesbuildとは?Go製の高速バンドラの使い方と採用判断を実装目線で解説が設定名の対応表として使えます。
minifyの有無で変わるnamesフィールドの中身
同じソースでも、圧縮するかどうかで.mapの内容は変わります。先ほどのesbuild(--minifyあり)が names: ["greet", "name"] を出力したのに対し、TypeScript 7.0.2 で tsc greet.ts --sourceMap --outDir dist を実行すると names は空配列。識別子をリネームしていないため、名前の復元情報が要らないからです。代わりに file(greet.js)が出力され、sourceRoot は空文字のキーとしてのみ現れ、sources は.mapから見た相対パス(この例では ["../greet.ts"])になります。つまり names が空の.mapを見たときは「壊れている」のではなく「圧縮工程を通っていない」と判断できます。
TypeScript側の主な設定は次の5つです。
sourceMap:.js.mapを別ファイルとして出力(既定はfalse)inlineSourceMap:.mapを別出力せずdata:application/json;base64,形式で.jsに埋め込む。sourceMapとは排他inlineSources:元ソース本文をsourcesContentに含める。上2つのどちらかとの併用が前提(1.5以降)mapRoot:デバッガーが.mapを探す場所を指定。文字列はそのまま.map内に書き込まれるdeclarationMap:.d.tsから元の.tsへ戻る対応を出力(2.9以降)
declarationMap だけは目的が違い、実行時デバッグではなくエディタ体験のための設定です。モノレポでパッケージ間の定義ジャンプが生成物の.d.tsに着地して困っているなら、ここを有効にすれば解決します。Rust製トランスパイラでの同等設定はSWCとは?Rust製コンパイラの設定と採用判断を実装目線で解説で整理しています。
本番公開でsourcesContentを残すかどうかの判断基準
「本番にソースマップを置くな」という助言は広く流通していますが、実際には何を隠したいのかによって選ぶべき設定が変わります。隠したいのが元コード本文なのか、ディレクトリ構成まで含めた設計情報なのかを先に決めてください。
webpackの公式ドキュメントは、この2つを明確に区別しています。hidden-source-map は source-map と同等の.mapを生成しつつ、生成ファイルに参照コメントを付けません。ブラウザからは.mapの存在自体が見えず、公式ドキュメントも「.mapファイルはWebサーバーへデプロイすべきではない」と明記。エラー監視サービスへ別経路でアップロードする前提の設定です。
一方 nosources-source-map は sourcesContent を含まない.mapを作ります。クライアント側でスタックトレースを元の位置へ復元できるのに元コードは読まれないため、公式ドキュメントはこちらをWebサーバーへ配置してよいとしています。ただし同じ箇所の注記どおり、ファイル名とディレクトリ構造は依然として露出。
esbuildでも同じ2軸を指定でき、実際の出力で確認できます。--sourcemap=external でビルドすると生成ファイル末尾の sourceMappingURL コメントが消え(hidden-source-map相当)、--sources-content=false を付けると.mapのキーから sourcesContent 自体が消えます(nosources-source-map相当)。
// 既定(sourcesContent あり)
["version", "sources", "sourcesContent", "mappings", "names"]
// --sources-content=false で出力した .map のキー
["version", "sources", "mappings", "names"]
エラー監視サービスへのアップロード運用
本番のスタックトレースを元コードの位置まで戻したいが.mapは公開したくない、という要求は hidden-source-map とエラー監視サービスの組み合わせで満たします。ビルド直後にCIから.mapをサービス側へアップロードし、配信物には含めずに破棄する流れ。リリースごとのバージョン識別子とアップロードした.mapの紐付けを外すと復元に失敗するため、CIのジョブ設計まで含めて用意してください。具体的な手順はDatadog Error Tracking実装手順:イシュー集約とソースマップ登録、APM/RUM/Logs連携で扱っています。なおこの紐付けをパスに依存させず解決するため、規格側でも生成物と.mapの双方に同一IDを埋め込む Debug ID の提案がStage 2まで進んでいます。
サードパーティ由来のDevTools警告とignoreListによる抑制
この章の2つの話題は、原因も対処も別物です。まず警告の切り分けから扱います。
404警告の切り分け手順
開発者ツールのコンソールに出る DevTools failed to load source map: Could not load content for ... HTTP error: status code 404 は、ソースマップ関連でもっとも多く遭遇するメッセージです。原因はほぼ1つ。読み込んでいるJavaScriptの末尾に sourceMappingURL コメントが残っているのに、指し示す.mapファイルが配信されていない状態です。npmパッケージが圧縮済みの.jsだけを同梱し、.mapを含めずに公開しているケースが典型例。
切り分けは、警告に書かれたファイル名が自社のコードか依存パッケージかで分かれます。自社コードのファイル名が出ているなら.mapが配信対象から漏れているかパスが違うので、ビルド設定とデプロイ対象を確認してください。依存パッケージのファイル名ならパッケージ側の問題なので、基本的に放置してかまいません。逆に hidden-source-map を選んだのに404が出ているなら、設定が効いていない証拠なので調査対象になります。
ignoreListによる他社コードの除外
404警告とは別に、他社コードがデバッグの邪魔をする問題には規格側の解があります。ECMA-426が定義する ignoreList は「フレームワークのコードやバンドラが生成したコードなど、第三者のコードとみなすべきファイルの添字を並べた任意のリスト」で、規格文書は「開発者が事前に設定しなくても、開発者が見たくない・ステップ実行したくないコードを開発ツールが避けられるようにする」ためのものと説明しています。
Chrome DevToolsはこの ignoreList を読み取り、スタックトレースから第三者フレームを自動的に隠し、展開時はグレー表示にします。Sourcesパネルのブレークポイント時のコールスタックにも同じ扱いが適用される仕組み。Googleの独自拡張として先行した x_google_ignoreList という名前は規格採用にあたって改められ、規格文書自身が「ignoreList が無い場合に非推奨の x_google_ignoreList を使うブラウザもある」と互換の存在を認めています。
設定作業なしで恩恵を受けられるかはツール次第で、Angularはv14.1.0以降、Nuxtはv3.3.1以降が既定でこのフィールドを出力。Vite・Rollupは設定で対応でき、webpackは第三者プラグインが必要です。node_modules の中を延々とステップ実行している状態なら、まずフレームワークのバージョンを上げるだけで解消する可能性があります。
ASTとソースマップの違い|比較が成立しない理由
「ASTとソースマップの違い」という問いは検索されますが、この2つは並べて選ぶ対象ではありません。抽象構文木(AST)はコンパイラやトランスパイラが変換処理を行うためにメモリ上に構築する中間表現であり、ソースマップはその変換の結果としてディスクに出力される成果物です。層が違うので、どちらを使うかという選択は発生しません。
両者の接点は位置情報にあります。パーサーが元ファイルを読むとき、各ノードに行と桁の情報を付与。変換後もノードは位置情報を保持しており、コード出力の段階で「このノードは元の何行何桁から来た」という記録がそのまま mappings のセグメントになります。先ほどデコードで確認した names の復元も、ASTのノードが変換前の識別子名を覚えていたからこそ書き出せるものです。
実装上の区別は寿命です。ASTはビルド後に消えますが、ソースマップは配信後もファイルとして残り、ブラウザやエラー監視サービスが後から消費します。ソースマップの読み書きにASTの知識は不要な一方、AST操作ツールを書く側は変換のたびに位置情報を引き継ぐ責務を負う。この領域に踏み込むならTypeScript Compiler APIとは|AST解析と型情報取得の手順とTypeScript 7対応が入口になります。標準化後の拡張もこの位置情報を豊かにする方向で進んでおり、変数スコープを持たせる Scopes がStage 3、範囲での対応付けを扱う Range Mappings がStage 2まで到達しています。Scopesが入れば、圧縮後のコードでも変数名や関数スコープを正しく復元できます。
よくある質問
.mapファイルを削除するとどうなりますか?
アプリケーションの動作には影響しません。.mapは開発者ツールとエラー監視サービスだけが読むファイルで、実行には使われません。削除すると開発者ツールで元コードが表示できなくなり、参照コメントが残っていれば404警告が出るようになります。
「DevTools failed to load source map」は放置しても大丈夫ですか?
依存パッケージのファイルが原因なら放置して問題ありません。これは開発者ツールを開いている人にしか見えない警告で、一般の利用者のブラウザには何も表示されず、アプリの動作にも影響しません。警告表示そのものを止めたい場合は、Chrome DevToolsの設定でJavaScriptソースマップの読み込みをオフにするのが最短です。
npmで公開するパッケージに.mapファイルを含めるべきですか?
含めるか、含めないなら参照コメントも消すか、どちらかに揃えてください。sourceMappingURL コメントだけ残して.mapを同梱しないと、そのパッケージを使う全員のコンソールに404警告が出ます。ソースを隠したい場合は nosources-source-map 相当の設定で sourcesContent を落とせば、元コードを配らずに位置の復元だけ提供できます。
インラインソースマップと外部.mapファイルはどちらを選ぶべきですか?
開発環境ではインライン、本番では外部を選びます。インラインはBase64で.jsに埋め込むためファイル管理が不要で、.mapの配信設定を用意できない環境でも動きますが、生成ファイルが肥大化し元コードが配信物に同梱されます。ただし本番の「外部」は必ずしも公開を意味しません。公開せずエラー監視サービスへ送る(hidden-source-map)か、sourcesContent を落として公開する(nosources-source-map)かを別途決める必要があります。
SassやCSSでもソースマップは同じ仕組みですか?
フォーマットは同じで、ECMA-426もCSSを適用範囲に含めています。違いは参照コメントの書き方だけで、CSSは行コメントを持たないため /*# sourceMappingURL=... */ というブロックコメント形式を使います。Sassのコンパイラも生成をオフにする設定を持っているので、本番ビルドでは切るのが一般的です。