パフォーマンス無関心で作ったサイトのパフォーマンスチューニング番外編 Nextjs AppRouter(色々試す)

はじめに

こんにちわ!こんばんわ!ご観覧ありがとうございます。
定期的に雑色な記事を投稿します。(タイトルは記事のメインの内容にしてますが)
基本的には抱えている問題を改善し、改善方法を記事にしてアップしようと考えています。

お題と経緯

NextjsのPageRouterからAppRouterに乗り換えてみてパフォーマンスを測ってみたいし、使ってもみたいなってことで、
今回はFirebaseのhostingでAppRouterがどこまで機能するかを、パフォーマンス向上の一環として試そうと思います!
まずはAppRouterでサンプルのAppを作成しつつ、備忘録で説明を書いていこうと思います。

今回の内容

画像の圧縮方法を記録していきたいと思います!

使用した技術

Category     Technology Stack
Frontend Nextjs,React,Typescript
Backend Typescript,Node,CloudFunction
Database Firestore
Design   XD  

前回の内容

前回記事

パフォーマンス無関心で作ったサイトのパフォーマンスチューニング番外編 Nextjs AppRouter(fetch) - zare926のブログ

NextjsのAppRouterでSGやSSR、ISRのfetchについてやってみました。

色々試す!

今回は色々試す回にします。
まずはクライアントコンポーネントから動的なfetchをしてみます。
FetchComponentとButtonのコンポーネントを作成

FetchComponent.tsx

"use client";
import React, { useState } from "react";
import { Button } from "./Button";

const FetchComponent = () => {
  const [user, setUser] = useState<{ id: number; name: string }>();
  const testFunc = async () => {
    const number = Math.floor(Math.random() * (10 - 1 + 1)) + 1;
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/users/${number}`
    );
    const user: { id: number; name: string } = await response.json();
    return user;
  };
  const fetchUser = async () => {
    const response = await testFunc();

    setUser(response);
  };
  return (
    <div>
      <Button text="fetch button" onClick={fetchUser} />
      <p>
        クライアントコンポーネントからfetchした値 :
        {user && <span>{user.name}</span>}
      </p>
    </div>
  );
};

export default FetchComponent;

Button.tsx

"use client";
import React from "react";

type Props = {
  text: string;
  onClick?: () => void;
};

export const Button = (props: Props) => {
  const { text, onClick } = props;
  return (
    <button
      className="bg-gray-200 hover:bg-gray-300 active:bg-gray-400 border border-gray-400  p-2 transition-all duration-300 ease-in-out"
      onClick={() => {
        onClick && onClick();
      }}
    >
      {text}
    </button>
  );
};

このコンポーネントをルートのpage.tsxに組み込む

import { FetchComponent } from "@/components/FetchComponent";
import { Squares } from "@/components/Squares";
import Link from "next/link";

export default async function Home() {
  const response = await fetch(`http://localhost:3000/sample-date-get`, {
    next: { revalidate: 5 },
  });
  const data = await response.json();

  return (
    <div className="p-8 relative">
      <h1 className="text-gradient-blue-green" data-content="TopPage">
        TopPage
      </h1>
      <Link href="/sample" className="text-sky-600">
        Sampleページへ
      </Link>
      <p>{data.data}</p>
      <FetchComponent />
      <Squares />
    </div>
  );
}

json placeholderでuserの名前を引っ張ってきてますが、うまく動いていますね。

ちなみにクライアントサイドでのfetch関数にISRの設定をしてもcacheはされなかったです。 caches APIなどクライアントサイドでは、他の方法でcacheする必要がありそうですね。

次にサーバーコンポーネントからfetchをしてcacheの設定をしてみます。

ChildrenServerComponent.tsx

import React from "react";

export const ChildrenServerComponent = async () => {
  const response = await fetch(`http://localhost:3000/sample-date-get`, {
    next: { revalidate: 20 },
  });
  const data = await response.json();

  return (
    <div>
      <p>
        サーバーサイドコンポーネントからfetchした値 :<span>{data.data}</span>
      </p>
    </div>
  );
};

page.tsx

import { ChildrenServerComponent } from "@/components/ChildrenServerComponent";
// import { FetchComponent } from "@/components/FetchComponent";
import { Squares } from "@/components/Squares";
import Link from "next/link";

export default async function Home() {
  const response = await fetch(`http://localhost:3000/sample-date-get`, {
    next: { revalidate: 5 },
  });
  const data = await response.json();

  return (
    <div className="p-8 relative">
      <h1 className="text-gradient-blue-green" data-content="TopPage">
        TopPage
      </h1>
      <Link href="/sample" className="text-sky-600">
        Sampleページへ
      </Link>
      <p>{data.data}</p>
      <ChildrenServerComponent />
      <Squares />
    </div>
  );
}

しっかりとサーバーでfetchされ表示されました。

親のサーバーコンポーネントではrevalidateを5秒に設定し、子のサーバーコンポーネントでは20秒にしましたが、同じAPIだったからなのか、親の5秒に引っ張られました。
逆に親の5秒をコメントアウトしたところ、親を含めて20秒の設定になりました。
APIを分けた場合、それぞれの設定が生かされました。

てとこですかね。

最後にクライアントコンポーネントの子にサーバーコンポーネントを設置してみます。

FetchComponent.tsx

"use client";
import React, { useState } from "react";
import { Button } from "./Button";
import { ChildrenServerComponent } from "./ChildrenServerComponent";

export const FetchComponent = () => {
  const [user, setUser] = useState<{ id: number; name: string }>();
  const testFunc = async () => {
    const number = Math.floor(Math.random() * (10 - 1 + 1)) + 1;
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/users/${number}`
    );
    const user: { id: number; name: string } = await response.json();
    return user;
  };
  const fetchUser = async () => {
    const response = await testFunc();

    setUser(response);
  };
  return (
    <div> 
      <Button text="fetch button" onClick={fetchUser} />
      <p>
        クライアントコンポーネントからfetchした値 :
        {user && <span>{user.name}</span>}
      </p>
      <ChildrenServerComponent />
    </div>
  );
};

page.tsx

import { FetchComponent } from "@/components/FetchComponent";
import { Squares } from "@/components/Squares";
import Link from "next/link";

export default async function Home() {
  const response = await fetch(`http://localhost:3000/sample-date-get`, {
    next: { revalidate: 5 },
  });
  const data = await response.json();

  return (
    <div className="p-8 relative">
      <h1 className="text-gradient-blue-green" data-content="TopPage">
        TopPage
      </h1>
      <Link href="/sample" className="text-sky-600">
        Sampleページへ
      </Link>
      <p>{data.data}</p>
      <FetchComponent />
      <Squares />
    </div>
  );
}

SC<CS<SC(async/await有)という設定ですが、結論これはダメでした。

クライアントコンポーネントは、コンポーネント単位でasync/awaitを使えないので、子のコンポーネントがサーバーサイドでasync/awaitを使用しているとエラーが出ます。
サーバーコンポーネントがasync/awaitを使っていなければ、今回の様な親子関係も可能です。

ちなみにSC<SC<CSは問題なく動きます。
この辺りは慣れが必要そうですね!

最後に

色々と把握できた回で満足でした!
ちなみに今回出たエラーを翻訳すると

エラー:async/awaitはクライアント・コンポーネントではまだサポートされていません。このエラーは、本来サーバ用に書かれたモジュールに誤って `'use client'` を追加してしまった場合によく起こります。

なんですが、将来的に対応する様になるんですかね。
そうするとかなり自由度があるようなないような。。。
動的なアプリを作る場合は結構考えものです。

ここまでみていただいてありがとうございました!!!