Next.js App Router SPA 難題破解:禁用 RSC 加載,告別頁面切換延遲

Next.js 作為一款卓越的前端框架,有效解決了服務(wù)端渲染問題,顯著提升網(wǎng)站性能,有利于SEO搜索引擎爬取。

然而,在使用 Next.js 的 App Router 構(gòu)建 SPA(單頁應(yīng)用)時,如果未能妥善處理其 React Server Components(RSC)的加載機制,頁面切換時容易出現(xiàn)延遲。

當(dāng)開發(fā)者在構(gòu)建 SPA 過程中執(zhí)行router.push操作時,頁面會向服務(wù)端發(fā)起請求。

這是因為它會抓取服務(wù)端的 RSC 片段,盡管這些片段的加載時間可能僅為幾十到幾百毫秒,但對于單頁應(yīng)用(SPA)而言,頻繁的此類請求會嚴(yán)重影響用戶體驗,導(dǎo)致界面有明顯的卡頓感。

Next.js App Router 每次 router.push 都回產(chǎn)生一次rsc服務(wù)端請求, 伴隨網(wǎng)絡(luò)速度或服務(wù)器邏輯復(fù)雜度,有時候等待超過 500ms,讓用戶有明顯難受的卡頓感

解題思路

參考 Next.js 的緩存模式相關(guān)文檔App Router Caching in Next.js可知,無論采用何種緩存模式,都會發(fā)起請求以進(jìn)行緩存操作,即使請求的是緩存內(nèi)容。

再查閱Routing: Linking and Navigating文檔,其中提到 Next.js 允許使用原生的window.history.pushStatewindow.history.replaceState方法來更新瀏覽器的歷史記錄棧,且無需重新加載頁面。

我們可以借助window.history API 進(jìn)行狀態(tài)推送,從而避免加載過程,防止網(wǎng)頁重新加載。

Next.js 路由代碼架構(gòu)

在實際的文件結(jié)構(gòu)設(shè)計中,大部分頁面可采用常規(guī)的服務(wù)端渲染方式,但在特定路由下,可將路由控制權(quán)完全交由自定義邏輯處理。

對于 SPA 路由,我們可在 App Router 內(nèi)指定一個路由專門處理單頁應(yīng)用的網(wǎng)址變化,以此繞開 Next.js 默認(rèn)的路由行為。

文件結(jié)構(gòu)如下:

app
├── page1
│   └── page.tsx  # 服務(wù)端渲染的正常網(wǎng)頁
├── page2
│   └── page.tsx
└── spa
    └── […spaSlugs]
        └── spaRouter.tsx  # 自定義一個 SPA router.tsx
        └── page.tsx  # 接管 SPA 應(yīng)用內(nèi)的所有路由網(wǎng)址變化,內(nèi)部路由不再使用 App Router 編寫更多 page.tsx

接著在app/spa/[...spaSlugs]/page.tsx中,我們將會要接管自定義一個自己的一個單頁應(yīng)用的客戶端路由器。

接管客戶端路由器 (Router)

在構(gòu)建自定義的單頁應(yīng)用路由器時,可以考慮引入第三方成熟的前端路由組件,如React RouterWouter。

不過,為簡化操作,這里使用 Express 的path-to-regexp package來實現(xiàn)。

app/spa/[...spaSlugs]/spaRouter.tsx代碼如下:

import { match } from 'path-to-regexp';
import { notFound } from 'next/navigation';

// 定義路由項的類型
interface ISpaceRoute {
  path: string;
  component: (props: { params: Partial<Record<string, string | string[]>> }) => React.JSX.Element;
}

// 路由映射數(shù)組
const spaceRoutes: ISpaceRoute[] = [
   {
    path: '/spa/users',
    component: () => <div>Users List</div>,
  },
  {
    path: '/spa/users/:id',
    component: ({ params }) => <div>用戶 ID: {params.id}</div>,
  },
];


// 處理路由匹配的函數(shù),記得外部 useMemo 緩存
export const handleRoute = (url: string): React.JSX.Element => {
  for (const route of spaceRoutes) {
    const matchFn = match(route.path);
    const matchResult = matchFn(url);
    if (matchResult) {
      const params = matchResult.params;
      return <route.component params={params} />;
    }
  }
  // throw 404 page
  notFound();
};

依據(jù) Next.js 文檔 Single-Page Applications with Next.js: Shallow routing on the client,usePathname 和 useSearchParams 這兩個 API 可用于客戶端動態(tài)獲取當(dāng)前網(wǎng)址。

app/spa/[...spaSlugs]/spaRouter.tsx代碼如下:

'use client';

import { handleRoute } from './spaRouter';
import { usePathname } from 'next/navigation';
import React from 'react';

export default function SpaceRouterEntryPage() {
  const pathname = usePathname();
  const com = React.useMemo(() => handleRoute(pathname), [pathname]);
  return com;
}

需要注意的是,useParams 不能在該場景下使用,因為它不會隨window.history.pushState客戶端網(wǎng)址的變化而更新,其值是固定的,因此避免使用 useParams。

路由跳轉(zhuǎn) (Link)

經(jīng)過上述處理,我們已經(jīng)定義好了路由。但要注意,跳轉(zhuǎn)到該定義的路由不能使用 NextJS 原生的useRouter,因為這會引發(fā)問題。

可以使用如下代碼進(jìn)行跳轉(zhuǎn):

window.history.pushState(null, '', '/spa/users');

為優(yōu)化用戶體驗,我是建議采用如下鏈接形式:

<a onClick={(e) => {
  e.preventDefault();
  window.history.pushState(null, '', '/spa/users');
}} href="/asdfdsf">
  Your Link
</a>

同時設(shè)置onClick和href屬性,這樣當(dāng)用戶鼠標(biāo)滑過鏈接時,會顯示鏈接的 URL 詳情,點擊后不會真正刷新網(wǎng)頁,而僅是執(zhí)行window.history.pushState操作。

總結(jié)

通過上述一系列優(yōu)化舉措,成功攻克了在開發(fā) Bika.ai 過程中,Next.js 構(gòu)建 SPA 時頁面切換延遲的難題。

優(yōu)化后的系統(tǒng),既保留了服務(wù)端渲染在 SEO 方面的顯著優(yōu)勢,確保網(wǎng)站易于被搜索引擎抓取和索引;又賦予了SPA單頁應(yīng)用模式時,快速響應(yīng)、操作敏捷的特性,極大提升了用戶交互體驗,實現(xiàn)了兩者的完美融合。

此外,本次優(yōu)化還實現(xiàn)了路由與邏輯代碼的分離和解耦。

這一成果意義重大,意味著我們能夠?qū)?SPA 應(yīng)用獨立拆解出來,形成一個完全獨立的程序。

例如,可以另行創(chuàng)建一個基于 Next.js的工程,或者新建一個基于 Vite 框架的工程,達(dá)成一套代碼在兩個不同框架中同時運行的目標(biāo)。

以我實際操作經(jīng)驗為例,在常規(guī)網(wǎng)頁服務(wù)端,我們采用 Next.js 框架,而在打包 PC 客戶端時,借助 Tauri(或 Electron)框架,輸出獨立的HTML+JS文件,使其能夠脫離服務(wù)器獨立運行,進(jìn)一步拓展了應(yīng)用的部署和使用場景。

如果您對我們同時兼容 Next.js 與 Vite 這兩套框架的獨特支持方式頗感興趣,歡迎留言與Kelly我分享,后續(xù),我會在接下來的文章里詳細(xì)闡釋其運作方法和內(nèi)在原理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容