למה מודל הקאשינג ב-Next.js השתנה מהיסוד?
אם עבדתם עם Next.js 15 או גרסאות קודמות של App Router, אתם בטח זוכרים את הכאב הזה. הקאשינג היה מרומז — Next.js פשוט החליט בשבילכם מה נכנס לקאש ומה לא, ומה שנשאר זה בלבול אינסופי. כן, באמת. מפתחים מצאו את עצמם מנסים לנחש למה דף מסוים מציג מידע ישן, או למה שינוי שעשו בבסיס הנתונים לא משתקף באפליקציה.
Next.js 16 הפך את הקערה על פיה.
עם Cache Components, הקאשינג הוא opt-in — כלומר, שום דבר לא נכנס לקאש אלא אם ציינתם את זה במפורש. זה שינוי מהותי שמחזיר את השליטה למי שצריך אותה — אליכם, המפתחים. מבחינתי, זה הצעד הכי חשוב שהפריימוורק עשה מאז שהציג את App Router.
אז בואו נצלול פנימה. במדריך הזה נעבור על כל מה שצריך לדעת כדי לעבוד עם Cache Components ביעילות: מהדירקטיבה use cache, דרך שליטה במשך הקאשינג עם cacheLife, ועד אסטרטגיות invalidation מתקדמות עם cacheTag ו-revalidateTag.
הפעלת Cache Components בפרויקט
הצעד הראשון הוא להפעיל את הפיצ׳ר בקונפיגורציה. פתחו את next.config.ts והוסיפו:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
ברגע שהפעלתם את cacheComponents, ההתנהגות של Next.js משתנה: כל פעולות שליפת הנתונים ב-App Router לא ייכנסו לקאש כברירת מחדל, אלא אם סימנתם אותן במפורש עם use cache. בפועל זה אומר שהנתונים שלכם תמיד יהיו טריים — עד שתחליטו אחרת.
המודל המנטלי: שלושה סוגי תוכן
עם Cache Components, כל חלק ב-UI שלכם נופל לאחד משלושה סוגים. כדאי להכיר אותם כי הם מהווים את הבסיס לכל מה שנלמד אחר כך:
- סטטי — קוד סינכרוני, חישובים פשוטים. ה-navbar, הפוטר, כותרות קבועות. אלה מרונדרים ב-build time בעלות אפסית.
- מקושש (Cached) — קוד אסינכרוני אבל דטרמיניסטי. שאילתות מבסיס נתונים, קריאות API ל-CMS, בעצם כל דבר שלא חייב להשתנות בכל בקשה. סמנו אותם עם
use cacheושלטו במשך הקאשינג עםcacheLife(). - דינמי — תוכן שחייב להיות טרי בכל בקשה. נתונים אישיים של המשתמש, עגלת קניות, התראות בזמן אמת. עטפו אותם ב-
<Suspense>בליuse cache.
המודל הזה הוא הבסיס של Partial Prerendering (PPR). הרעיון פשוט: Next.js בונה מעטפת סטטית ב-build time ומזרים את התוכן הדינמי בזמן הבקשה. התוצאה? TTFB מהיר ברמה של CDN, ובו זמנית תוכן מותאם אישית לכל משתמש.
שימוש ב-use cache ברמות שונות
הדירקטיבה use cache היא גמישה להפליא — אפשר להשתמש בה ברמת הקובץ, הקומפוננטה, או הפונקציה. בואו נראה את כל האפשרויות.
ברמת הפונקציה — קאשינג של נתונים
זו הגישה שאני ממליץ עליה ברוב המקרים. הרעיון הוא לשים את use cache כמה שיותר קרוב למקום שליפת הנתונים:
import { cacheLife, cacheTag } from 'next/cache'
export async function getProducts() {
'use cache'
cacheLife('hours')
cacheTag('products')
const products = await db.product.findMany({
orderBy: { createdAt: 'desc' },
take: 50,
})
return products
}
מה שיפה פה — הנתונים עצמם נכנסים לקאש, בלי קשר לקומפוננטה שמציגה אותם. אם כמה קומפוננטות קוראות ל-getProducts(), כולן ייהנו מאותו קאש. חסכוני ונקי.
ברמת הקומפוננטה — קאשינג של UI
export async function ProductList() {
'use cache'
cacheLife('hours')
cacheTag('product-list')
const products = await db.product.findMany()
return (
<section>
<h2>המוצרים שלנו</h2>
<ul>
{products.map((p) => (
<li key={p.id}>{p.name} - ₪{p.price}</li>
))}
</ul>
</section>
)
}
במקרה הזה כל הפלט של הקומפוננטה נכנס לקאש, כולל ה-HTML המרונדר. זה שימושי במיוחד כשהקומפוננטה כוללת חישובים כבדים מעבר לשליפת הנתונים עצמה.
ברמת הקובץ — קאשינג של כל הייצואים
'use cache'
import { cacheLife } from 'next/cache'
export async function getCategories() {
cacheLife('days')
return await db.category.findMany()
}
export async function getPopularTags() {
cacheLife('hours')
return await db.tag.findMany({
orderBy: { usageCount: 'desc' },
take: 20,
})
}
כש-use cache יושב בראש הקובץ, כל הפונקציות המיוצאות נכנסות לקאש אוטומטית. דבר נחמד — כל פונקציה עדיין יכולה להגדיר cacheLife שונה בהתאם לצרכים שלה.
שליטה במשך הקאשינג עם cacheLife
הפונקציה cacheLife() היא הדרך שלכם לקבוע כמה זמן תוכן נשאר בקאש. Next.js מגיע עם כמה פרופילים מובנים שמכסים את רוב המקרים:
| פרופיל | stale (שנ׳) | revalidate (שנ׳) | expire (שנ׳) | שימוש אופייני |
|---|---|---|---|---|
seconds | 0 | 1 | 60 | נתונים תנודתיים |
minutes | 300 | 60 | 3,600 | פיד חברתי |
hours | 3,600 | 900 | 86,400 | רשימות מוצרים |
days | 86,400 | 3,600 | 604,800 | פוסטים בבלוג |
weeks | 604,800 | 86,400 | 2,592,000 | תוכן אוורגרין |
max | 2,592,000 | 604,800 | 4,294,967,294 | תוכן כמעט סטטי |
שלושת הפרמטרים עובדים ביחד, וחשוב להבין את התפקיד של כל אחד:
- stale — כמה זמן הדפדפן משתמש בתוכן מהקאש המקומי לפני שבודק מחדש מול השרת.
- revalidate — כל כמה זמן השרת מרענן את הקאש ברקע (הגישה הידועה כ-stale-while-revalidate).
- expire — הזמן המקסימלי שתוכן יכול להישאר בקאש לפני שנמחק לגמרי. אחרי הזמן הזה, אין ברירה — צריך לשלוף מחדש.
יצירת פרופילים מותאמים אישית
הפרופילים המובנים עובדים טוב, אבל לא תמיד מתאימים למציאות שלכם. אפשר להגדיר פרופילים משלכם ב-next.config.ts:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
cacheLife: {
catalog: {
stale: 600, // 10 דקות — הלקוח משתמש בקאש
revalidate: 300, // 5 דקות — השרת מרענן ברקע
expire: 7200, // שעתיים — מקסימום חיי הקאש
},
editorial: {
stale: 3600, // שעה
revalidate: 14400, // 4 שעות
expire: 604800, // שבוע
},
},
}
export default nextConfig
השימוש אחר כך פשוט לגמרי:
export async function getCatalog() {
'use cache'
cacheLife('catalog')
return await fetchCatalogData()
}
cacheLife דינמי לפי תנאי
אחד הדפוסים שאני הכי אוהב הוא שימוש ב-cacheLife דינמי — קביעת משך קאש שונה בהתאם לתוצאה שחוזרת מהשאילתה:
import { cacheLife, cacheTag } from 'next/cache'
export async function getArticle(slug: string) {
'use cache'
cacheTag(`article-${slug}`)
const article = await db.article.findUnique({
where: { slug },
})
if (!article) {
// מאמר לא נמצא — קאשינג קצר, כי אולי יתווסף בקרוב
cacheLife('minutes')
return null
}
// מאמר קיים — קאשינג ארוך
cacheLife('days')
return article
}
נקודה חשובה: רק קריאה אחת ל-cacheLife צריכה להתבצע בכל הרצה של הפונקציה. אפשר לשים קריאות בענפי תנאי שונים (כמו בדוגמה למעלה), אבל ודאו שרק אחת מהן מתבצעת בפועל.
תיוג וניקוי קאש עם cacheTag ו-revalidateTag
הפונקציה cacheTag() מאפשרת לכם לתייג רשומות קאש כדי לנקות אותן בצורה סלקטיבית אחר כך. חשבו על זה כמו תוויות על קופסאות במחסן — כשצריכים לפנות פריט ספציפי, מחפשים לפי תווית במקום לפתוח הכל.
תיוג בסיסי
import { cacheTag, cacheLife } from 'next/cache'
export async function getPost(id: string) {
'use cache'
cacheTag(`post-${id}`, 'posts')
cacheLife('days')
return await db.post.findUnique({ where: { id } })
}
שימו לב לטריק פה: הוספנו שני תגים — post-${id} הספציפי ו-posts הכללי. ככה אפשר לנקות פוסט בודד או את כל הפוסטים בבת אחת, לפי הצורך.
ניקוי קאש עם revalidateTag
שינוי חשוב ב-Next.js 16 שכדאי לשים לב אליו: הפונקציה revalidateTag דורשת עכשיו שני ארגומנטים. הארגומנט השני מציין את פרופיל ה-cacheLife:
'use server'
import { revalidateTag } from 'next/cache'
export async function updatePost(id: string, data: FormData) {
await db.post.update({
where: { id },
data: {
title: data.get('title') as string,
content: data.get('content') as string,
},
})
// ✅ ה-API החדש — חובה לציין פרופיל
revalidateTag(`post-${id}`, 'max')
revalidateTag('posts', 'max')
}
הפרמטר max מפעיל stale-while-revalidate — התוכן הישן מוגש מיד למשתמש בזמן שהשרת מרענן ברקע. לרוב המקרים, max זו הבחירה הנכונה.
updateTag לעדכון מיידי
לפעמים stale-while-revalidate פשוט לא מספיק טוב. כשמשתמש ערך פוסט, הוא מצפה לראות את השינוי שלו מיד — לא את הגרסה הישנה. לזה בדיוק נועד updateTag:
'use server'
import { updateTag } from 'next/cache'
export async function publishPost(id: string) {
await db.post.update({
where: { id },
data: { status: 'published' },
})
// updateTag — מבטל את הקאש מיד, המשתמש רואה את השינוי ישר
updateTag(`post-${id}`)
}
ההבדל הקריטי בין השניים: revalidateTag מרענן ברקע (stale-while-revalidate), ואילו updateTag מבטל מיד ומאלץ שליפה חדשה (read-your-own-writes). עוד הבדל — updateTag עובד רק בתוך Server Actions, בעוד ש-revalidateTag עובד גם ב-Route Handlers.
דוגמה מלאה: דף מוצר עם PPR
בואו נרכיב דוגמה מעשית שמשלבת את כל מה שדיברנו עליו עד עכשיו. דף מוצר שכולל תוכן סטטי, מקושש ודינמי — בדיוק כמו באפליקציה אמיתית.
הקומפוננטה הראשית — דף המוצר
import { Suspense } from 'react'
import { ProductDetails } from '@/components/ProductDetails'
import { RecommendedProducts } from '@/components/RecommendedProducts'
import { ProductReviews } from '@/components/ProductReviews'
import { CartWidget } from '@/components/CartWidget'
import { Skeleton } from '@/components/Skeleton'
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return (
<main>
{/* מקושש — פרטי המוצר לא משתנים בכל בקשה */}
<ProductDetails productId={id} />
{/* דינמי — עגלת הקניות היא אישית למשתמש */}
<Suspense fallback={<Skeleton type="cart" />}>
<CartWidget />
</Suspense>
{/* מקושש — ביקורות מתעדכנות אבל לא בכל בקשה */}
<Suspense fallback={<Skeleton type="reviews" />}>
<ProductReviews productId={id} />
</Suspense>
{/* דינמי — המלצות מותאמות אישית */}
<Suspense fallback={<Skeleton type="recommendations" />}>
<RecommendedProducts productId={id} />
</Suspense>
</main>
)
}
קומפוננטת פרטי המוצר — מקוששת
import { cacheLife, cacheTag } from 'next/cache'
export async function ProductDetails({
productId,
}: {
productId: string
}) {
'use cache'
cacheLife('hours')
cacheTag(`product-${productId}`, 'products')
const product = await db.product.findUnique({
where: { id: productId },
include: { category: true, images: true },
})
if (!product) {
cacheLife('minutes')
return <p>מוצר לא נמצא</p>
}
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>₪{product.price}</span>
<span>קטגוריה: {product.category.name}</span>
</article>
)
}
קומפוננטת הביקורות — מקוששת עם חיי קאש קצרים יותר
import { cacheLife, cacheTag } from 'next/cache'
export async function ProductReviews({
productId,
}: {
productId: string
}) {
'use cache'
cacheLife('minutes')
cacheTag(`reviews-${productId}`)
const reviews = await db.review.findMany({
where: { productId },
orderBy: { createdAt: 'desc' },
take: 10,
include: { author: true },
})
return (
<section>
<h2>ביקורות ({reviews.length})</h2>
{reviews.map((review) => (
<div key={review.id}>
<strong>{review.author.name}</strong>
<p>{review.content}</p>
<span>{'⭐'.repeat(review.rating)}</span>
</div>
))}
</section>
)
}
שימו לב לבחירות כאן: פרטי המוצר מקוששים לשעות (הם לא ממש משתנים), ביקורות מקוששות לדקות (כי הן מתעדכנות לעיתים קרובות יותר), והעגלה וההמלצות הן דינמיות לחלוטין. זו בדיוק הגרנולריות ש-Cache Components נותנים לכם.
use cache: remote — קאשינג מבוזר
הקאש הרגיל של use cache נשמר בזיכרון של המופע הנוכחי. זה עובד מצוין — עד שאתם מגיעים לסביבות serverless. שם כל מופע מתחיל עם קאש ריק, ומקבלים cache misses תכופים שפוגעים בביצועים.
הפתרון: use cache: remote.
export async function getExchangeRates(currency: string) {
'use cache: remote'
cacheTag(`rates-${currency}`)
cacheLife({ expire: 3600 }) // שעה
const response = await fetch(
`https://api.exchangerate.host/latest?base=${currency}`
)
return response.json()
}
עם use cache: remote, הקאש נשמר בשירות חיצוני (Redis, Vercel KV, או כל backend אחר שתגדירו) ומשותף בין כל המופעים. כל המשתמשים שמבקשים את אותו מטבע נהנים מאותו קאש.
מתי להשתמש בזה: כשיש לכם נתונים שמשותפים בין הרבה משתמשים — מחירונים, שערי מטבע, קטלוגים — ואתם רצים בסביבת serverless.
use cache: private — קאשינג אישי
ומה אם צריכים לקשש נתונים שתלויים במשתמש ספציפי, כמו cookies()? לזה יש את use cache: private:
import { cookies } from 'next/headers'
export async function getUserDashboard() {
'use cache: private'
cacheLife({ stale: 60 })
const sessionId = (await cookies()).get('session-id')?.value
if (!sessionId) return null
const dashboardData = await fetchDashboardData(sessionId)
return dashboardData
}
כמה דברים חשובים על use cache: private שכדאי לזכור:
- התוצאות נשמרות רק בצד הלקוח (בזיכרון הדפדפן) — לא על השרת.
- הקאש לא שורד רענון דף (refresh). כלומר, הוא שימושי בעיקר לניווט בין דפים.
- זה מאפשר גישה ל-
cookies()ו-headers()בתוך הסקופ המקושש — מה שלא עובד עםuse cacheהרגיל. - הפונקציה עדיין רצה בכל רינדור צד-שרת. הקאשינג הוא רק עבור ניווט צד-לקוח.
אינטגרציה עם Partial Prerendering (PPR)
Cache Components הם הבסיס של PPR ב-Next.js 16. כשאתם מפעילים cacheComponents: true, PPR מופעל אוטומטית. הנה איך התהליך עובד:
- Build time: Next.js מרנדר את עץ הקומפוננטות. קומפוננטות עם
use cacheמרונדרות ונכנסות למעטפת הסטטית. - Request time: המעטפת הסטטית מוגשת מייד מה-CDN — אנחנו מדברים על TTFB של מילישניות. במקביל, ה"חורים" הדינמיים (ה-Suspense boundaries) מתחילים לטעון.
- Streaming: ברגע שהנתונים הדינמיים מוכנים, הם נשלחים ללקוח ב-stream ומחליפים את ה-fallback.
התוצאה בפועל: זמני LCP שיכולים להיות מתחת ל-100ms מה-edge, גם בדפים עם תוכן מותאם אישית. בהגינות — זה מרשים.
טעויות נפוצות ואיך להימנע מהן
1. שימוש ב-cookies() או headers() בתוך use cache
זו כנראה הטעות הנפוצה ביותר, וגם אחת שקל ליפול אליה. קריאה ל-cookies() או headers() בתוך פונקציה עם use cache תגרום לשגיאה:
// ❌ לא יעבוד
export async function getUserData() {
'use cache'
const session = (await cookies()).get('session') // שגיאה!
return await fetchUser(session?.value)
}
// ✅ הפתרון — העבירו את הערך כפרמטר
export async function getUserData(sessionId: string) {
'use cache'
cacheTag(`user-${sessionId}`)
return await fetchUser(sessionId)
}
// קראו ל-cookies בקומפוננטה החיצונית
export default async function Page() {
const sessionId = (await cookies()).get('session')?.value ?? ''
const user = await getUserData(sessionId)
return <Profile user={user} />
}
2. שכחת הארגומנט השני ב-revalidateTag
ב-Next.js 16, revalidateTag דורש שני ארגומנטים. אם אתם מגיעים מגרסה קודמת, הקריאה הישנה עם ארגומנט אחד תגרום לשגיאת TypeScript:
// ❌ deprecated — שגיאת TypeScript
revalidateTag('products')
// ✅ הדרך הנכונה
revalidateTag('products', 'max')
// ✅ אפשרות נוספת — ביטול מיידי
revalidateTag('products', { expire: 0 })
3. שימוש ב-unstable_ prefix
אם אתם מגיעים מ-Next.js 15 canary, חשוב לדעת ש-cacheLife ו-cacheTag כבר יציבים לגמרי ב-Next.js 16. הסירו את הפריפיקס unstable_:
// ❌ ישן
import { unstable_cacheLife, unstable_cacheTag } from 'next/cache'
// ✅ חדש
import { cacheLife, cacheTag } from 'next/cache'
4. ציפייה שהקאש ישרוד בסביבת serverless
הקאש של use cache הרגיל הוא in-memory. בסביבות serverless, כל מופע מתחיל חיים חדשים עם קאש ריק. אם שמתם לב ל-cache misses תכופים בפרודקשן, שקלו מעבר ל-use cache: remote או שימוש ב-custom cache handler.
5. קומפוננטה אסינכרונית לא עטופה כלל
אם קומפוננטה אסינכרונית לא עטופה ב-<Suspense> וגם לא מסומנת עם use cache, תקבלו שגיאה ברורה למדי:
// שגיאה: Uncached data was accessed outside of Suspense
// ✅ הפתרון — עטפו ב-Suspense
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
// ✅ או סמנו עם use cache
async function AsyncComponent() {
'use cache'
// ...
}
שאלות נפוצות
האם use cache מחליף את ISR?
טכנית, ISR עדיין עובד ב-Next.js 16. אבל הגישה המומלצת קדימה היא use cache בשילוב עם cacheLife. היתרון המרכזי: עם use cache השליטה היא ברמת הקומפוננטה או הפונקציה, ולא ברמת הדף כולו כמו ב-ISR. בפועל זה מאפשר גרנולריות הרבה יותר גבוהה — חלק אחד של הדף יכול להיות מקושש לשעה וחלק אחר לדקה בלבד.
האם אפשר להשתמש ב-use cache בקומפוננטות לקוח?
לא. use cache עובד רק בקוד שרץ על השרת — Server Components ופונקציות שרת. אי אפשר לשלב use client עם use cache באותו קובץ. אם צריכים קאשינג בצד הלקוח, השתמשו בספריות כמו TanStack Query או SWR.
מה ההבדל בין use cache, use cache: remote ו-use cache: private?
שלוש וריאציות עם מקרי שימוש שונים לגמרי:
use cache— קאש in-memory על השרת. מהיר, בלי עלות נוספת, אבל לא משותף בין מופעים שונים.use cache: remote— קאש בשירות חיצוני (Redis, KV). משותף בין כל המופעים, אבל מגיע עם latency ועלות.use cache: private— קאש בזיכרון הדפדפן בלבד. מאפשר גישה ל-cookies(), אבל לא שורד רענון דף.
איך Cache Components משפיעים על SEO?
בצורה חיובית מאוד, ובכנות — זה אחד הדברים שהכי שמחו אותי. PPR מבטיח שהמעטפת הסטטית (כולל כל תוכן מקושש) מוגשת מייד ל-crawlers. הם מקבלים HTML מלא בלי לחכות ל-JavaScript. תוכן דינמי שנמצא בתוך <Suspense> מזורם לאחר מכן, והחדשות הטובות — ה-crawlers של גוגל תומכים ב-streaming ומעבדים גם אותו. אתרים שמשתמשים ב-PPR רואים שיפור משמעותי בציוני Core Web Vitals, במיוחד ב-LCP ו-TTFB.
האם צריך לשנות קוד קיים כשעוברים ל-Next.js 16?
כן, אבל אין צורך בפאניקה — אפשר לעשות את זה בצורה הדרגתית. ברגע שמפעילים cacheComponents: true, כל שליפות הנתונים הופכות לדינמיות כברירת מחדל. אם לפני כן הסתמכתם על קאשינג מרומז, תצטרכו להוסיף use cache במקומות הרלוונטיים.
ההמלצה שלי: התחילו מהדפים הכבדים ביותר, הוסיפו use cache עם cacheLife מתאים, ובדקו את הביצועים. אין צורך לעשות הכל בבת אחת — ותראו שיפור כבר מהצעד הראשון.