Commentary
ページ遷移
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 |
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をご体験ください。
上記の表に挙げた技術は、ネイティブを除いて、全て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と組み合わせる必要があります。
Turbo Driveをインストールするだけで、ページ遷移はヌルサクになります。ネイティブな画面遷移とTurbo Driveによる画面遷移を比べていただくと一目瞭然です。
この効果のほとんどは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 useEffect、Next app routerの場合はまずはローダー画面だけがすぐに表示され、後になってちゃんとデータのある画面が現れます。そしてNext SSRの場合はクリック直後はページが変わらず、データのある画面が現れるのが遅れます。
このようにNext.jsはprefetch機能はありますが、機能するのは静的なページのところまでです。動的なコンテンツはprefetchされません。動的コンテンツが多いサイトの場合はNext.jsのprefetchは効果がかなり限定的です。なお本サイトは動的コンテンツのサイトを作成している開発者を念頭にした解説を行っています。そのため、App routerのキャッシュは極力オフにしており、全てのページはDynamic renderingさせています。
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を送ります。
用いる方式の違いにより、データ漏洩のリスクおよび注意しなければならないポイントが異なります。ここでは実際のデモを見せながら、具体的に紹介します。
ネイティブ(MPA)及ぼHotwireでは、サーバはHTMLのみをブラウザに送ります。HTMLには画面に表示される情報しかありません。そのため、意図しないデータをブラウザに送ってしまう可能性は低く、データ漏洩のリスクは少ないと言えます。
またユーザの権限によって見せるべきデータを変えなければならないときは、controllerもしくはviewテンプレートの中で出し分けることが一般的で、追加のセキュリティーレイヤーなどは不要です。
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
をブロックする処理を行っていません。
password_digest
は漏洩しません。レスポンスにはHTMLしか含まれないので、画面に表示しない内容はブラウザに送信されないためです。ブラウザの検証画面のネットワークタブを確認し、送信されてくる各ファイルの中身を見ても、password_digest
の情報はありません。/api/users
からのJSONレスポンスにpassword_digest
が出てしまうように作ってありますので、ここから漏洩します。しかしこのようなAPIは注意深く設計されることがほとんどだと思いますので、問題にはなりにくいと考えられます。script
タグ中にpassword_digest
が漏洩します。これはhydration用のデータであり、HTMLにpassword_digest
がレンダリングされるかどうかに関わらず含まれます。またページ遷移をするたびにダウンロードされるJSONファイルにも漏洩します。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はデータ漏洩に神経を使う必要がありそうです。