AstroにはPrefetch機能とIslands architectureという仕組みがあります。 「Prefetchは、ユーザーが次に遷移する画面を予測し、先読みする仕組み」 「Islands architectureは、静的HTMLの中に、動的なUIやサーバー処理をIslandとして分離し、それぞれ最適なタイミングで読み込む仕組み」 という感じです。
ここで、1つ疑問が浮かびました。
「Prefetchを使用する時、PrefetchとIslands architectureは、お互いに関係・干渉しているのか?」
なんとなく挙動のイメージはつくのですが、整理がてら本記事を書きます。
結論の先出し
「Prefetchを使用する時、PrefetchとIslands architectureは、お互いに関係・干渉しているのか?」
この疑問への答えは、以下の図で表せます。
Prefetchは図の左部分、Islands architectureは図の右部分がそれぞれの担当レイヤーになっています。すなわち、お互いに過度に干渉しているというよりは、機能として担当レイヤーが異なる、ユーザーの体験的には、担当レイヤーは連続している(Prefetch → Island処理)と言えると思います。
さてこの結論を胸に、中身を見ていきましょう。
前提情報 Prefetchとは何か
今回の記事では、PrefetchとIsland Architectureを扱うため、前提情報についてまず触れていきます。 さて、そもそもPrefetchとは何か、
ユーザーが次に遷移する画面を予測し、先読みする仕組み
上記の一言と図で表せると思います。
ただ、以下2つの部分にフレームワークやライブラリなどの設計デザイン、いわゆる哲学や思想というものが出るのだと思います。
- どのようにユーザーが遷移する画面の予測をするのか
- どのように先読みするのか
Astroの場合は、基本的にブラウザ標準のAPIを活用するスタンスをとっており、以下あたりが裏側で動いているとイメージしていただければいいと思います。
ユーザーが遷移する画面予測のトリガー
AstroのPrefetchでは、ユーザーが「次にそのリンクを選びそうか」を判断するために、hover / tap / viewport / loadの4つの戦略を選べます。デフォルトはhoverです。つまり、速さを優先するのか、不要な通信を減らすのかを、開発者がトリガー単位で調整できるわけです。
hoverは、リンクにホバーした時やキーボードフォーカスが当たった時に先読みします。実装上もmouseenter/focusinを起点にし、少し待ってから実行し、離れたらキャンセルするようになっています。tapはtouchstart/mousedownのタイミングで発火します。クリック直前なので、無駄な先読みをかなり減らしやすいです。viewportは、リンクがビューポートに入ったときに低優先度で先読みします。裏側ではIntersectionObserverベースで監視されています。loadは、ページの読み込み完了後に、そのページ内のリンクを順番に低優先度で先読みします。
この4つを見てわかるように、AstroのPrefetchは「次ページをどう読むか」だけでなく、「どの瞬間をユーザーの遷移意図とみなすか」まで含めて設計されています。
先読みのロジック
では、そのトリガーで実際に何をしているのか。Astroはここでもブラウザ標準の仕組みにかなり寄り添っています。通常のPrefetchでは、対応ブラウザなら<link rel="prefetch">を使って次ページの文書を先読みし、Safariなど<link rel="prefetch">がうまく機能しない場面ではfetch()にフォールバックします。さらにprefetch()APIの仕様上、低速回線やData Saverが有効な環境では無理に先読みしない仕組みになっています。
さらに、experimental.clientPrerenderを有効にした場合は、「Speculation Rules API」を使って、Prefetchだけでなく、Prerenderまで行うことで、次ページをより積極的に準備します。この時は、いわば既存のPrefetch処理の拡張と言えると思います。
つまり、Astroの先読みロジックはざっくり言うと以下のように整理できます。
- 通常時は
<link rel="prefetch">を優先する - 非対応ブラウザでは
fetch()にフォールバックする experimental.clientPrerenderを有効にした環境では Speculation Rules APIを活用する
この「ブラウザ標準を使い、必要な時だけフォールバックする」という姿勢は、Astroの設計思想がかなり表れている部分だと思います。
詳しくは、以前いくつか記事を書きましたので以下をご覧いただけると幸いです!
- プリフェッチから学ぶWebパフォーマンスの最適化
- Astroはなぜ速い?プリフェッチの内部実装を追う
- Speculation Rules API から見るブラウザのプリフェッチ戦略
- AstroとNext.jsのPrefetchを比較し、アプローチの違いを整理する
Islands architectureとは何か
概要
さて、ここからが本題です。Islands architectureとはなんでしょうか。
公式Docsを読めばなんとなく掴めるかと思います。
また、AstroのIslands architectureを理解するにあたり、Zennの記事も有用だと思います。このIslands architectureは、Next.jsのPPRと表面上は似ている為、比較記事も存在します。比較しながら考えることで、理解が深まると思うので添付しておきます。筆者の方々ありがとうございます!
上記を読みつつ、まとめると。Islands architectureとは、
静的HTMLの中に、必要な動的UIやサーバー処理をIslandとして埋め込む仕組み
この一言で表せると思います。いや、流石に、こんな単純ではないかもしれません。そもそもこのIslandという概念は、元来Astro発祥ではなく、外から取り入れられたものです。間違ったことは言えないので、以下Astroの公式ドキュメントを引用すると
引用:https://docs.astro.build/ja/concepts/islands/2019年にEtsyのフロントエンドアーキテクトKatie Sylor-Miller氏が初めて提案しました。その後、Preactの創始者Jason Miller氏が2020年8月11日の記事で概念を拡張し、体系立てて紹介しました。
そのため歴史的には、以下の感じです。
- Katie Sylor-Miller
- Jason Miller(Preact創始者)
Jason氏が布教した以降は、色々なフレームワークに影響を与えていたり、取り入れられています。ただ布教と言う点でAstroはAstro以降のフレームワークやライブラリにかなり大きな影響を与えていると思います。(Astroが好きなので、私のポジショントーク感は否めませんが…)
理解する上で、押さえるべき要点
AstroのIslands architectureを理解する上で、最低限押さえておきたいのは以下の3点です。
- Astroは、デフォルトでHTML・CSSのみを返す(Island未経由でのJavaScriptの扱いは、こちらへ)
- インタラクティブなUIを埋め込みたい場合など、特定の状況下ではIsland経由でハイドレーション1が必要
- その「Island」には、ブラウザで後からhydrateされるものと、サーバー側で別タイミングに描画されるものがある
Astro自体は、MPA方式のフレームワークで、Islands architectureは、AstroのデフォルトゼロJSや他のReactなどUIフレームワークを埋め込める特徴とともに語られることが多いですが、これら3つの特徴は、お互いに深く関係しあっていると言えると思います。
他のReactなどUIフレームワークを埋め込める特徴
の部分をSPA方式でAstroとは異なる思想により機能しているJSコードの塊としてみてみましょう。 あら不思議、3つともJSに関わる技術だと言えると思います。そういう意味で、Astroは非常にJavaScriptの扱いにこだわっているように感じますし、Islands architectureは、「どこにJavaScriptが必要か」だけでなく、「いつ実行するか」「どの単位で切り離すか」を考えるための設計でもあるなと感じます。
余談JavaScriptをIsland経由以外で書いたらどのように扱われるか
Astroでは、Islandを使用せずともJavaScriptを書くことができ、仮に.astroを使用したプロジェクトの場合、主に以下3通りの書き方があると思います。
- コンポーネントスクリプトの内に書く(.mdなどで言うfrontmatter部分。コードフェンスの内のこと)
- コンポーネントテンプレートで
<script>タグを使用する - src配下に
*.tsまたは、*.jsファイルを作成・配置し、.astroファイル内にて、<script src>でインポートする
簡潔に扱いを整理すると以下のような感じですね。
ケース1. コンポーネントスクリプトの内に書く
コンポーネントスクリプト内に書いたJavaScriptは、ビルド時またはサーバー描画時に実行されます。HTMLを作るための処理に使われ、ブラウザには送られません。
ケース2. コンポーネントテンプレートで<script>タグを使用する**
テンプレート内の<script>は、クライアントスクリプトとしてブラウザに送られます。DOM操作やイベント処理など、ページ上の振る舞いを書く場所です。
ケース3. src配下に*.tsまたは、*.jsファイルを作成・配置し、.astroファイル内にて、<script src>でインポートする
src配下の.tsや.jsを<script src>で読み込んだ場合も、ブラウザで実行されるクライアントスクリプトとして扱われます。テンプレート直書きよりも、再利用しやすかったり、長文のコードの場合はメンテナンスしやすいのが利点です。
2種類のIslandがあるよ?
Astroのドキュメントを見ていくと、Islandと一口に言っても役割の違う2つのIslandがあります。
Client Island
Client Islandは、インタラクションが必要なUIを、必要なタイミングでだけブラウザ側にhydrateする考え方です。Astroでは、UIフレームワークのコンポーネントも最初はHTMLとして出力され、そのうえでclient:load / client:idle / client:visible / client:media / client:onlyといったdirectiveに応じて、あとからJavaScriptを読み込んで動き出します。
要するに、Client Islandの関心事は「このUIをブラウザでいつ動かすか」です。ここが、後で出てくるPrefetchとの大きな違いでもあります。
Server Island
一方のServer Islandは、動的なコンテンツや重いサーバー処理を、ページ本体とは切り離して後から差し込むための仕組みです。server:deferを付けると、Astroはまずページ全体のHTMLを返し、その後で特別なエンドポイントにGETリクエストを送り、Server Islandの中身を別途取得します。slot="fallback"を用意しておけば、取得が終わるまでの仮表示も出せます。
つまり、Server Islandの関心事は「この部分をサーバーからいつ返すか」です。Client Islandがブラウザ側のhydrateタイミングを扱うのに対して、Server Islandはサーバー側のHTMLレスポンスタイミングを扱う、と捉えると整理しやすいです。
PrefetchとIslands architectureを実行フロー順に整理する
まず、両者がカバーしている範囲を分けておくとかなり理解しやすくなります。
Prefetchは、今見ているページから、次に遷移するかもしれないページをどこまで先読みしておくか という仕組みです。つまり、主に担当しているレイヤーは、「遷移の前」です。
一方Islands architectureは、遷移した先のページを、どの単位で・どのタイミングで動かすか を扱う仕組みです。こちらの主戦場は「遷移した後のページ内部」です。
この時点で、両者はどちらも「情報の取得方法を制御する目的」を持ちながらも、基本的には別のレイヤーを担当していることがわかります。
通常のPrefetchを前提にすると、実行フローはだいたい以下の順番で整理できます。
- 現在のページ上で、Astroがリンクに対して
hoverやtapなどのトリガーを待っている - 条件が揃うと、Astroが次ページの文書を
<link rel="prefetch">あるいはfetch()で先読みする - この段階では、まだユーザーは遷移していないので、次ページのClient Islandがhydrateされるわけではない
- 実際に遷移が起きると、先読み済みの文書を土台に次ページが表示される
- その後で、次ページ内のClient Islandが
client:*の条件に従ってhydrateされる server:deferを使ったServer Islandがある場合は、fallbackを見せつつ別リクエストで中身が差し込まれる
1・2部分が、Prefetch。4・5・6がIslands architectureの担当レイヤーです。
こうして見ると、通常のPrefetchとIslands architectureは、競合というよりバトンタッチの関係です。Prefetchが「次のページを先に用意する」役割を担い、Islands architectureが「そのページのどこを、いつ、どの環境で動かすか」を担うわけです。
まとめ
今回は、PrefetchというAstroの機能を通して、Islands architectureを観察してみました。 PrefetchとIslands architectureは、どちらもユーザー体験を良くするための仕組みですが、通常は見ているレイヤーが異なります。Prefetchは「遷移前に次ページをどう準備するか」、Islands architectureは「遷移後のページ内をどう分割し、いつ動かすか」を扱っています。
なので、通常のAstro Prefetchでは、両者は干渉するというより、前後の工程を分担していると考えるのが自然です。ただし、experimental.clientPrerenderでprerenderまで踏み込むと、遷移前から次ページの実行準備が進む可能性があるため、ここで初めてIslands architectureとの重なりが増えるのではないかと思います。
今回の件でIslands architectureについてより深く調査しましたが、以前より更にAstroの特徴として上がる理由がわかった気がします。Islands architectureは、Reactなどを埋め込む、JSハイドレーションの文脈で触れられることが多いですが、違う視点で見てみると、それらの埋め込みなどは、あくまで副産物な気もしました。
今後もいろいろ観察していこうと思います。本記事を読んでいただきありがとうございました。
皆さんもLet’s be an Astronaut!
参考資料
文中で触れたものも含め、参考にした資料をまとめておきます。
- プリフェッチから学ぶWebパフォーマンスの最適化
- Astroはなぜ速い?プリフェッチの内部実装を追う
- Speculation Rules API から見るブラウザのプリフェッチ戦略
- Prefetch | Astro公式Docs
- Experimental client prerendering | Astro公式Docs
- Islands | Astro公式Docs
- Template directives reference | Astro公式Docs
- Server islands | Astro公式Docs
- Speculation Rules API | MDN
-
rel=prefetch - HTML | MDNprefetch キーワードを <link> 要素の rel 属性に設定すると、ユーザーが将来の操作でターゲットリソースを必要とする可能性が高く、したがってブラウザーはリソースを先読みしてキャッシュすることでユーザーの使い勝手を向上させることができる可能性があることをブラウザーに示唆するものです。 <link rel="prefetch"> は同じサイトのナビゲーションリソース、または同じサイトのページで使用するサブリソースに使用されます。
developer.mozilla.org
- Astro Prefetch tap処理 | GitHubリポジトリ
- Astro Prefetch hover処理 | GitHubリポジトリ
- Astro Prefetch viewport処理 | GitHubリポジトリ
- Astro Prefetch load処理 | GitHubリポジトリ