現時点(2023/04/18 執筆時点)では<script type="importmap">
はhead内のサブリソース(cssやjsなど)の読み込みより先に書いておいたほうが安全である。
というのもfirefoxでちょっと変な挙動を確認したからだ(v112.0.1 (64-bit))
挙動の概要
端的に問題をまとめるとimport-mapsのJSON記述の前にちょっと大きめのブロッキングを起こすリソースがある場合、import-mapsによるパス解決がなされないケースがあるというものだ。
ちなみにchrome, edge, saferiでは問題は確認できなかった。
再現コードを用意してみた。
上記のページをfirefoxでみると執筆時点では以下のようなエラーがでる
Uncaught TypeError: The specifier “@hotwired/stimulus” was a bare specifier, but was not remapped to anything. Relative module specifiers must start with “./”, “../” or “/”.
import文が"./","../", "/"から始まっていないことを指摘するエラーである。
HTMLは以下のようになっている。
<script src="/javascripts/blocking.js"></script><!-- (1) -->
<script type="importmap">
{
"imports": {
"@hotwired/stimulus": "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.1/+esm"
}
}
</script>
<script src="/javascripts/loader.js" type="module"></script><!-- (2) -->
(1)のスクリプトは中で数百msループを回して大きめのブロッキングを再現させている
(2)で読まれているスクリプトの中にimport {Application} from '@hotwired/stimulus'
という記述があり、そこで本エラーがでている。
しかしながらimportmapで@hotwired/stimulus
に対する実URLのマッピングはなされているので問題ないように見える。
問題の原因
これは関連issueなど漁ってみるとspeculative parsingによる影響らしい。
通常HTMLのparserはdefer/asyncのついてないscriptと遭遇すると、document.writeによるHTML本文の書き足しを考慮してscriptを評価し終えるまでparserをストップさせる
しかしながらページ内にscriptがn個あったとして、遭遇するたびにリクエスト、評価を繰り返していくとパースがかなり遅くなってしまうので、そういったサブリソースが発見された場合、メインスレッドのHTML parse自体は止まるのだが、別のスレッドを作ってHTML本文中に他のサブリソースがないかを探し、あれば先にリクエスト、パースだけしておくという挙動がある。
これをspeculative parsingという。
これにより、import-mapsの定義の前に大きなブロッキングを発生させるscriptがあった場合、HTML parserは止まり、import-mapsの解析は進まないまま、後続のscriptのリクエストとパースが始まる。
後続のscriptがtype="module"だった場合、実行はDOMContentLoadedの直前になるが、パース自体は先に走ってるのでパースエラーがあった場合はその時点でエラーが確定する。
import文が"../","./", "/"から始まっていない場合、パースエラーとして扱われるため、import-mapsの解析が終わる前に後続のmoduleがパースされると本エラーが発生するというものだ。
chrome等も同じような実装はあるはずだが問題は起きてないのでよしなにやってるらしい。
ちなみにこれについて話されてるのは以下のissueである。
すぐ解決されそうか?
一応修正方針はきまっているらしく対応スレっぽいのがある。
しかしながら優先度は低そうなので修正までもうちょいかかるかもしれない
(prioriry) P3 Backlog
(Severity) S4 (Small/Trivial) minor significance, cosmetic issues, low or no impact to users
どうやって凌ぐ?
firefoxをサポートするのであればそれぞれのサブリソースの読み込み記述の前にimport-mapsの定義をもってくるのが無難だと思われる(冒頭に戻る)
前述のコードを例にとると(1)のスクリプトより前にimport-mapsの定義があればこの影響をうけなくなるので問題とは遭遇しない
<script type="importmap">
{
"imports": {
"@hotwired/stimulus": "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.1/+esm"
}
}
</script>
<script src="/javascripts/blocking.js"></script><!-- (1) -->
<script src="/javascripts/loader.js" type="module"></script><!-- (2) -->