در پروژههای بزرگ Next.js، نحوه و زمان فراخوانی درخواستهای شبکه (Network Requests) تاثیر مستقیم روی سرعت لود صفحه و حس کاربر از سرعت سایت دارد. در این درس، سه الگوی مهندسی بسیار مهم را بررسی میکنیم: دریافت متوالی (Sequential)، دریافت موازی (Parallel) و اشتراکگذاری بهینه دادهها بین سرور و کلاینت.
چیزهایی که در این درس یاد میگیرید:
-
چطور درخواستهای وابسته به هم (آبشاری) را مدیریت کنید.
-
چطور با حذف بلاکهای
awaitناخواسته، درخواستها را همزمان (موازی) استارت بزنید. -
تکنیک پیشرفتهی ترکیب
React.cacheو React Context برای پیشگیری از تکرار درخواستها.
۱. دریافت متوالی دادهها (Sequential Data Fetching)
دریافت متوالی یا آبشاری (Waterfall) زمانی رخ میدهد که اجرای یک درخواست، کاملاً به خروجی درخواست قبلی وابسته باشد. برای مثال، تا زمانی که مشخصات خواننده (Artist) را از دیتابیس نگیریم، نمیتوانیم لیست آلبومها یا پلیلیستهای او را لود کنیم؛ چون به artistID نیاز داریم:
// app/artist/[username]/page.tsx
import { Suspense } from 'react'
export default async function Page({ params }: { params: Promise<{ username: string }> }) {
const { username } = await params
// درخواست اول: دریافت اطلاعات خواننده (رندر صفحه تا پایان این خط معطل میماند)
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
{/* استفاده از Suspense برای اینکه بخش پلیلیست مزاحم لود اولیه بخشهای بالا نشود */}
<Suspense fallback={<div>در حال بارگذاری پلیلیستها...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
async function Playlists({ artistID }: { artistID: string }) {
// درخواست دوم: فقط پس از رندر شدن والد و رسیدن artistID استارت میخورد
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
چطور این ساختار را بهینه کنیم؟
در کدهای بالا، تگ <Suspense> اجازه میدهد که پلیلیستها بعداً استریم (Stream) شوند، اما خودِ صفحه همچنان معطلِ اتمامِ درخواستِ اول (getArtist) میماند.
-
راهکار اول: مطمئن شوید دیتابیس یا API اصلی در درخواست اول فوقالعاده سریع پاسخ میدهد یا خروجی آن به خوبی کَش شده است.
-
راهکار دوم: کل صفحه را با یک فایل
loading.tsxپوشش دهید تا کاربر به محض کلیک، فوراً شاکلهی اولیه صفحه را ببیند و هر دو بخش به نوبت استریم شوند.
۲. دریافت موازی دادهها (Parallel Data Fetching)
دریافت موازی زمانی رخ میدهد که درخواستهای موجود در یک مسیر، هیچ وابستگی به یکدیگر ندارند و میتوانند به صورت همزمان و در یک لحظه استارت بخورند.
یک اشتباه رایج که برنامهنویسان مرتکب میشوند، چیدن دکمههای await پشت سر هم است که باعث میشود درخواست دوم بیدلیل منتظر پایان درخواست اول بماند (بلاک شدن ناخواسته):
// ❌ الگوی اشتباه و کند (متوالیِ بیدلیل)
const artist = await getArtist(username) // خط اول که تمام شد...
const albums = await getAlbums(username) // ...تازه خط دوم استارت میخورد
🛠 راه حل سنک کردن درخواستها با Promise.all
برای موازیسازی، ابتدا توابع را بدون نوشتن کلمه await صدا میزنیم تا پرامیسِ آنها فوراً در پسزمینه سرور فعال شود. سپس با متد Promise.all منتظر حل شدن همزمان آنها میمانیم:
// app/artist/[username]/page.tsx
import Albums from './albums'
// ... توابع fetch در اینجا تعریف شدهاند ...
export default async function Page({ params }: { params: Promise<{ username: string }> }) {
const { username } = await params
// ۱. شلیک همزمان درخواستها (بدون await)
const artistPromise = getArtist(username)
const albumsPromise = getAlbums(username)
// ۲. انتظار برای پایان همزمان هر دو پرامیس
const [artist, albums] = await Promise.all([artistPromise, albumsPromise])
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums} />
</>
)
}
💡 نکته حرفهای: در متد
Promise.allاگر حتی یکی از درخواستها با خطا مواجه شود، کل عملیات شکست میخورد. اگر لود شدن صفحه شما حیاتی است و میتوانید با لود نشدن آلبومها کنار بیایید، به جای آن از متد بومیPromise.allSettledاستفاده کنید تا خطای هر کدام را جداگانه مدیریت کنید.
۳. اشتراک دادهها با ترکیب Context و React.cache
یکی از چالشهای پیشرفته، معماری برنامههایی است که در آن هم کامپوننتهای سروری (Server Components) و هم کامپوننتهای کلاینتی (Client Components) به یک دیتای مشترک (مثل مشخصات کاربرِ لاگین شده) نیاز دارند.
با ترکیب دو ابزار React.cache (برای کَش کردن درخواست در سرور) و React Context (برای اشتراک پرامیس در کلاینت)، میتوانید کاری کنید که در طول یک ریکوئست، دیتای کاربر فقط و فقط یکبار از API دانلود شود.
گام اول: ساخت تابع کششده روی سرور
تابع را درون متد cache از پکیج react میپیچیم. این ابزار تضمین میکند اگر این تابع ۱۰ بار هم در طول یک رندر صدا زده شود، فقط یکبار درخواست واقعی شبکه میزند:
// app/lib/user.ts
import { cache } from 'react'
export const getUser = cache(async () => {
const res = await fetch('https://api.example.com/user')
return res.json()
})
گام دوم: ساخت پرووایدر کلاینتی برای انتقال پرامیس
ما یک Context کلاینتی میسازیم که به جای دیتای خالص، خودِ پرامیسِ بازنشده را به عنوان Value ذخیره میکند:
// app/user-provider.tsx
'use client'
import { createContext } from 'react'
export const UserContext = createContext<Promise<{ id: string; name: string }> | null>(null)
export default function UserProvider({ children, userPromise }) {
return <UserContext.Provider value={userPromise}>{children}</UserContext.Provider>
}
گام سوم: استارت پرامیس در Layout بدون await
در بالاترین سطح ساختار (Layout سروری)، تابع کششده را صدا میزنیم و بدون معطل کردن رندر با await، پرامیس را به پرووایدر کلاینتی تحویل میدهیم:
// app/layout.tsx (Server Component)
import UserProvider from './user-provider'
import { getUser } from './lib/user'
export default function RootLayout({ children }: { children: React.ReactNode }) {
const userPromise = getUser() // 🔴 بدون await؛ فقط پرامیس ساخته میشود
return (
<html>
<body>
<UserProvider userPromise={userPromise}>{children}</UserProvider>
</body>
</html>
)
}
گام چهارم: مصرف دیتا در کلاینت و سرور بدون دانلود مجدد!
در سمت کلاینت: کامپوننت فرزند به کمک هوک use پرامیس را از Context بیرون کشیده و حل میکند (درون یک مرز Suspense والد):
// app/ui/profile.tsx (Client Component)
'use client'
import { use, useContext } from 'react'
import { UserContext } from '../user-provider'
export function Profile() {
const userPromise = useContext(UserContext)
const user = use(userPromise!) // زنده کردن پرامیس با هوک use
return <p>خوش آمدید، {user.name}</p>
}
در سمت سرور (یک صفحه دیگر): کامپوننت سروری میتواند مستقیماً همان تابع getUser را await کند. به لطف React.cache، هیچ درخواست شبکهی دومی ارسال نمیشود و دیتای ذخیره شده فورا تحویل داده میشود:
// app/dashboard/page.tsx (Server Component)
import { getUser } from '../lib/user'
export default async function DashboardPage() {
const user = await getUser() // ⚡ کاملاً کَش شده؛ هیچ درخواست تکراری ارسال نمیشود!
return <h1>داشبورد مدیریت {user.name}</h1>
}
⚠️ یادآوری مهم: طول عمر کَشِ متد
React.cacheفقط و فقط به اندازه طول عمر همان دیتای درخواستی کاربر (Single Request Lifecycle) است. یعنی با رفرش شدن صفحه یا درخواست یک کاربر دیگر، این کَش کاملاً از نو ساخته میشود و خطری برای تداخل اطلاعات کاربران با یکدیگر ندارد.
خلاصه راهبردها (Recap)
-
دریافت متوالی (Sequential): صفحات را آبشاری لود میکند. برای بخشهای وابسته به دیتا اجتنابناپذیر است اما باید با کامپوننتهای
<Suspense>خُرد شود. -
دریافت موازی (Parallel): برای بهینهسازی سرعت ترانزیشنها حیاتی است. پرامیسها را همزمان ایجاد کنید و با
Promise.allتحویل بگیرید. -
اشتراک پیشرفته: با جفت کردن
React.cacheدر سرور و انتقال پرامیسها به کلاینت پرووایدرها، معماری پروژه را یکپارچه، سبک و فوقالعاده سریع نگه دارید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: منبع مستندات