SCRIPT 要素の変遷から触れる Web の進化

はじめに

こんにちは。Repro で Booster の開発をしている杉浦と申します。

最近は JavaScript の盛り上がりが凄いですね。今ではブラウザ内にとどまらず、サーバサイドでも活用される様になりました。 これには、言語仕様が整理されたり機能が強化されたり、非常に大きな発展があったという点が大きいです。

実は、言語としての JavaScript だけではなく、最近 HTML との境界インタフェースとしての JavaScript の仕様も最近かなり明確化されてきています。 自分も HTML5 の最初のあたりまでは把握していたのですが、Booster の開発に携わる中で久しぶりに確認したところ、随分と仕様が進化し複雑になっていました。

今回はそんな HTML 規格の変化部分の紹介と、過去からの HTML と JavaScript の流れを振り返る簡単なまとめです。

HTML 仕様と SCRIPT 要素 の簡単な歴史

まず、過去の JavaScript と HTML の関係を簡単に確認してみましょう。

最初の JavaScript と HTML 2.0

JavaScript が登場したのは 1995 年。当時の最新ブラウザだった Netscape のバージョン 2.0 の新機能として作られました。 HTML に動的な要素を加えるのはもちろん、ブラウザの機能を様々操作するのが目的でした。ウィンドウを開いたりする部分は今でも使われていますが、当時使用頻度が大きかったのはプラグインとのブリッジインタフェースです。 少し時代がずれますが、Netscape3.0 当時の InfoWorld の記事 を見るとなんとなくノリがわかります。今からすると、カジュアルに document.write しているのにびっくりしますね。

この時のスクリプティング要素はブラウザ独自のもので、同じ 1995 年に規定された HTML2.0 (RFC1866) の仕様には SCRIPT 要素の記述は全くありません。

初めての SCRIPT の仕様化 (HTML 3.2)

HTML の仕様に SCRIPT 要素が登場するのは 1997 年、 HTML 3.2 からになります。この HTML3.2 の参考仕様にある SCRIPT の説明はわずか1段落。しかも STYLE 要素と共に紹介されているだけでした。

スクリプト言語の候補としての JavaScript (HTML 4.0)

次の 1998 年、HTML4.0 では独立した Script の章が追加されJavaScript は「選択可能な言語の1つ」として紹介されています。 またこの時、 document.write がHTML パーサが組み立てた DOM ツリーに動的な変化を起こすことができるという言及があります。 defer 属性もこの時に追加され、defer な要素では document.write が使えないとされています。

余談ですが、HTML 4.0 の規格上は text/tclスクリプトが例として挙げられているのですが、tcl が実際に HTML のスクリプティングとして使われているところを見たことはありません。この頃の HTML 規格はどちらかというと「現実を仕様に落とし込む」というよりは、「理想の HTML」を目指していた側面が大きかったからでしょう。

JavaScript が HTML 仕様上の標準言語に (HTML5)

HTML 4 からしばらくすると Ajax や SPA が考案され、より動的な Web のためスクリプトの需要は更に高まっていきます。そして 2008 年の HTML5 の最初の WorkingDraft では、ついに JavaScript が標準の言語として規定され type 属性が省略できるようになりました。また、 async 属性が導入されました。

そして、ここからスクリプトの実際に広まっている挙動の仕様化が少しずつ進んでいきます。 規格上で読み込みと実行の手順を定められ、挙動制御のための 内部的なフラグとして parser-insertedalready executed という 2 つが定義されました。

そして、HTMLのパース手順がある程度明確化され、ずっと暗黙的に使われていた document.write をHTMLパーサの中でどう扱うのか明示されるようになりました。 SCRIPT は defer が指定されるなど、いくつかの条件を満たさない限りはパーサに割り込めるモードで動作します。この状態の SCRIPT は先程のフラグを使って parser-insertedスクリプトと呼ぶことになりました。

(HTML5 WD Jan 2008 - Parsing HTML document から引用)

そして最新の HTML Living Standard では?

現在の HTML の規格は W3C から WHATWG に移され、また今までのようにバージョン番号をふるのではなく、更新され続ける "Living Standard" として管理されています。 実際には HTML5 の時代も Working Draft として何度も更新されていましたので、やり方としてはそんなに変わっていません。

さて、最新の 2023/09/25 時点の仕様では SCRIPT 要素はどうなったでしょうか?  HTML5 WD 初版からの変更点をまとめると以下の様になります (WD の後続の版で変更されたものも含みます)。

属性の追加

  • nomodule
  • crossorigin
  • integrity
  • referrerpolicy
  • blocking
  • fetchpriority

type 属性の位置づけ変更

既に JavaScript が基本となっている type ですが、そこに JavaScript の種別を示すものとして moduleimportmap が利用可能になりました。MIME-Type も引き続き利用可能ですが、非 JavaScript のものに関しては、単なるデータブロックとして扱われる事が明示されました。

また、これに伴い asyncdefer の動きも再整理されています。

(4.12.1 The script element から引用)

SCRIPT 要素の内部状態定義の大幅な拡充

現在では内部状態は "flag" と呼ばれることはなくなり、単に "associated pieces of state" (関連した状態)となりました。

そしてこの内部状態ですが、already executed が削除(置き換え)されたほか、新しいものが大量に追加されています。 ざっと上げると以下のようになります (SCRIPT 固有でない、関係するものも含みます)。

  • parser document (parser-inserted)
  • preparation-time document
  • type (type 属性の値ではなく、内部的なタイプ。null classic, module, importmap のどれかになる)
  • force async
  • from an external file
  • ready to be parser-executed
  • already started
  • delaying the load event
  • result
  • steps to run when the result is ready
  • connected
  • scripting is disabled
  • script nesting level
  • parser pause flag

ここでそれぞれの詳細には触れませんが、SCRIPT の今の動きを何とか仕様化したいという意図が感じられる量ですね。 今まで個々のブラウザが暗黙に処理していた内容が、このような名前付きの状態により表現されることになりました。

ちなみに、これらの内部状態はあくまでブラウザ (UserAgent) が管理するものであって、現状 DOM API などを利用してスクリプトから値にアクセスすることは出来ません。

スクリプトの準備と実行ステップの詳細化

スクリプトの実行手順に関して、HTML 4 まではブラウザに任させれており、HTML5 WD 初版でも動作中のスクリプトが更に script タグを追加した場合の手順が書かれているのみでした。

今の HTML Standard では、パーサが SCRIPT に到達した際の実行準備および実行後の処理手順が定義されるようになっています。 ここでは、上で挙げた内部状態の更新や、状態による挙動の切り替えが結構詳細に書かれており、現状 34 ステップ、枝番を含むと 60 ステップ程度もあります。

パース手順の厳密化

ブラウザがネットワークから HTML を受け取り、トークナイザに渡される前のステップが分割されました。 これに伴い document.write から入ってくる位置も更新されています。

(Overview of the parsing model から引用)

まとめ

というわけで一通り仕様上の SCRIPT の変更点を見てきましたが、現在の HTML Standard はかなり頑張って既存のブラウザの挙動を仕様に落とし込もうとしているように見えます。

例えば、 createElementで作成したscript要素が非同期になるのか、script要素をコピーすると再実行されるかなど、細かい挙動はHTML5の初期では明確ではありませんでした。

これらブラウザに任せられていた仕様が、基本的には既存の挙動を元に、かなりの部分が仕様として明示されるようになってきています。

ただ一方、慎重に後方互換性が維持されているため、標準動作が今では嬉しくないものも残っています。

例えば、単純に SCRIPT 要素を書くと parser-inserted スクリプトとして扱われます。つまり、パーサを一時停止させたり、投機的な先行パース結果を破棄する必要が発生するため、その処理が必要ない場合は単にパフォーマンスを下げるだけになってしまいます。 現在の通常の使い方であれば、多くの場合は必要ないでしょう。

最新のツールなら対応された適切なタグを出力する期待ができますが、外部のスクリプトスニペットが最適化されておらず、追加していく際にどんどんサイトが重くなっていくケースもかなり見受けられます。

おわりに

以上、簡単ですが HTML 仕様上の SCRIPT の変遷をまとめてみました。

このような仕様やその経緯は特定の機能開発に直接結びつくものではありません。それでも前提として頭に入れておくと、素早いトラブルシュートやより踏み込んだ最適化など幅広いアプローチができるようになると感じています。

現在 Repro Booster はこういったことも含め、様々な手法でサイト高速化に取り組んでいます。まだ課題はありますが今後の発展に期待いただければ幸いです。

また、Repro ではさまざまなポジションで開発者を募集しています。少しでも興味がある方は是非 Wantedly のページをご覧ください!

https://www.wantedly.com/companies/repro/projects