⏱️

僅かなJavaScriptでリンク先をホバーで先読みをする仕組みを実装する

2024/01/22に公開

そもそもプリフェッチとは

ユーザーが特定のページにアクセスする前にバックグラウンドでページを事前に読み込んで置くというテクニックです。
筆者はあまりNext.jsには詳しくないのですが、Next.jsでも結構しっかり使われているようです。

https://zenn.dev/frontendflat/articles/nextjs-prefetch

Rails(Turbo)でもプリフェッチの仕組みが今後導入されそう

また、RailsでおなじみのHotwiredのTurboでもリンクをホバーすることでリンク先をプリフェッチする仕組み(ここではInstantClickと言います)を実現するPRが提出され、
DHH率いる37Signalsも好意的に受け入れていて、BaseCampでこのTurboのPRをどうやら検証しているようです。

https://github.com/hotwired/turbo/pull/1101

ちょっと一昔に一斉を風靡したdev.toもこのInstantClickを利用して、
リンク先のプリフェッチを行っていて、サクサク軽快に動くユーザー体験を実現するためのテクニックであると言われています。
https://developers.forem.com/frontend/instant-click

実際にホバーしたときにプリフェッチするのはどのくらい効果があるのか?

InstantClickはリンクをクリックするユーザーはリンクの上にマウスカーソルを移動して、クリックするまでに200ms - 300ms経過します。
ということを言及してなるほどなぁと思いました。擬似的に体験できるサイトもあるので、こちらでテストしてみてください。
http://instantclick.io/click-test

touchstartとかの場合は効果あるの??

ぶっちゃけほぼないと思っている。
Next.jsの場合は、ホバーのタイミングによるプリフェッチの他に、ViewportPrefetch、つまりコンテンツが表示されたタイミングでプリフェッチを開始するようにしているらしい。

MPAでできないんだっけ?

しかし、TurboもInstantClickもPjax、PushStateとAjax(XHR)を組みわせたものであり、HTMLをJavaScriptでレンダリングしていて、本当の意味のMPAではないと言えます。
Next.jsやTurbo、InstantClickのようなライブラリがなくても、プリフェッチの仕組みを作れるのではないか?と思い、リンク先ををホバーして先読みを行うInstantClickをMPAで実装してみることにしました。

HTMLのPrefetch属性を使う

HTMLには便利なものとして、結構昔からプリフェッチを行うための属性というか機能があります。

HTMLのPrefetch属性

ここにあるリンクのプリフェッチを利用します。

https://developer.mozilla.org/ja/docs/Glossary/Prefetch#リンクのプリフェッチ

prefetchで指定されたリンクは表示しているページの読み込みが完了した後で、取得を開始します。(

対応状況

対応状況としては2024/01/18現在、SafariにはiOSもmac Safariにも対応していませんが、それ以外のブラウザではいい感じに動きそうです。

https://caniuse.com/?search=prefetch

簡単に作ってみる

実は下記のコードで実現できます。JavaScriptの場合、下記のコードで実現ができます。
下記のコードはHTMLクラスにprefetchがついているAタグを対象に、
mouseoverをするかtouchstartでプリフェッチを行います。

Productionで使う場合は、外部サイトの場合やdownloadの場合はプリフェッチしないとか、そういった細かいハンドリングが必要かと思います。

function preloadLink(event) {
    const href = event.currentTarget.getAttribute('href');
    const link = document.createElement('link');
    link.rel = 'prefetch';
    link.href = href;
    document.head.appendChild(link);
}
const links = document.querySelectorAll('a.prefetch');
links.forEach(link => {
    link.addEventListener('mouseover', prefetchLink);
    link.addEventListener('touchstart', prefetchLink);
});

挙動

プリフェッチがおこなわれているかをチェックする

実装して、プリフェッチの挙動をネットワークで確認し、画面遷移をしたときに下記のようになっていれば成功です。プリフェッチキャッシュというのが表示されているかと思います。

遷移したときにプリフェッチした結果を利用するため、画面遷移も一瞬で完了します。
また再度ホバーをしてもブラウザキャッシュの期限切れになるまでまたプリフェッチは始まりませんでした。

プリフェッチ中に画面遷移した場合

ざっくりChromeで動かした結果、振る舞い的にはプリフェッチしてるときにデータを読み込んでも引き継がれ、例えば2秒かかるページを、プリフェッチして1秒後にアクセスしたら残り1秒読み込んだ段階でページが表示されました。

このような挙動を示すので、プリフェッチをする際に中断がどうだとかはあんまり深く考えなくてもブラウザでよしなにやってくれそうな感じがしました。

⚠注意点

ChromeではCache-Control: no-cache, privateとかだとキャッシュされなかった。
ブラウザによってキャッシュポリシーが異なるなどの差異については調べていない。
Railsで取り込まれそうなPRについてはキャッシュの保持期間が10秒程度なので、
そこに合わせてCache-Controlを設定しておいてみました。

class PagesController < ApplicationController
  def show
    response.headers["Cache-Control"] = "public, max-age=10"
  end
end

さらに発展させるために

touchstarhよりももっと早くプリフェッチをしたいという場合は、
Next.jsの挙動を参考にHTML要素がViewPortに表示されたらプリフェッチを行うように変更してみるのがいいかなと思っています。

まとめ

MPAであってもHTMLのPrefetchを使うことで、Next.jsもTurboもInstantClickも使わないで、軽快なページ遷移をわずか数十行のコードで実現できました。
このようにブラウザは進化していて、標準的に実装されている機能だけでもここまで実装できることがわかりました。
皆様も、このようにブラウザの標準的な機能を活かすことでより良い体験を簡単に作ってみてはいかがでしょうか?

OSIRO テックブログ

Discussion