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

はじめに

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

お題と経緯

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

今回の内容

AppRouterについて調べていきたいと思います!

使用した技術

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

前回の内容

前回記事

パフォーマンス無関心で作ったサイトのパフォーマンスチューニング④ 画像のアップロード圧縮 - zare926のブログ

画像アップロード時の圧縮方法でした!

Nextjs AppRouterとは

簡単にいうとサーバーサイドでコンポーネントレンダリングをして、HTMLを返してくれる(全部じゃない)ルーティングのベース機能です。
PageRouterからどのように変わったのかは以下記事がわかりやすかったです。

ざっくりApp Router入門【Next.js】

init

$  npx create-next-app@latest
Need to install the following packages:
  create-next-app@13.5.2
Ok to proceed? (y)
✔ What is your project named? … app-router-sample-app
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias? … No / Yes

$ cd app-router-sample-app
$ code. // 設定していればvscode起動

触っていく!

PageRouterはpageディレクトリの中でルーティングさせてましたが、AppRouterはappディレクトリの中でルーティングをさせます。
またPageRouterからの変更点として以下のようなディレクトリ名かつファイル名である必要があります。

.
├── app
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx
│   ├── page.tsx
│   └── sample
│       └── page.tsx
└── components
    └── Squares.tsx

すでに初期状態からsample/page.tsxとcomponents/Squares.tsxを作成しています。
layout.tsxはPageRouterの_app.tsxの役割って感じですね。
ディレクトリごと作成できるので、仕様を分けたいページがある場合は便利です。(と解釈している)
各ファイルをみましょう。
page.tsx

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

export default function Home() {
  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>
      <Squares />
    </div>
  );
}

sample/page.tsx

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

export default function Home() {
  return (
    <div className="p-[100px]">
      <h1 className="text-gradient-blue-green" data-content="SamplePage">
        SamplePage
      </h1>
      <Link href="/" className="text-sky-600">
        Topページへ
      </Link>
      <Squares />
    </div>
  );
}

components/Squares.tsx

"use client";
import React, { useEffect, useState } from "react";

export const Squares = () => {
  const [squares, setSquares] = useState<
    {
      top: string;
      left: string;
      width: string;
      height: string;
      transform: string;
    }[]
  >([]);

  useEffect(() => {
    const generateRandomSquare = () => ({
      top: `${Math.random() * 50 + 25}%`,
      left: `${Math.random() * 100}%`,
      width: `${Math.random() * 40 + 10}px`,
      height: `${Math.random() * 40 + 10}px`,
      transform: `rotate(${Math.random() * 360}deg)`,
    });

    const initialSquares = Array.from({ length: 100 }, generateRandomSquare);
    setSquares(initialSquares);

    const interval = setInterval(() => {
      const newSquares = initialSquares.map(generateRandomSquare);
      setSquares(newSquares);
    }, 10000);

    return () => clearInterval(interval);
  }, []);

  return (
    <div className="relative h-[1000px]">
      {squares.map((square, index) => (
        <div
          key={index}
          className="border border-black rotate-animation"
          style={{
            position: "absolute",
            top: square.top,
            left: square.left,
            width: square.width,
            height: square.height,
            transform: square.transform,
            transition: "transform 10s, top 10s, left 10s",
          }}
        ></div>
      ))}
    </div>
  );
};

実際の動きはこんな感じ

動きましたね!
ということで説明をしていきます。
まず大事なルーティングですが、ディレクトリ名がurlとなり、直下にpage.tsxを作るようになりました。
/sampleでsample/page.tsxが表示されるということですね。
これらはすべてサーバーコンポーネントとなります。
そしてここがよくひっかかる!サーバーコンポーネントはuseStateが使えません。
使いたい場合は、components/Squares.tsxの一番上に定義した"use client";を設定する必要があります。
これはクライアントコンポーネントですよという宣言になり、このような制約ができているので、最初はひっかかりそう
レンダリングはサーバーからしてしまうので、state更新時にクライアントコンポーネントでないといけないという解釈をしました。

最後に

今回はほぼ準備でしたが、次回以降はサーバーコンポーネント側でfetch関数を扱い、データ取得などを行ってみようと思います。
SGやISRなど切り替わった部分を実装してみて、FirebaseへHostingしてみようと思います。
これがうまくいけば作成しているサイトもAppRouterに切り替え可能なはず!
ちなみにサーバーに負担させるメリット?的ない解説はここに載っていました。

Next.jsの新常識「App Router」を学ぼう! (1/3)|CodeZine(コードジン)

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