やかんです。

「お気づきだろうか」と生意気な書き出しをしてしまいすみません。

※今回の記事はやや専門的な内容になります。

ブログの動作をちょっと速くした。

言葉の通りでございますが、今までブログの動き遅かったですよね。すみません。

お詫び申し上げます。

具体的には、

  • キャッシュを利用するようにした
  • 取得するデータを小分けにした

という2点で、速度の改善を図りました。

東大生やかんのブログ
やかん

実際どのくらい違いが出ているのかは分かりません

実装

今回リファクタしたのは投稿の取得周りです。一言で言うと、「今までは1つの投稿につき『前後の投稿』も同時に取得していたが、リファクタによって、『前後の投稿』を後回しにするようにした」となります。

「前後の投稿」とは、

↑これのことです。1つの投稿につき、次の投稿の情報、前の投稿の情報を取得したいというモチベでした。で、今までは「表示している投稿、その次の投稿、その前の投稿」を1度に取得していたから処理に時間がかかっていたんですが、それを「まず表示している投稿を取得して、それが終わったらその次の投稿、その前の投稿を取得する」というように処理を分けることで1つあたりの処理を軽くしました。

具体的な実装を以下に貼ります。wordpressとnextjsを利用したブログ作成などの参考になれば。

まずこれが投稿自体のコード。

import { format } from "date-fns";
import { Suspense } from "react";
import { MdCalendarMonth } from "react-icons/md";

import { AroundPostNav } from "@/components/around-post-nav";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { BeatLoader } from "@/components/ui/loader";
import { Separator } from "@/components/ui/separator";
import { WidthWrapper } from "@/components/width-wrapper";
import { getPost } from "@/lib/get-post";

type Props = {
  slug: string;
};

export async function PostContent({ slug }: Props) {
  const post = await getPost({ slug });

  return (
    <WidthWrapper>
      <div className="flex flex-col space-y-8">
        <h1 className="my-4 text-3xl">{post.title}</h1>
        <div className="flex items-center justify-between">
          <div className="flex items-center">
            <div>
              <Avatar>
                <AvatarImage
                  src="/yakan.png"
                  width={400}
                  height={400}
                  alt="yakan"
                />
                <AvatarFallback>やかん</AvatarFallback>
              </Avatar>
            </div>
            <p>やかん</p>
          </div>
          <div className="flex items-center">
            <MdCalendarMonth className="inline-block h-4 w-4" />
            <p>{format(new Date(post.date), "yyyy/MM/dd")}</p>
          </div>
        </div>
        <Separator className="my-4 bg-slate-400" />
        <div
          className="post-content"
          dangerouslySetInnerHTML={{ __html: post.content }}
        />
        <Separator className="my-4 bg-slate-400" />
        <Suspense
          fallback={
            <div className="mt-[20vh] flex min-h-screen justify-center">
              <BeatLoader color="#475569" />
            </div>
          }
        >
          <AroundPostNav post={post} />
        </Suspense>
      </div>
    </WidthWrapper>
  );
}

で、AroudPostNavというのが、「次の投稿」「前の投稿」のボタンの部分。コードは以下。

import { BsCaretLeftFill, BsCaretRightFill } from "react-icons/bs";

import { ButtonLink } from "@/components/ui/button-link";
import { getAroundPost } from "@/lib/get-post";
import { Post } from "@/lib/type";

type Props = {
  post: Post;
};

export async function AroundPostNav({ post }: Props) {
  const nextPost = await getAroundPost({ post, direction: "next" });
  const prevPost = await getAroundPost({ post, direction: "prev" });

  return (
    <div className="flex justify-between">
      {nextPost ? (
        <ButtonLink href={`/${nextPost.slug}`}>
          <BsCaretLeftFill className="inline-block h-4 w-4" />
          次の投稿
        </ButtonLink>
      ) : (
        <div />
      )}
      {prevPost ? (
        <ButtonLink href={`/${prevPost.slug}`}>
          前の投稿
          <BsCaretRightFill className="inline-block h-4 w-4" />
        </ButtonLink>
      ) : (
        <div />
      )}
    </div>
  );
}

で、ちょいちょい登場しているgetPost関数、getAroundPost関数は以下。コメントとか雑ですが。

import { notFound } from "next/navigation";
import { z } from "zod";

import { getPostsPrimitive } from "@/lib/get-posts";
import { postSchema } from "@/lib/schemas";
import type { Post } from "@/lib/type";
import { wpPrefix } from "@/lib/utils";

/**
 * 記事によってfetchリクエストの詳細を変える必要がある
 */
const pageSlugs = ["about-yakan", "about-blog", "privacy-policy"];

const fetchPost = ({ slug }: { slug: string }) => {
  if (pageSlugs.includes(slug)) {
    return fetch(`${wpPrefix()}/pages?slug=${slug}`);
  } else {
    return fetch(`${wpPrefix()}/posts?slug=${slug}`, {
      // next: { revalidate: 3600 },
      // NOTE: 現状、投稿した記事を編集することってあまりないから、キャッシュを使うようにする。
    });
  }
};

const getPostArgumentSchema = z.object({
  slug: z.string(),
});
type GetPostArgument = z.infer<typeof getPostArgumentSchema>;

async function getPostPrimitive({ slug }: GetPostArgument) {
  const res = await fetchPost({ slug })
    .then((res) => res.json())
    .catch((err) => {
      console.error(err);
      throw new Error("Failed to fetch data");
    });

  if (res.length === 0) {
    notFound();
  }

  const post: Post = {
    id: res[0].id,
    date: res[0].date,
    title: res[0].title.rendered,
    slug: res[0].slug,
    content: res[0].content.rendered,
  };

  return post;
}

/**
 * 前後の記事の取得
 */
const getAroundPostArgumentSchema = z.object({
  post: postSchema,
  direction: z.union([z.literal("prev"), z.literal("next")]),
});
type GetAroundPostArgument = z.infer<typeof getAroundPostArgumentSchema>;

export async function getAroundPost({
  post,
  direction,
}: GetAroundPostArgument) {
  const params =
    direction === "prev"
      ? `before=${post.date}&order=desc`
      : `after=${post.date}&order=asc`;

  const aroundPost = await getPostsPrimitive({
    params,
  });

  if (aroundPost.length === 0) {
    return null;
  }

  return aroundPost[0];
}

/**
 * getPost
 */
export async function getPost({ slug }: GetPostArgument) {
  const post = await getPostPrimitive({ slug });

  return post;
}

wordpressから記事を取得する場合の参考になれば嬉しいですが、なかなか曲者ですよねー。zod使ってサーバーサイドでバリデーションをかけたいところですが、wordpress rest apiの返り値がいつまでも同じスキーマを使ってくれるとも限りませんし、そもそも外部依存のスキーマを自分で実装したくはないです。壊れやすくなるので。

あ、あとキャッシュもためるようにしました。

ということで、こちらの記事は以上となります。最後までお読みいただき、ありがとうございます。