Top
beta

Commentary

Page Transitions

ページ遷移

Hotwireで一番最初に紹介する技術はTurbo Driveです。Turbo Driveはリンクのクリックによるページ遷移(全画面置換)の技術で、Next.jsでいうとLinkタグに相当します。

画面遷移なので、一見すると従来のMPAと変わらない動きですが、Turbo DriveもNext.jsのLinkタグも内部ではSPA的な技術を採用しており、スピード等のUI/UXが改善されています。画面数が多いウェブサイト・ウェブアプリでは、全体のUI/UXに大きなインパクトがあります

※ Turbo DriveもLinkタグも最初にサイトを訪問した時のUI/UXではなく、サイト内のリンクをクリックしたときの動作を担います

ページ遷移技術の比較
技術データロードローダー表示prefetchセキュリティその他
ネイティブ(MPA)先にロードするしないJavaScript, CSSは要再読み込み
Hotwire (Turbo Drive)先にロードするするhoverでprefetch
Next SSG先にロード要作成する△ (要DAL)動的なサイトでは使えないが、参考までに紹介
Next useEffect後にロード要作成静的な部分まで△ (要DAL)useEffect内のfetchはprefetchされない
Next page router SSR先にロード要作成しない△ (要DAL)SSRを使うとprefetchしない
Next app router Server component先にロード要作成静的な部分まで△ (要DAL)dynamic componentを使った場合はloading.jsのところまでprefetch

Turboの実装 #

HotwireはTurbo/Stimulus/Stradaの3つのパーツからなっています。ウェブで使用するのはTurboとStimulusですが、双方ともJavaScriptのファイルを読み込むだけで使用でき、インストールは簡単です。ここではTurbo Driveを使用しますので、TurboのJavaScriptファイルを読み込みます。

MPAのページにTurboのJavaScriptファイルを読み込むだけで、Turbo Driveによる画面遷移が手に入ります。 インストール方法はファイルを読み込むだけです。公式ドキュメントを確認してください。

本サイトではnpm等を使用せずに、コンパイル済みのJavaScriptファイルをダウンロードしてインストールしました。TurboのJavaScriptファイルはpublic/hotwire/javascript/turbo.es2017-esm.jsにダウンロードされており、templates/layouts/header.ejsから参照されています。

templates/layouts/header.ejs

<head>
  ...
  <script src="/hotwire/javascript/turbo.es2017-esm.js" data-turbo-track="reload" type="module"></script>
  ...
</head>

Turbo Driveを導入した時の効果はデモページでご確認ください。

ページ遷移のUX #

本サイトでは、各技術を使った際のUXを確認していただくために、デモを用意しています。上の表の各技術名をクリックして、実際にUXをご体験ください。

SPAの効果 #

上記の表に挙げた技術は、ネイティブを除いて、全てSPA (Single-page Application: シングルページアプリケーション)です。Next.jsの場合はSSRであっても、2つ目のページはSPA的に遷移します。

ここでいうSPA的というのは、ページ切り替え時にAJAX等を使っていて、一見するとページ全体は切り替わっているものの、裏でロードされたJavaScriptやCSSはそのまま残しているという意味です。先のページをメモリに残しつつ画面遷移するため、よりスムーズなページ切り替えが可能になり、またキャッシュなどのパフォーマンス最適化がしやすくなっています。

ただし最近の高性能マシンではJavaScriptやCSSの読み込みが高速であり、上記のメリットをほとんど感じることができません。実際AstroなどのフレームワークはSSRをするものの、2ページ目のSPA的遷移は省略しています。

そのため、Turbo DriveやLinkタグによるUI/UXを感じるためには、次に紹介するprefetchと組み合わせる必要があります。

Prefetchの効果 #

Turbo Driveをインストールするだけで、ページ遷移はヌルサクになります。ネイティブな画面遷移とTurbo Driveによる画面遷移を比べていただくと一目瞭然です。

この効果のほとんどはprefetchによるものです。マウスカーソルがリンクの上をホバーした時に、フライングをしてサーバにリクエストを飛ばします。そして実際にユーザがリンクをクリックしたとき、すでにリンク先ページは読み込まれていますので、瞬間的に画面遷移ができます。

Next.jsのprefetchは条件によっては動かない #

Next.jsにもprefetchがあります。しかし多くのケースでは効果がありません。Pages routerの場合、SSRのページではprefetchは効果がありません。またApp routerでDynamic renderingの場合は最初のloading.jsファイルまでしかprefetchしませんので、prefetchはローディング画面の表示を早めてくれるだけの効果しかありません。逆に言うと、Next.jsの場合、Pages routerのSSGやApp routerのStatic renderingの場合に限ってならprefetchが有効になります

本サイトの例を見ても、Next SSGの場合はヌルサクな画面遷移をしますが、Next useEffectNext app routerの場合はまずはローダー画面だけがすぐに表示され、後になってちゃんとデータのある画面が現れます。そしてNext SSRの場合はクリック直後はページが変わらず、データのある画面が現れるのが遅れます。

このようにNext.jsはprefetch機能はありますが、機能するのは静的なページのところまでです。動的なコンテンツはprefetchされません。動的コンテンツが多いサイトの場合はNext.jsのprefetchは効果がかなり限定的です。なお本サイトは動的コンテンツのサイトを作成している開発者を念頭にした解説を行っています。そのため、App routerのキャッシュは極力オフにしており、全てのページはDynamic renderingさせています。

各種の技術を使用した時の画面遷移の違い
Page Transition image

キャッシュの効果 #

Turbo Driveには仕組みをほとんど理解していなくても安心して使用できるキャッシュがあります。

以前に訪問したページに遷移すると、Turbo Driveは以前のページ内容をプレビューとして表示します(キャッシュから表示)。そして同時にサーバにリクエストを投げ、サーバから最新のページを受け取ると、すぐにプレビューの内容と入れ替えます。

キャッシュの問題は古い情報が表示されたままになってしまうことですが、この仕組みのおかげでTurbo Driveはこの問題を回避しています。つまりキャッシュを利用しているおかげでユーザとしてはページ遷移が瞬間的に起こったように感じ、UI/UXが大幅に向上する一方、キャッシュの欠点がないわけです。

これは簡単なことではありません。多くの場合、キャッシュを導入すると予想しない動きが起きてしまい、うまく利用するのが困難です。最近で言えば、Next.jsでキャッシュを導入してみたものの、評判が悪くてv15でデフォルトをキャッシュオフにしたのは記憶に新しいところです。残念ながらNext.jsではまだ代替案が用意されておらず、キャッシュを利用できるかどうかは慎重に判断する必要があります。

セキュリティ: データ漏洩 #

ここではサーバからブラウザに機密性の高い情報が流れてしまうデータ漏洩の問題を取り上げます。上記の各方式は、サーバからブラウザにデータを渡す仕組みが異なります。ネイティブとTurbo DriveはHTMLのみを送ります。Next.js pages routerのSSRはHTMLに加え、React Hydration用のデータを送り、一方でapp routerはServer Componentの場合はHTMLに加えてRSC payloadを送ります。またuseEffectの中からfetchをする場合はJSON APIを送ります。

用いる方式の違いにより、データ漏洩のリスクおよび注意しなければならないポイントが異なります。ここでは実際のデモを見せながら、具体的に紹介します。

HTMLだけを送る場合はリスクが少ない #

ネイティブ(MPA)及ぼHotwireでは、サーバはHTMLのみをブラウザに送ります。HTMLには画面に表示される情報しかありません。そのため、意図しないデータをブラウザに送ってしまう可能性は低く、データ漏洩のリスクは少ないと言えます。

またユーザの権限によって見せるべきデータを変えなければならないときは、controllerもしくはviewテンプレートの中で出し分けることが一般的で、追加のセキュリティーレイヤーなどは不要です。

JSONを送る場合は注意が必要 #

useEffectを使う場合は、useEffectの中からサーバのJSON APIに対してデータfetchをすることが一般的です。このAPIは一般にバックエンドのエンジニアが管理し、フロントエンドと合意したAPI設計に基づいて、出すべくデータと隠しておくべきデータを明確に管理します。その結果、データ漏洩が起こりにくい体制になっています。

一方でNext.jsによるSSG/SSRページ作成は、フロントエンドに任されることが多くなります。しかし実際には、Next.jsも(見えないところで)JSON APIが作られます。getStaticProps(), getServerSideProps()の返り値がそのままJSON APIになるのです。このJSON APIは2回目以降のページ遷移の時にそのままサーバからブラウザに送られます。また初回ロードの時も、SSRのHTMLの最後に、hydration用のデータとしてブラウザに送られます。このようにgetStaticProps(), getServerSideProps()の返り値はそのままブラウザに送られます。

useEffectの時のJSON APIの時と異なり、getStaticProps(), getServerSideProps()の返り値を明確に取り決めることはないと思います。しかしここで隠すべきデータをしっかりブロックしなければ、機密データは漏洩してしまいます。

セキュリティのデモ #

本デモでは、敢えてセキュリティ上問題のあるコードをバックエンドで書いています。セキュリティの問題を浮き彫りにするためです。

具体的にはUser repositoryがそのままpassword_digest(秘密の情報)を返してしまうようにしています。また各エンドポイントでもpassword_digestをブロックする処理を行っていません。

  • ネイティブ画面遷移(MPA)およびTurbo Driveを使っている場合はpassword_digestは漏洩しません。レスポンスにはHTMLしか含まれないので、画面に表示しない内容はブラウザに送信されないためです。ブラウザの検証画面のネットワークタブを確認し、送信されてくる各ファイルの中身を見ても、password_digestの情報はありません。
  • Next.js useEffectの時は/api/usersからのJSONレスポンスにpassword_digestが出てしまうように作ってありますので、ここから漏洩します。しかしこのようなAPIは注意深く設計されることがほとんどだと思いますので、問題にはなりにくいと考えられます。
  • Next.js Pages routerのSSGおよびSSRの時は、最初にダウンロードされるHTMLファイル最下部のscriptタグ中にpassword_digestが漏洩します。これはhydration用のデータであり、HTMLにpassword_digestがレンダリングされるかどうかに関わらず含まれます。またページ遷移をするたびにダウンロードされるJSONファイルにも漏洩します。
  • Next.js App routerのServer componentだけを使っている場合はpassword_digestは漏洩しません。RSC payloadはHTMLにレンダリングされる内容しか含まないためです。しかしServer componentの中にClient componentを埋め込んでいる場合はデータが漏洩する可能性がありますので、要注意です。

Next.jsをセキュアにする場合はUser repositoryのデータをそのままコンポーネントに渡さず、Data Access Layerを作り、この中で権限に応じて必要なデータのみを含むDTO(Data Transfer Object)を作成することが公式ページで奨励されています。 Reactの方でもReact Taint APIで対策されていく見込みですが、ただしこれはどちらかというと注意喚起のメカニズムだけであり、対応は別途必要になりそうです。

一方でHotwireの場合は、HTMLを出力するテンプレートファイル自身がこのようなData Access Layerの役割を果たしているとも言えます。実際、権限チェックなどはview Templateの中でやることが多いです。不要な情報が漏れ出ている場合は画面でもすぐに確認できますので、安全性が高いと言えます。

結論としてMPAやTurbo Driveを使用するときに比べ、Next.jsはデータ漏洩に神経を使う必要がありそうです。