iT邦幫忙

2021 iThome 鐵人賽

DAY 18
1
Modern Web

從零開始學習 Next.js系列 第 18

Day18 - 如何在頁面中預先載入其他的頁面 (prefetch)

  • 分享至 

  • xImage
  •  

前言

在前面的章節已經介紹了 Next.js 是 file-based routing 的架構,在路由至其他頁面時,通常會使用到 <Link /> 這個 component,這個 component 提供不少的 props 可以針對不同的情況做設定,今天我們要了解 prefetch 這個 props 的功用,它可以讓切換頁面更有效率。

要注意的是 prefetch 只在 production 環境有用!

啟動一個 Next.js 專案

也許你會需要修改 global.css: https://gist.github.com/leochiu-a/5276029a65017c660bb91dcba6bab53c

一個很常見的起手式,我們使用 create-next-app 以 typescript 啟動一個 Next.js 的專案:

$ npx create-next-app --typescript

創建完後,你可以在 pages/ 這個資料夾中看到類似以下的資料夾結構,原本預設的檔案沒有 products.tsx ,這是後來加上去的,我們將使用 //products 兩個頁面來做實驗:

資料夾結構

我們將 pages/index.tsx 的內容改成以下這個模樣,首頁中的內容只有包含一個 <Link> ,可以點擊路由到 /products 頁面。在前面的章節介紹過 <Link> 裡面的 children 只能是 string 或是 HTML 的 <a> ,如果使用其他的 tag 則會報錯:

import type { NextPage } from "next";
import Link from "next/link";

const Home: NextPage = () => {
  return (
    <Link href="/products">
      <a>link to products</a>
    </Link>
  );
};

export default Home;

接著看到 pages/products.tsx 的內容,它是一個 SSR 的頁面,透過 getServerSideProps 傳遞一個 products 的字串,並在 component 中渲染這個字串,非常地單純:

interface Props {
  products: string;
}

const Products = ({ products }: Props) => {
  return <div>{products}</div>;
};

export const getServerSideProps = async () => {
  return {
    props: { products: "products" },
  };
};

export default Products;

prefetch 一個 SSR 頁面

各位讀者要先知道「 prefetch 只在 production 環境有用」,亦即使用 yarn dev 啟動開發用伺服器什麼事情都不會發生,我們要使用 yarn buildyarn start 將 Next.js 應用打包後,再啟動伺服器:

$ yarn build
$ yarn start

接著你可以在瀏覽器中看到 [http://localhost:3000](http://localhost:3000) 中的首頁內容如下:

screely

使用 Chrome 的 devtool 看看 /prodcuts 的 chunk 是不是已經被預先載入了:

  • 打開 Chrome 的 devtool (cmd + option + i)
  • 點擊 Network tab
  • 取消勾選「Disable cache」
  • 重新整理頁面

你可以看到 /products 的 chunk 已經被預先下載,而且 Size 的欄位是 prefetch cache,意思是會儲存在 Chrome 的 prefetch cache 中:

screely

為什麼 /products 的 chunk 可以預先載入?

Next.js 之所以可以做到這件事要歸功於 HTML 的 <link> ,在 <head> 加上以下這種寫法,可以「預先載入未來可能被用到的資源」:

<link rel="prefetch" href="/images/big.jpeg" />

pages/index.tsx 這個頁面中可以看到加入了 <Link href='/products'> 的內容,而 Next.js 知道使用者可能會點擊連結切換到 /products 這個頁面,所以便讓這個頁面成為會被「prefetch」的資源。

screely

取消 /products 頁面被 prefetch

在 Next.js 9 以後,使用 <Link> 都會被預設加入到 prefetch 的資源中,如同上面看到的例子,雖然我們沒有在 <Link> 傳入 prefetch={true} ,但是 /products 這個頁面仍然會被加入到 prefetch 的資源中。

但是有時候並不是所有使用 <Link> 的頁面都需要被 prefetch,所以很直覺地在 <Link> 上傳入 prefetch={false} ,將會使得該頁面不會被 prefetch。

我們以 pages/index.tsx 這個頁面中的 <Link> 為例,修改頁面中的內容:

<Link href="/products" prefetch={false}>
  <a>link to products</a>
</Link>

同樣執行 yarn buildyarn start 後,瀏覽該頁面的 Elements tab,會發現在 <head> 中找不到 /products 被預先載入的訊息。

screely

當然,如果再切換到 Network tab,就看不到 /products 的 chunk 被載入。

Conditional rendering 的 <Link> 會觸發 prefetch 嗎?

在頁面上總是會有一些 <Link> 並非是一開始就渲染在頁面上,而是使用者跟頁面互動過後,達成某些條件才渲染在畫面上,讀者們也許就會有這樣的問題「條件式渲染的 <Link> 也可以讓頁面的 chunk 被 prefetch 嗎?」

我們來實驗看看,修改 pages/index.tsx 中的內容,以條件式渲染的方式顯示 <Link> ,在使用者點擊按鈕後, <Link> 才會出現在畫面上:

const Home: NextPage = () => {
  const [visible, setVisible] = useState(false);

  return (
    <div>
      {visible && (
        <Link href="/products">
          <a>link to products</a>
        </Link>
      )}

      <button onClick={() => setVisible(true)}>show link</button>
    </div>
  );
};

同樣執行 yarn buildyarn start 後,瀏覽該頁面的 Elements tab,會發現在 <head> 中找不到 /products 被預先載入的訊息。

但是,在點擊「show link」的按鈕之後,你會發現 <head> 中動態載入了以下內容,讓 /prodcuts 的 chunk 成為會被預先載入的資源:

screely

此時再打開 Network tab,原本沒有預先載入的 products chunk 資源,也在 <link> 被設定後被載入儲存到 prefetch cache 中。

screely

router.push 的自定義路由也能夠 prefetch 嗎?

在有些情況 <Link> 也許不能夠滿足需求,必須使用 next/routeruseRouter 在切換頁面之前執行一些操作,例如驗證表單、 GA 事件等等,在這種情況要怎麼 prefetch 頁面呢?

可以使用 router.prefetch 將指定的頁面作為 prefetch 的頁面,這種做法跟 conditional rendering 的狀況有些相似,從「檢視原始碼」中會發現原本的頁面中是不包含 <link rel="prefetch"> 的資源,但是在頁面載入之後, router.prefetch 再動態地指定 prefetch 的資源,讓瀏覽器自動預先抓取資源。

const router = useRouter();

useEffect(() => {
  router.prefetch("/products");
}, [router]);

要特別注意的是, router.prefetchyarn dev 下不會起作用。

以下為 pages/index.tsx 的範例程式,在頁面載入時動態地 prefetch /products 的頁面 chunk,在點擊按鈕後觸發 handleClick() 後,此時 /products 頁面已經被預先載入,所以就可以有效縮減 /products 的頁面載入時間

const Home: NextPage = () => {
  const router = useRouter();

  useEffect(() => {
    router.prefetch("/products");
  }, [router]);

  const handleClick = () => {
    router.push("/products");
  };

  return (
    <div>
      <button onClick={handleClick}>to /products</button>
    </div>
  );
};

小結

在這篇文章中,我們了解了 Next.js 的 prefetch 機制,並且知道 <Link> 在預設的情況下都會 prefetch 頁面。如果是在 conditional rendering 的頁面中,只要 <Link> 被渲染後,也可以動態地 prefetch 指定的頁面。

而有時候 <Link> 無法滿足我們的需求,會使用到 router.push 這種自定義路由的方式,此時可以使用 router.prefetch 達到 prefetch 頁面的效果。

最後,再次提醒不論是 <Link> 或是 router.prefetch,prefetch 這個特性只有在 production 的環境下才能使用, yarn dev 是無法觸發 prefetch 資源的 。

Reference


上一篇
Day17 - 把 Next.js 部署到 Vercel 上吧!
下一篇
Day19 - 寫出更有品質的程式碼,信 eslint 得永生
系列文
從零開始學習 Next.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
AndrewYEE
iT邦新手 3 級 ‧ 2023-02-16 15:48:38

請問NextPage是做什麼用的呢? 為何要使用NextPage?

我要留言

立即登入留言