インポートマップとモジュール解決
裸の指定子をバンドラなしでブラウザに解決させ、依存の差し替えもCDN移行もHTML1行で完結。import mapの解決アルゴリズムとスコープの仕組みを原理から押さえます。
- 1.ブラウザは相対・絶対URL以外の裸の指定子(例: react)を単独では解決できない。import mapがモジュール指定子をURLへ変換する解決表として働く。
- 2.解決は最長一致(longest prefix match)で、スコープ付きマッピングはパス階層内でのみ有効になり、通常マッピングより優先される。
- 3.従来は最初のモジュール評価前に単一のimport mapを確定させる必要があったが、現行仕様では複数定義・動的追加が可能で、重複キーは先勝ちで無視される。
なぜブラウザはreactを解決できないのか
ESモジュールの import 文で相対パス(./a.js)や絶対URL(https://example.com/a.js)を書けば、ブラウザは指定子をそのままURLとして扱い解決できます。ところが import React from "react" のような**裸の指定子(bare specifier)**は、そもそもURLとして不正です。スキームもドット始まりの相対パスも持たない文字列を、ブラウザは単独では取得先に変換できません。
Node.jsではこの変換を node_modules 探索と package.json の exports フィールドが担いますが、ブラウザにファイルシステム探索の概念はありません。import mapは、この裸の指定子を実URLへ変換するための宣言的な解決表をHTML側に持ち込む仕組みです。バンドラが担っていた「依存名→実ファイル」の対応づけを、ビルドではなくブラウザの解決アルゴリズムに肩代わりさせます。ESMの読み込みが construction・instantiation・evaluation の3フェーズで進む点は ESモジュールの解決・リンク・評価フェーズ が扱う内容で、import mapが介入するのは最初のフェーズ、指定子解決の入口です。
import mapの基本構造
import map は <script type="importmap"> に埋め込むJSONで、imports キーの下に「指定子→URL」の対応を列挙します。
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18",
"react-dom/client": "https://esm.sh/react-dom@18/client",
"app/": "./src/app/"
}
}
</script>
<script type="module">
import React from "react";
import { createRoot } from "react-dom/client";
import { Header } from "app/components/header.js";
</script>
これだけで、ビルドツールなしに import React from "react" がCDN上のファイルへ解決されます。バンドラが不要になるわけではなく、開発時の即時反映や依存の差し替え、小規模プロジェクトでのビルドレス開発の選択肢が増えるのが実利です。
"app/": "./src/app/" のようにキーと値の両方が / で終わるエントリは、プレフィックス一致として扱われます。app/components/header.js という指定子は、app/ の部分を ./src/app/ に置き換えて ./src/app/components/header.js に解決されます。末尾スラッシュのないエントリは完全一致のみで、ディレクトリ的な展開はしません。
解決アルゴリズム:最長一致で決まる
複数のマッピングが同じ指定子の前方部分にマッチしうる場合、ブラウザは最も長く一致するキーを採用します。これが最長一致(longest prefix match)です。
{
"imports": {
"lodash/": "https://esm.sh/lodash@4/",
"lodash/debounce": "https://esm.sh/lodash-es@4/debounce.js"
}
}
import debounce from "lodash/debounce" は、lodash/(プレフィックス)と lodash/debounce(完全一致)の両方に前方一致しますが、より長く具体的な lodash/debounce が優先されます。一方 import pick from "lodash/pick" は完全一致のエントリがないため、lodash/ プレフィックスにフォールバックして解決されます。登録順ではなく一致の長さが優先順位を決める点が、CSSの詳細度に近い性質だと理解すると整理しやすくなります。
import map にもマッピングがなく、相対パス・絶対URLでもない裸の指定子は解決不能として扱われ、そのimport文を含むモジュールのフェッチが失敗します。実行時例外ではなく、ESモジュールの解決フェーズで止まる点は通常のURL解決の失敗と同じ扱いです。バンドラを外す際は、コード中の全ての裸の指定子に対応するエントリを漏れなく用意する必要があります。
スコープ付きマッピング:パス階層で解決を切り替える
scopes キーを使うと、特定のパス配下でだけ有効な指定子マッピングを定義できます。これは同じ指定子名でも読み込み元によって違うバージョンを解決したいときに使います。
{
"imports": {
"lib": "https://esm.sh/lib@2"
},
"scopes": {
"/legacy/": {
"lib": "https://esm.sh/lib@1"
}
}
}
/legacy/ 配下から読み込まれたモジュールが import x from "lib" すると lib@1 に、それ以外からの同じ指定子は lib@2 に解決されます。スコープの適用範囲は、インポート元モジュールのURLがスコープキーのパスに前方一致するかどうかで判定されます。スコープも複数マッチしうるため、まずスコープキー同士の最長一致で対象スコープを絞り、その中のマッピングをトップレベルの imports より優先して適用します。トップレベルの imports はいわば「既定のスコープ」として最後にフォールバックされる位置づけです。
| 観点 | トップレベル imports | scopes 内マッピング |
|---|---|---|
| 適用範囲 | 全モジュールに共通 | 指定パス配下から読まれた場合のみ |
| 優先順位 | スコープがなければ採用 | 一致するスコープがあれば最優先 |
| 用途 | 既定の依存解決 | レガシー部分やサブツリーだけ別バージョン |
import mapのタイミング制約
import map はブラウザの解決に介入するため、いつ読み込まれるかに制約があります。ただしこの制約は当初の仕様より緩和されており、実装差にも注意が必要です。
- 初期の仕様およびそれを反映した実装(例: 2023年頃までのChromium)では、import mapは最初のモジュールスクリプトが評価される前に確定していなければならず、1ドキュメントにつき1つしか書けませんでした。
- その後の仕様改定(Chromium 133、Firefox・WebKit追随)で、この制約は緩和されました。1つのドキュメントに複数の
<script type="importmap">を書ける上、モジュールの評価が始まった後からでも動的に追加できます。すでに解決済みの指定子には影響しませんが、未解決の指定子には新しいマッピングが反映されます。 - 複数のimport mapで同じ指定子キーが重複した場合、エラーにはならず、先に登録されたマッピングが優先され、後から来た重複エントリは無視されます(import map自体は無効化されません)。すでに解決済みのモジュールに対する再定義も同様に無視されます。
- 外部ファイル参照(
<script type="importmap" src="...">)にも対応していますが、非同期取得である以上、後続のモジュール実行を暗黙に遅らせる形で足並みを揃える必要があります。
複数import mapの許可と動的追加は比較的新しい仕様変更です。古いブラウザや実装が追随していない環境では、2つ目の <script type="importmap"> がエラーになったり、最初のモジュール評価後の追加が無視されたりする場合があります。対象ブラウザのサポート状況を確認し、確実性が必要な場面では引き続き単一のimport mapを最初のモジュール評価前に確定させる書き方が安全です。
import() による動的インポートも、静的 import 文と同じimport mapを参照して指定子を解決します。ルーティングに応じて import(`app/pages/${name}.js`) のように組み立てる場合でも、app/ プレフィックスのマッピングがあれば解決対象になります。ただしテンプレートリテラルの埋め込み変数を含む指定子は静的解析ができないため、バンドラによる事前解決・プリロードの対象にはできません。
バンドラなし開発とCDN移行の実利
import map が実務で効くのは、依存の実体をHTML側の1箇所に集約できる点です。CDNのメジャーバージョンを上げるときはimport mapの値を書き換えるだけで済み、アプリコード中の import "react" は一切変更不要です。開発中は素の <script type="module"> を python -m http.server のような単純な静的サーバーで動かし、本番だけバンドル・圧縮する、といった段階的な使い分けも可能です。スクリプトの取得・実行タイミングそのものは レンダリングブロックとパース阻害(defer/async/moduleの差) が扱う type=module の既定挙動(defer相当)と組み合わさるため、import mapを使っても評価順の直感は変わりません。
CDN経由でサードパーティスクリプトを読み込む以上、配信経路の改ざん検出も合わせて検討する価値があります。integrity 属性によるハッシュ照合は Subresource Integrityとリソース改ざん検出 が扱う領域で、import mapのマッピング先URLに対しても同様にCORSと crossorigin の制約が関わります。クロスオリジンの解決先を使う場合の応答共有の前提は CORS(オリジン間リソース共有) の仕組みと地続きです。
まとめ
(1) ブラウザは裸の指定子を単独では解決できず、import mapがURLへの変換表を提供する。(2) 解決は登録順ではなく最長一致で、末尾スラッシュのエントリはプレフィックスマッピングとして展開される。(3) scopes は特定パス配下限定のマッピングで、一致するスコープがあればトップレベル imports より優先される。(4) 従来は最初のモジュール評価前に単一のimport mapを確定させる必要があったが、現行仕様では複数定義・動的追加が可能になり、同一指定子の重複は先勝ちで無視される(無効化はされない)。ただしブラウザ・バージョンによる挙動差があるため、確実性が必要な場面では単一のimport mapを最初のモジュール評価前に確定させる書き方が安全。
Web/フロントエンド Article
インポートマップとモジュール解決を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
JavaScript
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
解決は最長一致(longest prefix match)で、スコープ付きマッピングはパス階層内でのみ有効になり、通常マッピングより優先される。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「JavaScript / ESモジュール」に近いか確認する。
- 強みである「ブラウザは相対・絶対URL以外の裸の指定子(例: react)を単独では解決できない。import mapがモジュール指定子をURLへ変換する解決表として働く。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。