Next.js: مكونات التخزين المؤقت و use cache
آخر تحديث: 2026-08-26
use cacheهو أكثر واجهات برمجة التطبيقات تحولاً في Next.js 16 — إنه يرتقي بالتخزين المؤقت من "أثر جانبي لجلب البيانات" إلى "مواطن من الدرجة الأولى على مستوى المكونات".
1. ما ستتعلمه
- أمر
use cache()ومفهوم Content Cache cacheTag()لوسم المحتوى المخزن مؤقتاًcacheLife()لتعيين مدة انتهاء صلاحية التخزين المؤقتrevalidateTag()لإبطال التخزين المؤقت عند الطلب- الفرق بين التخزين المؤقت الضمني لـ fetch والتخزين المؤقت الصريح لـ
use cache
2. قصة حقيقية لمهندس نظم
(1) نقطة الألم: أربع استدعاءات API متطابقة في نفس الصفحة
أثناء مراجعة لوحة تحكم TaskFlow، لاحظ تشارلي أن المكونات الأربعة في الصفحة — <UserAvatar> و <UserGreeting> و <UserStats> و <UserNotifications> — كل منها كان يستدعي fetch('/api/user'). بينما كان التخزين المؤقت لنفس الرابط يعمل بشكل جيد، تم استدعاء دالة استعلام قاعدة البيانات getUserFromDB() أربع مرات. ينطبق التخزين المؤقت لـ fetch فقط على طلبات HTTP؛ وهو غير فعال تماماً ضد استدعاءات الدوال الداخلية للخادم.
| السؤال | البيانات |
|---|---|
| استدعاءات قاعدة البيانات على مستوى الصفحة | 4 استعلامات متطابقة |
| الوقت لكل استعلام | 200 مللي ثانية |
| إجمالي الوقت الإضافي | 600 مللي ثانية مهدرة |
| هدر QPS لقاعدة البيانات | 4 أضعاف |
(2) الحل باستخدام use cache
قم بتغليف الدالة بـ
use cache()— هذا يخزن أي استدعاءات داخلية للدوال مؤقتاً، بما في ذلك استعلامات قاعدة البيانات والعمليات الحسابية وقراءة الملفات.
// app/dashboard/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getUserData() {
'use cache'
cacheTag('user-data')
cacheLife({ stale: 300, revalidate: 600 })
const user = await db.user.findUnique({ where: { id: 1 } }) // تنفذ مرة واحدة فقط
return user
}
export default async function DashboardPage() {
const user = await getUserData() // أول استدعاء ينفذ استعلام قاعدة البيانات
// الاستدعاءات اللاحقة ستعيد النتيجة المخزنة مؤقتاً مباشرة
return (
<div>
<UserAvatar user={user} />
<UserGreeting user={user} />
<UserStats user={user} />
<UserNotifications user={user} />
</div>
)
}
(3) العائد
| البُعد | النموذج التقليدي | use cache |
|---|---|---|
| عدد استعلامات قاعدة البيانات | 4 | 1 |
| التأخير الإضافي | 600 مللي ثانية | 0 مللي ثانية |
| دقة التخزين المؤقت | مستوى الرابط | مستوى الدالة/المكون |
| تخزين مؤقت للمنطق المخصص | ❌ غير مدعوم (fetch لـ HTTP فقط) | ✅ يدعم أي كود |
3. توجيه use cache() و Content Cache
use cache هو توجيه على مستوى الدالة — أضف 'use cache' في أعلى الدالة للإشارة إلى أن نتيجة الدالة يجب تخزينها مؤقتاً. يُسمى المحتوى المخزن مؤقتاً Content Cache، وهو طبقة تخزين مؤقت جديدة كلياً قُدمت في Next.js 16 ومستقلة عن Data Cache التقليدي (التخزين المؤقت لـ fetch).
graph TB
subgraph "Next.js 16 Cache System"
A[Request Memoization<br/>ذاكرة على مستوى الطلب]
B[Data Cache<br/>تخزين fetch المؤقت]
C[Content Cache<br/>use cache]
D[Full Route Cache<br/>تخزين المسار الكامل المؤقت]
end
A --> E[داخل نفس الطلب<br/>نفس fetch إزالة التكرار]
B --> F[استمرار عبر الطلبات<br/>force-cache/no-store]
C --> G[تخزين نتائج الدوال العشوائية مؤقتاً<br/>تحكم tag + life]
D --> H[تخزين HTML على مستوى الصفحة مؤقتاً]
style C fill:#d4edda
| مستوى التخزين المؤقت | النطاق | المُحفز | دورة الحياة |
|---|---|---|---|
| Request Memoization | طلب واحد | تلقائي (نفس الرابط) | نهاية الطلب |
| Data Cache | عبر الطلبات | fetch(options) |
يعتمد على الإعداد |
| Content Cache | عبر الطلبات | توجيه 'use cache' |
cacheLife + cacheTag |
| Full Route Cache | عبر الطلبات | وقت البناء / وقت التشغيل | Revalidate / عند الطلب |
(1) الصيغة الأساسية
// app/cache-demo/actions.ts
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
export async function getExpensiveData(id: string) {
'use cache'
cacheTag('expensive', `id-${id}`) // الوسم
cacheLife({ stale: 60, revalidate: 300 }) // إرجاع خلال 60 ثانية stale، إعادة المحاولة بعد 300 ثانية
// جميع العمليات داخل هذه الدالة مخزنة مؤقتاً
const result = await db.query(...)
return result
}
(2) إعدادات cacheLife
| المعامل | النوع | الوصف | مثال |
|---|---|---|---|
stale |
number (بالثواني) | إرجاع النتيجة مباشرة أثناء صلاحية التخزين المؤقت؛ لا يشغل تحديثاً في الخلفية | { stale: 60 } |
revalidate |
number (بالثواني) | إعادة تنفيذ الدالة بعد انقضاء هذه المدة | { revalidate: 3600 } |
expire |
number (بالثواني) | انتهاء صلاحية التخزين المؤقت بشكل مطلق؛ إجبار على إعادة الجلب | { expire: 86400 } |
▶ مثال: الاستخدام الأساسي لـ use cache (مستوى الصعوبة ⭐)
المخرجات:
يعرض واجهة مستخدم مكون getExpensiveData.
// app/cache-basic/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getServerTime() {
'use cache'
cacheTag('server-time')
cacheLife({ stale: 10, revalidate: 30 })
// محاكاة عملية تستغرق وقتاً
await new Promise(resolve => setTimeout(resolve, 1000))
return { time: new Date().toISOString(), server: process.env.HOSTNAME ?? 'local' }
}
export default async function CacheBasicPage() {
const [t1, t2, t3] = await Promise.all([
getServerTime(),
getServerTime(),
getServerTime(),
])
return (
<div>
<h1>use cache — أساسي</h1>
<p>الاستدعاء 1: {t1.time}</p>
<p>الاستدعاء 2: {t2.time}</p>
<p>الاستدعاء 3: {t3.time}</p>
<p><em>جميع الاستدعاءات الثلاثة أعادت نفس النتيجة المخزنة مؤقتاً (بدون حساب مكرر)</em></p>
</div>
)
}
المخرجات:
الاستدعاء 1: 2026-07-06T10:00:00.000Z
الاستدعاء 2: 2026-07-06T10:00:00.000Z ← نفس الطابع الزمني (إصابة في التخزين المؤقت)
الاستدعاء 3: 2026-07-06T10:00:00.000Z ← نفس الطابع الزمني (إصابة في التخزين المؤقت)
المخرجات:
الاستدعاء 1: 2026-07-06T10:00:00.000Z
الاستدعاء 2: 2026-07-06T10:00:00.000Z ← تداخل بالميلي ثانية في التخزين المؤقت
الاستدعاء 3: 2026-07-06T10:00:00.000Z
4. الاستخدام العملي لـ cacheTag و cacheLife
cacheTag() لوسم المحتوى المخزن مؤقتاً، و cacheLife() للتحكم في سياسة انتهاء الصلاحية. بالدمج مع revalidateTag()، يمكن تحقيق تحكم دقيق في التخزين المؤقت.
(1) استراتيجية الوسم والتصنيف
graph TB
A[بيانات التطبيق] --> B[بيانات المستخدم]
A --> C[بيانات المنتج]
A --> D[بيانات الطلب]
B --> E[tag: user-profile]
B --> F[tag: user-settings]
C --> G[tag: products]
C --> H[tag: product-{id}]
D --> I[tag: orders]
D --> J[tag: order-{id}]
style A fill:#cce5ff
| السياسة | أمثلة الوسوم | عمليات الإبطال | النطاق |
|---|---|---|---|
| دقيق | product-42 |
revalidateTag('product-42') |
منتج واحد فقط |
| متوسط | products |
revalidateTag('products') |
جميع المنتجات |
| خشن | catalog |
revalidateTag('catalog') |
الدليل بالكامل |
(2) الوسوم الديناميكية
// app/products/[id]/page.tsx
import { unstable_cacheTag as cacheTag } from 'next/cache'
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)
return <ProductView product={product} />
}
async function getProduct(id: string) {
'use cache'
cacheTag('products', `product-${id}`) // وسوم ديناميكية بناءً على المعاملات
return fetch(`https://api.example.com/products/${id}`).then(r => r.json())
}
▶ مثال: استراتيجية التخزين المؤقت المتدرج (مستوى الصعوبة: ⭐⭐)
المخرجات:
يجلب البيانات ويعرض النتيجة.
// app/cache-strategy/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
async function getUserProfile(userId: number) {
'use cache'
cacheTag('users', `user-${userId}`)
cacheLife({ stale: 120, revalidate: 600 }) // دقيقتان stale، 10 دقائق revalidate
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`).then(r => r.json())
}
async function getUserPosts(userId: number) {
'use cache'
cacheTag('posts', `user-posts-${userId}`)
cacheLife({ stale: 60, revalidate: 300 })
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`).then(r => r.json())
}
export default async function CacheStrategyPage() {
const [profile, posts] = await Promise.all([
getUserProfile(1),
getUserPosts(1),
])
return (
<div>
<h1>{profile.name}</h1>
<p>البريد الإلكتروني: {profile.email}</p>
<h2>المقالات ({posts.length})</h2>
<ul>{posts.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
</div>
)
}
المخرجات:
يجلب البيانات من جانب الخادم ويعرض قائمة بالعناصر.
▶ مثال: التصفية بالوسم في Server Action لا تعمل (مستوى الصعوبة: ⭐⭐)
المخرجات:
يعرض المكون واجهة المستخدم الموصوفة في المتصفح.
// app/cache-invalidation/page.tsx
import { unstable_cacheTag as cacheTag, revalidateTag } from 'next/cache'
async function getTaskList() {
'use cache'
cacheTag('tasks')
return fetch('https://jsonplaceholder.typicode.com/todos?_limit=5').then(r => r.json())
}
export default async function CacheInvalidationPage() {
const tasks = await getTaskList()
return (
<div>
<h1>قائمة المهام</h1>
<ul>{tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
<form action={async () => {
'use server'
revalidateTag('tasks') // انقر الزر لتحديث التخزين المؤقت لقائمة المهام فوراً
}}>
<button type="submit">تحديث المهام</button>
</form>
</div>
)
}
المخرجات:
نموذج يحتوي على حقول إدخال وزر إرسال.
عند الإرسال، يقوم Server Action بمعالجة البيانات وإبطال وسوم التخزين المؤقت ذات الصلة.
النص المرئي: قائمة المهام
5. التخزين المؤقت الضمني لـ fetch مقابل التخزين المؤقت الصريح لـ use cache
| البُعد | التخزين المؤقت الضمني لـ fetch (Data Cache) | التخزين المؤقت الصريح لـ use cache (Content Cache) |
|---|---|---|
| طريقة التشغيل | fetch(url) تلقائي |
توجيه 'use cache' داخل جسم الدالة |
| المحتوى المخزن مؤقتاً | استجابة HTTP | نتيجة أي دالة |
| حالات الاستخدام | استدعاءات API، طلبات HTTP | استعلامات قاعدة البيانات، العمليات الحسابية المعقدة، قراءة الملفات |
| نظام الوسم | next: { tags: [...] } |
دالة cacheTag() |
| التحكم بالوقت | next: { revalidate: N } |
دالة cacheLife() |
| وضع الإبطال | revalidateTag() / revalidatePath() |
revalidateTag() (نفس الوسم) |
(1) متى تستخدم التخزين المؤقت لـ fetch، ومتى تستخدم use cache؟
| السيناريو | طريقة التخزين المؤقت الموصى بها | السبب |
|---|---|---|
| استدعاء APIs خارجية | Data Cache (fetch) |
دعم أصلي لدلالات HTTP |
| استدعاء قاعدة البيانات | Content Cache (use cache) |
استدعاء قاعدة البيانات ليس fetch لـ HTTP |
| عمليات حسابية معقدة (50 مللي ثانية+) | Content Cache | يمكن تخزين أي دالة مؤقتاً |
| قراءة الملفات/الإعدادات | Content Cache | لا يمكن تنفيذ عمليات الملفات عبر fetch |
| معالجة مختلطة HTTP + DB | Content Cache | تخزين كتل كاملة من المنطق مؤقتاً |
▶ مثال: دمج نوعي التخزين المؤقت (مستوى الصعوبة: ⭐⭐⭐)
المخرجات:
تعرض الصفحة كما هو موصوف أعلاه، مع تحديث واجهة المستخدم بناءً على السلوك الموصوف.
// app/cache-combo/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
// 1. التخزين المؤقت لـ fetch: استدعاء API خارجي
async function getExternalPosts() {
return fetch('https://jsonplaceholder.typicode.com/posts', {
next: { tags: ['external-posts'], revalidate: 300 }
}).then(r => r.json())
}
// 2. use cache: معالجة البيانات
async function getProcessedPosts() {
'use cache'
cacheTag('processed-posts')
cacheLife({ stale: 60, revalidate: 600 })
const raw = await getExternalPosts() // يعتمد على التخزين المؤقت لـ fetch
return (raw as any[]).map((p: any) => ({
id: p.id,
title: p.title.toUpperCase(),
summary: p.body.slice(0, 100),
}))
}
export default async function CacheComboPage() {
const posts = await getProcessedPosts()
return (
<div>
<h1>المقالات المعالجة ({posts.length})</h1>
<ul>{posts.map((p: any) => (
<li key={p.id}><strong>{p.title}</strong><p>{p.summary}</p></li>
))}</ul>
</div>
)
}
المخرجات:
يجلب البيانات من جانب الخادم ويعرض قائمة بالعناصر.
6. مثال كامل: بنية التخزين المؤقت لنظام إدارة المستخدمين
// app/cache-system/page.tsx — بنية تخزين مؤقت كاملة
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
// ======== طبقة البيانات (تخزين مؤقت صريح use cache) ========
type User = { id: number; name: string; email: string }
type Post = { id: number; title: string; body: string }
async function getUsers(): Promise<User[]> {
'use cache'
cacheTag('users')
cacheLife({ stale: 120, revalidate: 600 })
return fetch('https://jsonplaceholder.typicode.com/users').then(r => r.json())
}
async function getUserPosts(userId: number): Promise<Post[]> {
'use cache'
cacheTag('posts', `user-posts-${userId}`)
cacheLife({ stale: 60, revalidate: 300 })
return fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`).then(r => r.json())
}
async function getStats(users: User[]) {
'use cache'
cacheTag('stats')
cacheLife({ stale: 300, revalidate: 1800 })
return {
totalUsers: users.length,
avgNameLength: users.reduce((s, u) => s + u.name.length, 0) / users.length,
}
}
// ======== مكونات الصفحة ========
export default async function CacheSystemPage() {
const users = await getUsers()
const stats = await getStats(users)
return (
<div style={{ maxWidth: 900, margin: '0 auto', padding: 24 }}>
<h1>إدارة المستخدمين</h1>
<StatsCard stats={stats} />
<div style={{ display: 'grid', gap: 16, marginTop: 24 }}>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
</div>
)
}
// ======== المكون الفرعي (تخزين مؤقت مستقل) ========
async function UserCard({ user }: { user: User }) {
const posts = await getUserPosts(user.id)
return (
<div style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
<h2>{user.name}</h2>
<p style={{ color: '#666' }}>{user.email}</p>
<details>
<summary>المقالات ({posts.length})</summary>
<ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
</details>
</div>
)
}
function StatsCard({ stats }: { stats: { totalUsers: number; avgNameLength: number } }) {
return (
<div style={{ background: '#f0f4ff', borderRadius: 8, padding: 16, display: 'flex', gap: 32 }}>
<div><strong>إجمالي المستخدمين</strong><p style={{ fontSize: 24 }}>{stats.totalUsers}</p></div>
<div><strong>متوسط طول الاسم</strong><p style={{ fontSize: 24 }}>{stats.avgNameLength.toFixed(1)}</p></div>
</div>
)
}
// ======== المهام الإدارية ========
// app/cache-system/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshUserData() {
revalidateTag('users') // تحديث قائمة المستخدمين
}
export async function refreshPosts(userId: number) {
revalidateTag(`user-posts-${userId}`) // تحديث مقالات مستخدم محدد
}
export async function refreshAll() {
revalidateTag('users')
revalidateTag('posts')
revalidateTag('stats')
}
// app/cache-system/admin-button.tsx
'use client'
export function AdminControls() {
return (
<div style={{ display: 'flex', gap: 8, margin: '16px 0' }}>
<form action={async () => {
const { refreshUserData } = await import('./actions')
await refreshUserData()
}}>
<button type="submit">تحديث المستخدمين</button>
</form>
<form action={async () => {
const { refreshAll } = await import('./actions')
await refreshAll()
}}>
<button type="submit">تحديث الكل</button>
</form>
</div>
)
}
▶ مثال: التحقق من إصابة التخزين المؤقت (مستوى الصعوبة: ⭐⭐)
المخرجات:
نموذج يحتوي على حقول إدخال وزر إرسال.
عند الإرسال، يقوم Server Action بمعالجة البيانات وإبطال وسوم التخزين المؤقت ذات الصلة.
النص المرئي: إدارة المستخدمين
// app/cache-verify/page.tsx — التحقق مما إذا كانت هناك إصابة في التخزين المؤقت
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'
let callCount = 0
async function getUniqueId() {
'use cache'
cacheTag('unique-id')
cacheLife({ revalidate: 30 })
callCount++
return { id: crypto.randomUUID(), calls: callCount }
}
export default async function CacheVerifyPage() {
const [a, b, c] = await Promise.all([
getUniqueId(),
getUniqueId(),
getUniqueId(),
])
return (
<div>
<h1>التحقق من التخزين المؤقت</h1>
<p>النتيجة أ: {a.id} (الاستدعاء #{a.calls})</p>
<p>النتيجة ب: {b.id} (الاستدعاء #{b.calls})</p>
<p>النتيجة ج: {c.id} (الاستدعاء #{c.calls})</p>
<p><strong>نفس المعرف + calls = 1 → إصابة في التخزين المؤقت</strong></p>
</div>
)
}
المخرجات:
المحتوى: التحقق من التخزين المؤقت | نفس المعرف + calls = 1 → إصابة في التخزين المؤقت
المخرجات:
النتيجة أ: 550e8400-e29b-41d4-a716-446655440000 (الاستدعاء #1)
النتيجة ب: 550e8400-e29b-41d4-a716-446655440000 (الاستدعاء #1) ← إصابة في التخزين المؤقت
النتيجة ج: 550e8400-e29b-41d4-a716-446655440000 (الاستدعاء #1) ← إصابة في التخزين المؤقت
❓ أسئلة شائعة
use cache و useMemo؟useMemo هو hook من جانب العميل يخزن العمليات الحسابية مؤقتاً داخل المتصفح فقط ويكون نطاقه عملية render واحدة. أما use cache فهو توجيه من جانب الخادم يستمر التخزين المؤقت عبر الطلبات، ويدعم الوسوم وأوقات انتهاء الصلاحية، ويسمح لـ Server Actions بإبطاله عند الطلب. لكل منهما حالات استخدام مختلفة تماماً.cacheLife؟stale، تُعاد النتيجة المخزنة مؤقتاً مباشرة (بدون تشغيل تحديث في الخلفية). إذا تجاوز الوقت stale ولكنه أقل من revalidate، تستمر إعادة النتيجة المخزنة مؤقتاً، ولكن يتم تشغيل إعادة حساب في الخلفية. إذا تجاوز الوقت revalidate، ينتظر النظام نتيجة جديدة. الإعدادات الموصى بها: اضبط stale على التأخير المقبول للمستخدم، و revalidate على أقصى حداثة للبيانات.fetch و use cache؟revalidateTag('products') سيمسح كلًا من الإدخال الموسوم بـ next: { tags: ['products'] } من التخزين المؤقت لبيانات fetch والإدخال الموسوم بـ cacheTag('products') من Content Cache. هذه هي الآلية الأساسية لتحقيق إبطال موحد للتخزين المؤقت.use cache في مكون عميل (client component)؟'use cache' صالح فقط في مكونات الخادم (server components) أو دوال الخادم. في مكونات العميل، يجب استخدام useMemo أو حلول التخزين المؤقت من جانب العميل مثل React Query.unstable_ في unstable_cacheLife و unstable_cacheTag؟unstable_ بعد 1-2 إصدارات رئيسية (متوقع في Next.js 17 أو 18 stable).📖 ملخص
- توجيه
'use cache'يجعل نتائج أي دالة من جانب الخادم قابلة للتخزين المؤقت، وليس مقتصراً على جلب HTTP cacheTag()لوسم المحتوى المخزن مؤقتاً؛ يدعم الوسوم الديناميكية (بناءً على المعاملات)cacheLife()للتحكم في سياسة انتهاء صلاحية التخزين المؤقت (وضع مزدوج stale + revalidate)revalidateTag()يمسح جميع الوسوم المقابلة من التخزين المؤقت لـ fetch و Content Cache دفعة واحدة- التخزين المؤقت الضمني لـ
fetch(Data Cache) مناسب لطلبات HTTP، بينما التخزين المؤقت الصريح لـuse cache(Content Cache) مناسب لأي دالة - دقة التخزين المؤقت من الخشن إلى الدقيق: مستوى الصفحة → مستوى البيانات → مستوى الدالة؛ استخدم
use cacheلتنفيذ التخزين المؤقت على مستوى الدالة - أفضل ممارسة: استخدم
use cacheلاستعلامات قاعدة البيانات والعمليات الحسابية المعقدة ودوال معالجة البيانات؛ واستخدم التخزين المؤقت لـfetchلواجهات HTTP API
📝 تمارين
-
مسألة أساسية (⭐): أنشئ
app/cache-demo/page.tsxواستخدم دالة'use cache'لتغليف عملية محاكاة تستغرق وقتاً (await new Promise(resolve => setTimeout(resolve, 2000))). استدعها ثلاث مرات في الصفحة وتحقق من أن الاستدعاء الثاني يبدأ بتأخير صفري (إصابة في التخزين المؤقت). -
تمرين متقدم (⭐⭐): أنشئ صفحة تتضمن قائمة مستخدمين وقائمة مقالات المستخدمين. استخدم
cacheLife({ revalidate: 300 })لقائمة المستخدمين وcacheLife({ revalidate: 60 })لقائمة المقالات. أضف زري Server Action في أسفل الصفحة لتحديث وسم المستخدم ووسم المقالة على التوالي. تحقق من أن الوسوم تعمل بشكل مستقل. -
تحدٍّ (⭐⭐⭐): نفذ "لوحة تحليلات البيانات" التي تسترجع البيانات من ثلاثة مصادر مختلفة (HTTP API + قاعدة بيانات محاكاة + دوال حسابية) وتستخدم
use cacheللتخزين المؤقت المركزي. لكل مصدر بيانات استراتيجيةcacheLifeمختلفة. أضف زر "تحديث الكل بالإجبار" يستدعيrevalidateTagلتحديث جميع لوحات المعلومات في وقت واحد. أنشئ أداة لتتبع معدلات إصابة التخزين المؤقت.