Next.js / Next.js кэш систем

Next.js кэш систем

Next.js-н хамгийн хүчтэй, мөн хамгийн ойлгоход хэцүү сэдвүүдийн нэг бол кэш систем. Зөв ашиглавал апп маш хурдан ажиллана — нэг хүсэлтийн үр дүнг хадгалж, дараагийн хүсэлтэд дахин тооцоолохгүйгээр шууд буцаана. Энэ хичээлд кэшний үндсэн ойлголтуудыг практик жишээгээр ойлгоно.

Кэш гэж юу вэ?

Сурагч /courses хуудас нээх бүрт сервер файлуудыг унших, JSON задлан шинжлэх, HTML үүсгэх ажиллагааг давтан хийх шаардлагагүй. Нэг удаа үүсгэсэн үр дүнг кэш-д хадгалж, дараагийн хүсэлтэд тэрийг л буцаана.

код
Эхний хүсэлт:   хэрэглэгч → сервер → файл унших → HTML үүсгэх → кэш хадгалах → хэрэглэгч
Дараагийн хүсэлт: хэрэглэгч → кэш → хэрэглэгч   (сервер хүртэл очдоггүй!)

fetch кэш

Next.js-н fetch нь анхнаасаа кэшлэдэг. cache болон next.revalidate сонголтоор зан үйлийг тохируулна:

typescript
// lib/api.ts

// 1. Анхны кэш — нэг удаа татаж хадгална (build дээр)
const staticData = await fetch("https://api.example.com/courses", {
  cache: "force-cache", // Анхны утга, заримдаа дурдахгүй ч ижил
});

// 2. Кэшлэхгүй — дүр бүрт шинэ өгөгдөл татна
const freshData = await fetch("https://api.example.com/notifications", {
  cache: "no-store",
});

// 3. Тогтмол хугацааны дараа шинэчлэнэ (ISR)
const revalidatedData = await fetch("https://api.example.com/leaderboard", {
  next: { revalidate: 60 }, // 60 секундэд нэг удаа шинэчлэнэ
});

| Сонголт | Зан үйл | Хэзээ ашиглах | | ------------------------- | ------------------------- | --------------------------- | | cache: 'force-cache' | Build дээр нэг удаа татна | Статик агуулга | | cache: 'no-store' | Дүр бүрт татна | Бодит цагийн өгөгдөл | | next: { revalidate: N } | N секундэд нэг удаа | Тогтмол шинэчлэгдэх өгөгдөл |

Route segment кэш — revalidate экспорт

fetch-д биш харин хуудас бүхэлдээ хэр олон шинэчлэгдэхийг тохируулахад revalidate константыг экспортолно:

tsx
// app/courses/page.tsx
import { getAllCourses } from "@/lib/courses";

// Бүх энэ хуудасны fetch дуудлагууд 3600 секундэд нэг удаа шинэчлэгдэнэ
export const revalidate = 3600; // 1 цаг

export default async function CoursesPage() {
  const courses = await getAllCourses(); // Файл системээс унших — кэшлэгдэнэ
  return (
    <main>
      {courses.map((c) => (
        <div key={c.slug}>{c.title}</div>
      ))}
    </main>
  );
}
tsx
// app/leaderboard/page.tsx
// Энэ хуудас кэшлэгдэхгүй — дүр бүрт шинэ өгөгдөл татна
export const dynamic = "force-dynamic";

export default async function LeaderboardPage() {
  // ...
}

unstable_cache — Функц кэшлэх

fetch биш харин өөрийн функцийг кэшлэхэд unstable_cache ашиглана. Файл системийн функц, Supabase query зэрэгт тохиромжтой:

typescript
// lib/courses.ts
import { unstable_cache } from "next/cache";
import { readFile, readdir } from "fs/promises";
import path from "path";

export const getAllCourses = unstable_cache(
  async () => {
    const coursesDir = path.join(process.cwd(), "content/courses");
    const slugs = await readdir(coursesDir);

    return Promise.all(
      slugs.map(async (slug) => {
        const raw = await readFile(
          path.join(coursesDir, slug, "course.json"),
          "utf-8",
        );
        return JSON.parse(raw);
      }),
    );
  },
  ["all-courses"], // Кэшний түлхүүр
  { revalidate: 3600 }, // 1 цагт нэг удаа шинэчлэнэ
);
typescript
// Supabase query кэшлэх
import { createClient } from "@/lib/supabase/server";

export const getTopUsers = unstable_cache(
  async () => {
    const supabase = await createClient();
    const { data } = await supabase
      .from("profiles")
      .select("username, xp")
      .order("xp", { ascending: false })
      .limit(10);
    return data ?? [];
  },
  ["top-users"],
  { revalidate: 300 }, // 5 минутад нэг удаа
);

revalidatePath — Кэш гараар цэвэрлэх

Хэрэглэгч шинэ хичээл дуусгасны дараа /profile хуудасны кэш шалтгаалсан кэшийг цэвэрлэж болно. Server Action болон API route-д ашигладаг:

typescript
// app/api/progress/route.ts
import { revalidatePath } from "next/cache";
import { createClient } from "@/lib/supabase/server";

export async function POST(request: Request) {
  const { courseSlug, lessonSlug } = await request.json();

  const supabase = await createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) {
    return Response.json({ error: "Нэвтрэх шаардлагатай" }, { status: 401 });
  }

  await supabase.from("lesson_progress").upsert({
    user_id: user.id,
    course_slug: courseSlug,
    lesson_slug: lessonSlug,
  });

  await supabase.rpc("increment_xp", { user_id: user.id, amount: 10 });

  // Профайл хуудасны кэш цэвэрлэнэ — дараагийн зочлоход шинэ өгөгдөл татна
  revalidatePath("/profile");

  return Response.json({ success: true });
}

Кэш тохиргоог сонгох нь

код
Агуулга хэзээ өөрчлөгддөг вэ?
│
├── Хэзээ ч өөрчлөгддөггүй (статик текст, зураг)
│   └── cache: 'force-cache'  эсвэл  revalidate: false
│
├── Тогтмол хугацааны дараа өөрчлөгддөг (хичээлийн жагсаалт)
│   └── next: { revalidate: 3600 }
│
├── Хэрэглэгч үйлдэл хийсний дараа өөрчлөгддөг (профайл, явц)
│   └── revalidatePath() ашиглах
│
└── Дүр бүрт өөр байх ёстой (хэрэглэгчийн тусгай өгөгдөл)
    └── cache: 'no-store'  эсвэл  dynamic: 'force-dynamic'

Дараагийн хичээлд:

ISR (Incremental Static Regeneration) ба revalidation-г нарийвчлан судална. revalidatePath, revalidateTag ашиглан кэшийг нарийн удирдах аргыг ойлгоно.