Commentary
ページ遷移
ウェブで一番多いのはリンクをクリックして、別のページに遷移する操作です。ページ遷移は一番基本のUI/UXであり、ユーザの体感に非常に大きな影響を与える大事なものです。
また開発者としては、一番よく作るインタラクションですので、開発効率を高める上でもコードが簡単に書けることが非常に重要です。セキュリティもしっかり確保する必要があります。
ここでは各種の技術を紹介しながら、その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 |
まずは一番基本的なブラウザネイティブなMPAによるページ遷移を確認します。MPAによる画面遷移のデモをご確認ください。またコードは/users
のページと/products
をご覧ください。
一番下のボタン(Products/Usersへ遷移)で/users
と/products
の間を画面遷移しますが、これがブラウザネイティブなMPAの画面推移です。
ごく普通のHTMLページです。通常通りにa
タグを使ったリンクを使用していて、非常に簡単です。
<a href="/api/hotwire/products"
class="btn-primary">Productsへ推移 (Turbo Off)</a>
いまだにMPA的なページ推移がウェブで大多数です。例えばAmazonや楽天はMPA的な画面遷移をします。Appleもそうですし、ソフトバンクもMPAです。
次にHotwire Turbo Driveによるページ遷移を確認します。Turbo Driveのデモはここから確認できます。またTurbo Driveのコードは先のMPAの場合とほとんど変わりませんがGitHubで確認できます。
layouts/header.ejs
ファイルを読み込んでいますが、ここの中の<script src="/hotwire/javascript/turbo.es2017-esm.js" ...>
でTurbo Driveを読み込んでいます。turbo.es2017-esm.js
のファイルは公式ドキュメントの通りにコンパイル済みのJavaScriptファイルをダウンロードしたののです。a
タグです。templates/page_drive/users.ejs
<%- include("../layouts/header.ejs") -%>
templates/layouts/header.ejs
<!doctype html>
<html lang="ja">
<head>
...
<script src="/hotwire/javascript/turbo.es2017-esm.js" data-turbo-track="reload" type="module"></script>
...
</head>
...
Next.js側はまず最初にSSGの例を紹介します。Next.jsのSSGはページをビルド時に作成するものです。本サイトは主に頻繁にデータが更新されるウェブアプリを想定していて、本来であればSSGはこれにマッチしないのですが、Next.js SSRとの比較のために紹介しています。
コードは/users
、および/products
で紹介しています。
getStaticProps()
の中で直接allUsers()
でUser情報を取得するものになっています。
export async function getStaticProps() {
const users = await allUsers()
return {props: {users}}
}
getStaticProps()
によってページを作成しますので、データが頻繁に更新されるタイプのウェブサイトでは使用できません。今回は比較のために例示しましたが、本サイトの他のページと同じ条件ではありませんNext.jsのSSRを使わない場合は、useEffect()
などを使ってページに表示するデータを読み込みます。純粋なSPAを作るときによく採用されている手法ですが、Next.jsの場合もしばしば使われます。
useEffect()
を使ったデモはここからご確認いただけます。またコードはUsers側および
Products側をGitHubに公開しています。
下記のようにuseEffect()
の中でfetch()
を使い、データを取得しています。そのデータはsetUsers()
でステートに詰め込み、画面を表示させています。また途中のローディング画面を表示するためにsetLoading()
でロード中表示のステートもセットしています。
なお下記のコードはエラー処理が含まれていません。エラーを正しく処理し、ソフト404ページを表示したり、ユーザにフィードバックするのであればコードはさらに複雑になります。
useEffect(() => {
console.log("Fetch start for Users useEffect")
fetch("/api/users").then(res => res.json())
.then(data => {
setUsers(data)
setLoading(false)
})
},[])
Page routerのSSRは初回アクセス時のページローディングが速いことや、SEOに強いなどのメリットがあります。これはいずれも初回ロード時の話のみです。一旦ユーザがウェブサイトを訪問した後は、2回目以降のページ遷移がUI/UX上重要になります。Page router SSRのデモサイトはこちらにあります。
Page router SSRのコードはUsers側とProducts側をご覧ください。
なお、下記のコードのhideLoadingIndicator
のところは、ローディングアニメーションに関わるところで、デモでのみ使用するものです。通常の実装では不要です。
それ以外のところは比較的シンプルで、単にpageコンポーネントに渡すusers
を用意しています。エラー処理を含めるともう少し複雑になります。
pages/users_ssr/index.tsx
// Simulate Next.js acting as a BFF for a JSON API server
export async function getServerSideProps(context: GetServerSidePropsContext) {
console.log("Fetch start for Users SSR")
const res = await fetch(process.env.URL + "/api/users")
const users = await res.json()
const hideLoadingIndicator = !!(context.query.hide_loading_indicator);
return {props: {users, hideLoadingIndicator}}
}
Server ComponentはSSRとよく似ていますが、Suspenseを使ってローディングアニメーションを簡単に表示できるという特徴があります。このおかげでフィードバックがないという上記のSSRの問題点を解決できます。デモはこちらに用意しています。
コードは比較的簡単で、getUsers()
関数でusers
を取得し、コンポーネントの中で表示しています。なお、time
を取得しているのはNext.js v14のキャッシュの挙動を確認するためで、デモだけのために用意しています。
またloading.tsx
のファイルはローディングアニメーションを表示するものです。
app/users_app/page.tsx
// Simulate Next.js acting as a BFF for a JSON API server
async function getUsers(): Promise<User[]> {
const res = await fetch(process.env.URL + "/api/users")
const users = await res.json()
return users
}
export default async function UsersAppIndex() {
const users = await getUsers()
const time = new Date().toLocaleTimeString()
...
app/users_app/loading.tsx
export default function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return <>
<div className="w-full mt-24">
<Image src={rocketImage} alt="loader" className="w-16 h-16 mx-auto"/>
</div>
</>
}
useEffect()
の場合とUI/UXは非常に似ていますuseEffect()
と同じになっただけですdynamic rendering
)はprefetchが効きません。新しいページが表示されるまでの時間が短縮されないのはこのためです。上述のように、Hotwire Turbo DriveからNext.js Server Componentsに至るまで、ページ遷移のUI/UXを改善するための技術は多数使われています。SPA, prefetch, キャッシュ, suspenseなどがこれに該当します。
しかしすべてが高い効果を発揮するわけではなく、ケースバイケースでもあります。
上記の表に挙げた各ページ遷移法は、ネイティブ(MPA)を除いて、すべてSPA (Single-page Application: シングルページアプリケーション)です。
ここでいうSPAは、ページ切り替え時にAJAX等を使っていて、一見するとページ全体は切り替わっているものの、裏でロードされたJavaScriptやCSSはそのまま残しているという意味です。前のページをメモリに残しつつ画面遷移するため、よりスムーズなページ切り替えが可能になります。
ただし最近のデバイスではJavaScriptやCSSの読み込みが高速であり、上記のメリットをほとんど感じることができません。実際AstroなどのフレームワークはSSRをするものの、2ページ目のSPA的遷移は省略していて、単純なMPAとして動作します。
このように、SPAというだけではページ遷移のUI/UXの改善は期待できなくなっています。ただしSPAはメモリを維持してくれるため、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は以前のページ内容をプレビューとして表示します(キャッシュから表示)。そして同時にサーバにリクエストを投げ、サーバから最新のページを受け取ると、すぐにプレビューの内容と入れ替えます。
Next.jsもキャッシュがありますが、キャッシュの更新処理がわかりにくく、古い情報が残ってしまいやすい問題がありました。評判が悪いために、v15ではデフォルトがキャッシュオフになりました。Next.jsのキャッシュは特別なチューニングが必要なレーシングカーのようなもので、仕組みを熟知している開発者が丁寧にウェブサイトを作る場合は非常に効果的ですが、通常利用では取り扱いに注意を要するものでした。
一方でTurbo Driveのキャッシュの場合は古い情報は一時的に表示されますが、すぐに最新情報に更新されます。サーバ負荷、ネットワーク負荷低減効果はありませんが、優れたUI/UXに加え、開発者として扱いやすい利点があります。