عملکرد برنامههای React Server Component خود را به حداکثر برسانید. این راهنمای جامع، تابع 'cache' در React را برای واکشی کارآمد داده، حذف دادههای تکراری و memoization بررسی میکند.
تسلط بر React `cache`: بررسی عمیق کش کردن داده در کامپوننتهای سرور
معرفی React Server Components (RSCs) نشاندهنده یکی از مهمترین تغییرات الگو در اکوسیستم React از زمان ظهور Hooks است. RSCها با امکان اجرای کامپوننتها منحصراً در سرور، الگوهای جدید قدرتمندی را برای ساخت برنامههای سریع، پویا و غنی از داده ارائه میدهند. با این حال، این الگوی جدید یک چالش اساسی را نیز معرفی میکند: چگونه میتوانیم دادهها را به طور کارآمد در سرور واکشی کنیم بدون اینکه گلوگاههای عملکردی ایجاد کنیم؟
یک درخت کامپوننت پیچیده را تصور کنید که در آن چندین کامپوننت مجزا به یک قطعه داده یکسان نیاز دارند، مانند پروفایل کاربر فعلی. در یک برنامه سنتی سمت کلاینت، ممکن است آن را یک بار واکشی کرده و در یک state سراسری یا یک context ذخیره کنید. در سرور، در طول یک گذر رندر واحد، واکشی سادهلوحانه این دادهها در هر کامپوننت منجر به پرس و جوهای اضافی پایگاه داده یا فراخوانی API میشود، سرعت پاسخ سرور را کاهش میدهد و هزینههای زیرساختی را افزایش میدهد. این دقیقاً همان مشکلی است که تابع داخلی React به نام `cache` برای حل آن طراحی شده است.
این راهنمای جامع شما را به سفری عمیق در تابع React `cache` میبرد. ما بررسی خواهیم کرد که این تابع چیست، چرا برای توسعه مدرن React ضروری است و چگونه آن را به طور موثر پیادهسازی کنیم. در پایان، شما نه تنها «چگونگی» بلکه «چرایی» را نیز درک خواهید کرد و به شما این امکان را میدهد که برنامههایی با کارایی بالا را با React Server Components بسازید.
درک «چرایی»: چالش واکشی داده در کامپوننتهای سرور
قبل از اینکه به سراغ راهحل برویم، درک فضای مسئله بسیار مهم است. React Server Components در یک محیط سرور در طول فرآیند رندر برای یک درخواست خاص اجرا میشوند. این رندر سمت سرور یک گذر واحد از بالا به پایین برای تولید HTML و بار RSC برای ارسال به کلاینت است.
چالش اصلی، خطر ایجاد یک "آبشار داده" است. این اتفاق زمانی رخ میدهد که واکشی داده به صورت ترتیبی و پراکنده در سراسر درخت کامپوننت انجام شود. یک کامپوننت فرزند که به داده نیاز دارد، تنها *پس از* رندر شدن والدش میتواند واکشی خود را شروع کند. بدتر از آن، اگر چندین کامپوننت در سطوح مختلف درخت به داده دقیقاً یکسانی نیاز داشته باشند، ممکن است همه آنها واکشیهای مستقل و یکسانی را فعال کنند.
مثالی از واکشی اضافی
ساختار صفحه داشبورد معمولی را در نظر بگیرید:
- `DashboardPage` (کامپوننت سرور ریشه)
- `UserProfileHeader` (نمایش نام و آواتار کاربر)
- `UserActivityFeed` (نمایش فعالیتهای اخیر کاربر)
- `UserSettingsLink` (بررسی مجوزهای کاربر برای نمایش لینک)
در این سناریو، `UserProfileHeader`، `UserActivityFeed` و `UserSettingsLink` همگی به اطلاعاتی در مورد کاربر فعلی وارد شده نیاز دارند. بدون مکانیزم کش، پیادهسازی ممکن است به این صورت باشد:
(کد مفهومی - از این ضد الگو استفاده نکنید)
// In some data fetching utility file
import db from './database';
export async function getUser(userId) {
// Each call to this function hits the database
console.log(`Querying database for user: ${userId}`);
return await db.user.findUnique({ where: { id: userId } });
}
// In UserProfileHeader.js
async function UserProfileHeader({ userId }) {
const user = await getUser(userId); // DB Query #1
return <header>Welcome, {user.name}</header>;
}
// In UserActivityFeed.js
async function UserActivityFeed({ userId }) {
const user = await getUser(userId); // DB Query #2
// ... fetch activity based on user
return <div>...activity...</div>;
}
// In UserSettingsLink.js
async function UserSettingsLink({ userId }) {
const user = await getUser(userId); // DB Query #3
if (!user.canEditSettings) return null;
return <a href="/settings">Settings</a>;
}
برای یک بارگذاری صفحه، ما سه پرس و جو پایگاه داده یکسان انجام دادهایم! این ناکارآمد، کند و غیرقابل مقیاس است. در حالی که میتوانیم این مشکل را با "بالا بردن state" و واکشی کاربر در والد `DashboardPage` و ارسال آن به عنوان props (prop drilling) حل کنیم، این کار کامپوننتهای ما را به شدت به هم متصل میکند و میتواند در درختهای تو در تو پیچیده شود. ما به راهی برای واکشی داده در جایی که مورد نیاز است نیاز داریم و در عین حال اطمینان حاصل کنیم که درخواست زیربنایی فقط یک بار انجام میشود. اینجاست که `cache` وارد میشود.
معرفی React `cache`: راه حل رسمی
تابع `cache` ابزاری است که توسط React ارائه شده است و به شما امکان میدهد نتیجه یک عملیات واکشی داده را کش کنید. هدف اصلی آن حذف دادههای تکراری درخواست در یک گذر رندر سرور است.
در اینجا ویژگیهای اصلی آن آورده شده است:
- این یک تابع مرتبه بالاتر است: شما تابع واکشی داده خود را با `cache` میپیچید. این تابع، تابع شما را به عنوان آرگومان دریافت میکند و یک نسخه جدید و memoized از آن را برمیگرداند.
- محدوده درخواست: این مهمترین مفهومی است که باید درک کنید. کش ایجاد شده توسط این تابع برای مدت زمان یک *چرخه درخواست-پاسخ سرور* دوام میآورد. این یک کش دائمی و بین درخواستی مانند Redis یا Memcached نیست. دادههای واکشی شده برای درخواست کاربر A کاملاً از درخواست کاربر B جدا شده است.
- Memoization مبتنی بر آرگومانها: هنگامی که تابع کش شده را فراخوانی میکنید، React از آرگومانهایی که ارائه میدهید به عنوان یک کلید استفاده میکند. اگر تابع کش شده دوباره با همان آرگومانها در طول همان رندر فراخوانی شود، React از اجرای تابع صرف نظر میکند و نتیجه ذخیره شده قبلی را برمیگرداند.
اساساً، `cache` یک لایه memoization مشترک و با دامنه درخواست ارائه میدهد که هر کامپوننت سرور در درخت میتواند به آن دسترسی داشته باشد و مشکل واکشی اضافی ما را به طور ظریف حل میکند.
نحوه پیادهسازی React `cache`: یک راهنمای عملی
بیایید مثال قبلی خود را برای استفاده از `cache` بازسازی کنیم. پیادهسازی به طرز شگفتآوری ساده است.
نحو و کاربرد اصلی
اولین قدم، وارد کردن `cache` از React و پیچیدن تابع واکشی داده است. بهترین روش این است که این کار را در لایه داده یا یک فایل ابزار اختصاصی انجام دهید.
import { cache } from 'react';
import db from './database'; // Assuming a database client like Prisma
// Original function
// async function getUser(userId) {
// console.log(`Querying database for user: ${userId}`);
// return await db.user.findUnique({ where: { id: userId } });
// }
// Cached version
export const getCachedUser = cache(async (userId) => {
console.log(`(Cache Miss) Querying database for user: ${userId}`);
const user = await db.user.findUnique({ where: { id: userId } });
return user;
});
همین! `getCachedUser` اکنون یک نسخه deduplicated از تابع اصلی ما است. `console.log` داخل آن راهی عالی برای تأیید این است که پایگاه داده فقط زمانی hit میشود که تابع با یک `userId` جدید در طول یک رندر فراخوانی شود.
استفاده از تابع کش شده در کامپوننتها
اکنون، میتوانیم کامپوننتهای خود را برای استفاده از این تابع کش شده جدید به روز کنیم. زیبایی اینجاست که کد کامپوننت نیازی به آگاهی از مکانیزم کش ندارد. فقط تابع را به طور معمول فراخوانی میکند.
import { getCachedUser } from './data/users';
// In UserProfileHeader.js
async function UserProfileHeader({ userId }) {
const user = await getCachedUser(userId); // Call #1
return <header>Welcome, {user.name}</header>;
}
// In UserActivityFeed.js
async function UserActivityFeed({ userId }) {
const user = await getCachedUser(userId); // Call #2 - a cache hit!
// ... fetch activity based on user
return <div>...activity...</div>;
}
// In UserSettingsLink.js
async function UserSettingsLink({ userId }) {
const user = await getCachedUser(userId); // Call #3 - a cache hit!
if (!user.canEditSettings) return null;
return <a href="/settings">Settings</a>;
}
با این تغییر، وقتی `DashboardPage` رندر میشود، اولین کامپوننتی که `getCachedUser(123)` را فراخوانی میکند، پرس و جو پایگاه داده را فعال میکند. فراخوانیهای بعدی `getCachedUser(123)` از هر کامپوننت دیگری در همان گذر رندر، فوراً نتیجه کش شده را بدون hit دوباره پایگاه داده دریافت میکنند. کنسول ما فقط یک پیام "(Cache Miss)" را نشان میدهد و مشکل واکشی اضافی ما را کاملاً حل میکند.
بررسی عمیقتر: `cache` در مقابل `useMemo` در مقابل `React.memo`
توسعهدهندگانی که از پسزمینه سمت کلاینت میآیند، ممکن است `cache` را شبیه به سایر APIهای memoization در React بدانند. با این حال، هدف و دامنه آنها اساساً متفاوت است. بیایید تفاوتها را روشن کنیم.
| API | محیط | دامنه | مورد استفاده اصلی |
|---|---|---|---|
| `cache` | فقط سرور (برای RSCها) | چرخه درخواستی-پاسخی | حذف دادههای تکراری درخواستها (به عنوان مثال، پرس و جوهای پایگاه داده، فراخوانی API) در سراسر درخت کامپوننت در طول یک رندر سرور. |
| `useMemo` | کلاینت و سرور (Hook) | هر نمونه کامپوننت | Memoizing نتیجه یک محاسبه پرهزینه در داخل یک کامپوننت برای جلوگیری از محاسبه مجدد در رندرهای بعدی آن نمونه کامپوننت خاص. |
| `React.memo` | کلاینت و سرور (HOC) | پیچیدن یک کامپوننت | جلوگیری از رندر مجدد یک کامپوننت اگر props آن تغییر نکرده باشد. یک مقایسه سطحی از props انجام میدهد. |
به طور خلاصه:
- از `cache` برای به اشتراک گذاشتن نتیجه یک واکشی داده در بین کامپوننتهای مختلف در سرور استفاده کنید.
- از `useMemo` برای جلوگیری از محاسبات پرهزینه در داخل یک کامپوننت واحد در طول رندرهای مجدد استفاده کنید.
- از `React.memo` برای جلوگیری از رندر مجدد کل یک کامپوننت به طور غیرضروری استفاده کنید.
الگوهای پیشرفته و بهترین شیوهها
همانطور که `cache` را در برنامههای خود ادغام میکنید، با سناریوهای پیچیدهتری روبرو خواهید شد. در اینجا چند بهترین روش و الگوهای پیشرفته وجود دارد که باید در نظر داشته باشید.
کجا توابع کش شده را تعریف کنیم
در حالی که از نظر فنی میتوانید یک تابع کش شده را در داخل یک کامپوننت تعریف کنید، اکیداً توصیه میشود که آنها را در یک لایه داده جداگانه یا ماژول ابزار تعریف کنید. این کار باعث جداسازی دغدغهها میشود، توابع را به راحتی قابل استفاده مجدد در سراسر برنامه شما میکند و اطمینان میدهد که از همان نمونه تابع کش شده در همه جا استفاده میشود.
روش خوب:
// src/data/products.js
import { cache } from 'react';
import db from './database';
export const getProductById = cache(async (id) => {
// ... fetch product
});
ترکیب `cache` با کش کردن در سطح فریم ورک (به عنوان مثال، Next.js `fetch`)
این یک نکته حیاتی برای هر کسی است که با یک فریم ورک full-stack مانند Next.js کار میکند. Next.js App Router، API بومی `fetch` را برای حذف خودکار دادههای تکراری درخواستها گسترش میدهد. در زیر کاپوت، Next.js از React `cache` برای پیچیدن `fetch` استفاده میکند.
این بدان معناست که اگر از `fetch` برای فراخوانی یک API استفاده میکنید، نیازی نیست که خودتان آن را در `cache` بپیچید.
// In Next.js, this is AUTOMATICALLY deduplicated per-request.
// No need to wrap in `cache()`.
async function getProduct(productId) {
const res = await fetch(`https://api.example.com/products/${productId}`);
return res.json();
}
بنابراین، چه زمانی باید از `cache` به صورت دستی در یک برنامه Next.js استفاده کنید؟
- دسترسی مستقیم به پایگاه داده: وقتی از `fetch` استفاده نمیکنید. این رایجترین مورد استفاده است. اگر از یک ORM مانند Prisma یا یک درایور پایگاه داده به طور مستقیم استفاده میکنید، React هیچ راهی برای اطلاع از درخواست ندارد، بنابراین باید آن را در `cache` بپیچید تا deduplication را دریافت کنید.
- استفاده از SDKهای شخص ثالث: اگر از یک کتابخانه یا SDK استفاده میکنید که درخواستهای شبکه خود را ایجاد میکند (به عنوان مثال، یک کلاینت CMS، یک SDK درگاه پرداخت)، باید آن فراخوانیهای تابع را در `cache` بپیچید.
مثال با Prisma ORM:
import { cache } from 'react';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// This is a perfect use case for cache()
export const getUserFromDb = cache(async (userId) => {
return prisma.user.findUnique({ where: { id: userId } });
});
رسیدگی به آرگومانهای تابع
React `cache` از آرگومانهای تابع برای ایجاد یک کلید کش استفاده میکند. این کار برای مقادیر اولیه مانند رشتهها، اعداد و بولیانها بیعیب و نقص کار میکند. با این حال، هنگامی که از اشیاء به عنوان آرگومان استفاده میکنید، کلید کش بر اساس ارجاع شی است، نه مقدار آن.
این میتواند منجر به یک دام رایج شود:
const getProducts = cache(async (filters) => {
// ... fetch products with filters
});
// In Component A
const productsA = await getProducts({ category: 'electronics', limit: 10 }); // Cache miss
// In Component B
const productsB = await getProducts({ category: 'electronics', limit: 10 }); // Also a CACHE MISS!
حتی اگر دو شی دارای محتوای یکسانی باشند، آنها نمونههای متفاوتی در حافظه هستند و در نتیجه کلیدهای کش متفاوتی ایجاد میکنند. برای حل این مشکل، باید ارجاعات شی پایدار را ارسال کنید یا به طور عملیتر، از آرگومانهای اولیه استفاده کنید.
راه حل: استفاده از مقادیر اولیه
const getProducts = cache(async (category, limit) => {
// ... fetch products with filters
});
// In Component A
const productsA = await getProducts('electronics', 10); // Cache miss
// In Component B
const productsB = await getProducts('electronics', 10); // Cache HIT!
مشکلات رایج و نحوه اجتناب از آنها
-
درک نادرست دامنه کش:
دام: فکر کردن به اینکه `cache` یک کش سراسری و دائمی است. توسعه دهندگان ممکن است انتظار داشته باشند دادههای واکشی شده در یک درخواست در درخواست بعدی در دسترس باشند، که میتواند منجر به اشکالات و مشکلات دادههای قدیمی شود.
راه حل: همیشه به یاد داشته باشید که `cache` به ازای هر درخواست است. وظیفه آن جلوگیری از کار اضافی در یک رندر واحد است، نه در بین چندین کاربر یا جلسه. برای کش کردن دائمی، به ابزارهای دیگری مانند Redis، Vercel Data Cache یا هدرهای کش HTTP نیاز دارید.
-
استفاده از آرگومانهای ناپایدار:
دام: همانطور که در بالا نشان داده شد، ارسال نمونههای جدید شی یا آرایه به عنوان آرگومان در هر فراخوانی، هدف `cache` را به طور کامل از بین میبرد.
راه حل: توابع کش شده خود را طوری طراحی کنید که تا حد امکان آرگومانهای اولیه را بپذیرند. اگر مجبور به استفاده از یک شی هستید، اطمینان حاصل کنید که یک ارجاع پایدار را ارسال میکنید یا به serialize کردن شی به یک رشته پایدار (به عنوان مثال، `JSON.stringify`) برای استفاده به عنوان یک کلید فکر کنید، اگرچه این میتواند پیامدهای عملکردی خود را داشته باشد.
-
استفاده از `cache` در کلاینت:
دام: به طور تصادفی وارد کردن و استفاده از یک تابع پیچیده شده با `cache` در داخل یک کامپوننت که با دستورالعمل `"use client"` علامتگذاری شده است.
راه حل: تابع `cache` یک API فقط سرور است. تلاش برای استفاده از آن در کلاینت منجر به یک خطای زمان اجرا میشود. منطق واکشی داده خود، به ویژه توابع پیچیده شده با `cache` را، کاملاً در داخل کامپوننتهای سرور یا در ماژولهایی که فقط توسط آنها وارد میشوند، نگه دارید. این کار جداسازی تمیز بین واکشی داده سمت سرور و تعاملات سمت کلاینت را تقویت میکند.
تصویر بزرگ: چگونه `cache` در اکوسیستم مدرن React جای میگیرد
React `cache` فقط یک ابزار مستقل نیست. این یک قطعه اساسی از پازلی است که مدل React Server Components را عملی و کارآمد میکند. این امکان را برای یک تجربه توسعهدهنده قدرتمند فراهم میکند که در آن میتوانید واکشی داده را با کامپوننتهایی که به آن نیاز دارند هممکان کنید، بدون اینکه نگران جریمههای عملکردی ناشی از درخواستهای اضافی باشید.
این الگو در هماهنگی کامل با سایر ویژگیهای React 18 کار میکند:
- Suspense: هنگامی که یک کامپوننت سرور منتظر داده از یک تابع کش شده است، React میتواند از Suspense برای پخش یک fallback loading به کلاینت استفاده کند. به لطف `cache`، اگر چندین کامپوننت منتظر دادههای یکسانی باشند، میتوانند همگی به طور همزمان پس از تکمیل یک واکشی داده واحد un-suspended شوند.
- Streaming SSR: `cache` تضمین میکند که سرور درگیر انجام کارهای تکراری نمیشود و به آن اجازه میدهد تا پوسته HTML و تکههای کامپوننت را سریعتر به کلاینت رندر و پخش کند و معیارهایی مانند Time to First Byte (TTFB) و First Contentful Paint (FCP) را بهبود بخشد.
نتیجه گیری: Cache In و سطح برنامه خود را بالا ببرید
تابع `cache` در React ابزاری ساده اما عمیقاً قدرتمند برای ساخت برنامههای وب مدرن و با کارایی بالا است. این ابزار به طور مستقیم چالش اصلی واکشی داده در یک مدل کامپوننت سرور محور را با ارائه یک راه حل ظریف و داخلی برای حذف دادههای تکراری درخواست برطرف میکند.
بیایید نکات کلیدی را مرور کنیم:
- هدف: `cache` فراخوانیهای تابع (مانند واکشی داده) را در یک رندر سرور واحد حذف میکند.
- دامنه: حافظه آن کوتاه مدت است و فقط برای یک چرخه درخواست-پاسخ دوام میآورد. این یک جایگزین برای یک کش دائمی مانند Redis نیست.
- چه زمانی از آن استفاده کنیم: هر منطق واکشی داده غیر از `fetch` (به عنوان مثال، پرس و جوهای مستقیم پایگاه داده، فراخوانی SDK) را که ممکن است چندین بار در طول یک رندر فراخوانی شود، بپیچید.
- بهترین روش: توابع کش شده را در یک لایه داده جداگانه تعریف کنید و از آرگومانهای اولیه برای اطمینان از کشهای قابل اعتماد استفاده کنید.
با تسلط بر React `cache`، شما فقط چند فراخوانی تابع را بهینه نمیکنید. شما مدل واکشی داده اعلانی و کامپوننت محور را میپذیرید که React Server Components را بسیار متحول میکند. پس پیش بروید، آن واکشیهای اضافی را در کامپوننتهای سرور خود شناسایی کنید، آنها را با `cache` بپیچید و شاهد بهبود عملکرد برنامه خود باشید.