در پروژههای بزرگ Next.js، نحوه و زمان فراخوانی درخواستهای شبکه (Network Requests) تاثیر مستقیم روی سرعت لود صفحه و حس کاربر از سرعت سایت دارد. در این درس، سه الگوی مهندسی بسیار مهم را بررسی میکنیم: دریافت متوالی (Sequential)، دریافت موازی (Parallel) و اشتراکگذاری بهینه دادهها بین سرور و کلاینت.
چطور درخواستهای وابسته به هم (آبشاری) را مدیریت کنید.
چطور با حذف بلاکهای await ناخواسته، درخواستها را همزمان (موازی) استارت بزنید.
تکنیک پیشرفتهی ترکیب React.cache و React Context برای پیشگیری از تکرار درخواستها.
دریافت متوالی یا آبشاری (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 پوشش دهید تا کاربر به محض کلیک، فوراً شاکلهی اولیه صفحه را ببیند و هر دو بخش به نوبت استریم شوند.
دریافت موازی زمانی رخ میدهد که درخواستهای موجود در یک مسیر، هیچ وابستگی به یکدیگر ندارند و میتوانند به صورت همزمان و در یک لحظه استارت بخورند.
یک اشتباه رایج که برنامهنویسان مرتکب میشوند، چیدن دکمههای 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استفاده کنید تا خطای هر کدام را جداگانه مدیریت کنید.
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>
}
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) است. یعنی با رفرش شدن صفحه یا درخواست یک کاربر دیگر، این کَش کاملاً از نو ساخته میشود و خطری برای تداخل اطلاعات کاربران با یکدیگر ندارد.
دریافت متوالی (Sequential): صفحات را آبشاری لود میکند. برای بخشهای وابسته به دیتا اجتنابناپذیر است اما باید با کامپوننتهای <Suspense> خُرد شود.
دریافت موازی (Parallel): برای بهینهسازی سرعت ترانزیشنها حیاتی است. پرامیسها را همزمان ایجاد کنید و با Promise.all تحویل بگیرید.
اشتراک پیشرفته: با جفت کردن React.cache در سرور و انتقال پرامیسها به کلاینت پرووایدرها، معماری پروژه را یکپارچه، سبک و فوقالعاده سریع نگه دارید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://nextjs.org/docs/app