第7章 Turbo Drive の基本
この章のねらい
第5章で作った Relay は、まだ Hotwire のカスタマイズを何もしていません。それでも、リンクをたどると画面が一瞬で切り替わります。これは Turbo Drive が働いているからです。
この章では、その Turbo Drive の心臓部を理解します。鍵になるのは visit(訪問)という考え方です。第3部のあいだ、この言葉を何度も使います。
この部ではページ遷移を visit として理解します。フォーム送信には別のライフサイクルがあり、その応答から visit へ進む場合があります。Frame の読み込みや Stream の適用も、通常の Drive visit と区別します。
7.1 通常のページ遷移
まず、Turbo がなかった頃のページ遷移を思い出します。
従来の Rails では、リンクをクリックするたびに、ブラウザは次のことをしていました。
- サーバーに新しいページを要求する
- 返ってきた HTML で、ページ全体を捨てて作り直す
- 新しい文書で CSS と JavaScript を処理する(HTTP キャッシュを利用できる場合は再転送しない)
この作り直しには、いくつかの問題があります。毎回ページ全体を再構築するため、表示が途切れる場合があります。戻る操作ではブラウザの bfcache やスクロール復元が働くこともあり、常に白くなるわけではありません。そして、JavaScript で保持していた状態(開いていたメニューなど)も、すべてリセットされます。
ページの大部分(ヘッダー、サイドバー、CSS、JavaScript)は前のページと同じなのに、毎回まるごと作り直しているわけです。ここに無駄があります。
7.2 Turbo Drive の visit と body 差し替え
Turbo Drive は、この無駄をなくします。リンクのクリックを横取りし、ページ全体の作り直しではなく、必要な部分だけの差し替えに変えます。
Turbo Drive がリンク遷移で行うことを、本書では visit と呼びます。visit の中身は次のとおりです。
- リンク先の HTML を、バックグラウンドで取得する(
fetch) - 取得した HTML の
<body>で、いまの<body>を差し替える <head>は、まるごと捨てずにマージする(7.3 で扱います)
既存のページを表示しながら次の HTML を取得し、表示の途切れを減らします。<head> で読み込み済みの CSS や JavaScript も、読み込み直されません。だから、JavaScript の実行環境を毎回ゼロから立て直さずに済みます。これが、Relay のリンク遷移が速い理由です。
なお、visit には 2 種類あります。リンクのクリックなど新しいページへ進むときの visit(application visit)と、ブラウザの戻る・進むで起きる visit(restoration visit)です。restoration visit はスクロール位置やキャッシュと関わります。この章では主に application visit を扱い、restoration visit とキャッシュは第9章で扱います。
なお、速くなるのは「読み込み済みの資産を読み直さない」からであって、<body> の中身は差し替わります。開いていたメニューのような body 内の UI 状態は、特別な指定をしない限りリセットされます(状態を保つ方法は第9章で扱います)。
大切なのは、サーバー側は何も特別なことをしていない点です。Relay のコントローラは、通常どおり HTML を返しているだけです。その HTML をどう適用するか(全体を作り直すか、body だけ差し替えるか)を、ブラウザ側の Turbo Drive が引き受けています。
7.3 head のマージと data-turbo-track="reload"
visit では <body> を差し替えますが、<head> は扱いが異なります。<head> には CSS や JavaScript の読み込み指定が入っています。これを毎回読み込み直すと、Turbo Drive の利点が消えてしまいます。
そこで Turbo Drive は、<head> を捨てずにマージします。新しいページで増えた要素は取り込み、すでに読み込み済みの CSS や JavaScript はそのまま使い続けます。
ただし、困る場面があります。アプリをデプロイして CSS や JavaScript の中身が変わったときです。古い CSS のまま body だけ差し替え続けると、見た目が壊れます。
これを防ぐのが data-turbo-track="reload" です。Rails の既定レイアウトでは、CSS の読み込みにこの指定が付いています。
app/views/layouts/application.html.erb
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
ここで追跡対象になるのは、data-turbo-track="reload" を付けた要素です。上の例では、stylesheet_link_tag に自分でこの属性を指定した CSS がそれにあたります。Turbo Drive は、visit のたびに追跡対象の新旧を比べ、内容が変わっていたら(=デプロイで CSS が更新されていたら)、body だけの差し替えをやめて、ページ全体を読み込み直します。
2 行目の javascript_importmap_tags は、import map を使って JavaScript を読み込むためのタグをまとめて生成するヘルパーです(第6章)。追跡したい資産には、このように data-turbo-track="reload" を付けて監視対象にする、と理解しておけば十分です。
つまり、普段は visit で速く、アセットが変わったときだけ安全にフルリロードする、という使い分けが自動で行われます。
7.4 progress bar
visit では、リンク先の HTML をバックグラウンドで取得します。回線が遅いと、取得に時間がかかることがあります。その間、画面は前のページのままなので、ユーザーには「クリックが効いたのか」がわかりません。
Turbo Drive は、これに備えて progress bar(進捗バー)を用意しています。visit に時間がかかると、画面上部に細いバーが表示されます。
ここで 1 つ知っておくべき仕様があります。progress bar は、visit を始めてすぐには出ません。既定では 500 ミリ秒経ってから表示されます。多くの遷移は 500 ミリ秒以内に終わるため、その場合はバーが出ません。一瞬で終わる遷移にまでバーを出すと、かえってちらついて見えるからです。
この遅延時間は変更できますが、まずは「すぐに終わる遷移ではバーは出ない」という既定の動きを理解しておけば十分です。
7.5 visit を無効化する
Turbo Drive は、同一オリジンの対象リンク・フォームを自動で扱います。外部オリジン、download 属性、対象外拡張子や明示的な無効化などは除外されます。ほとんどの場面ではそれで問題ありませんが、ときには visit させたくないリンクもあります。たとえば、別のサーバーが返すファイルのダウンロードや、Turbo と相性の悪い外部ページへの遷移です。
そうしたリンクには、data-turbo="false" を付けます。
<%= link_to "PDF をダウンロード", report_path(format: :pdf), data: { turbo: false } %>
data-turbo="false" が付いたリンクは、Turbo Drive が横取りせず、従来どおりのページ遷移になります。
この指定は、囲んだ範囲にもまとめて効かせられます。あるコンテナ全体で visit を切り、その中の一部だけ visit を戻したい場合は、内側で data-turbo="true" を指定します。
無効化はあくまで例外的な手段です。Turbo Drive を切るほど、Hotwire を使う意味は薄れます。「ここは visit させない方がよい」とはっきり言える場面に限って使ってください。
第7章では、Turbo Drive のリンク遷移を visit として理解しました。次の 第8章 では、フォーム送信から visit へ進む流れ、そして成功と失敗でコントローラが返すべきものが決まっていることを見ます。
参考資料
- Turbo Drive(Handbook): https://turbo.hotwired.dev/handbook/drive
- Turbo の属性リファレンス: https://turbo.hotwired.dev/reference/attributes