Day18 - 如何在页面中预先载入其他的页面 (prefetch)

前言

在前面的章节已经介绍了 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


<<:  Day 18:产地直送,先拿再用-Vuex State、Getters

>>:  Day 18 Docker Compose 简介和安装

Day 13 - 半自动标签图片的方法与实作

Day 13 - 半自动标签图片的方法与实作 以下介绍一个自制的简易的半自动标签图片的作法,主要是因...

前端工程师也能开发全端网页:挑战 30 天用 React 加上 Firebase 打造社群网站|Day26 根据主题筛选文章列表

连续 30 天不中断每天上传一支教学影片,教你如何用 React 加上 Firebase 打造社群...

Powershell 入门添加参数帮助信息

我们写的脚本不仅仅是自己使用,有时需要分享给别人使用。这种情况下,帮助信息可以更好地帮助使用者,使用...

Day15 X Tree Shaking

在昨天我们学会了 code splitting 与 dynamic import 的技巧,让程序在...

D03 / 怎麽摆放我的画面 - Layout & Alignment

今天大概会聊到的范围 basic layout arrangement & alignme...