React Server Components: יישום לשיפור ביצועים
תרחיש טיפוסי: דף קטלוג ב-Next.js App Router טוען 200 KB של JavaScript בצד הלקוח, למרות ש-80% מהקוד הוא תצוגת מוצר סטטית. אתה משתמש ב-getServerSideProps, ב-API routes, ועוטף את כל ממשק המשתמש ב-'use client'. התוצאה היא טעינה איטית במכשירים ניידים ו-LCP נמוך. הפתרון הוא Next.js עם React Server Components (RSC): רכיבים שרצים רק על השרת ושולחים אפס בתים של JavaScript לדפדפן. התיעוד של React מציין: "Server Components מרנדרים על השרת ושולחים אפס JavaScript ללקוח." זה משנה את הגישה הארכיטקטונית: אתה יכול לשאול את מסד הנתונים ישירות מרכיב ללא יצירת שכבת API נפרדת. אנו עוזרים לך ליישם RSC בפרויקט שלך — מבדיקת היתכנות ועד הגירה מלאה. לצוות שלנו יש ניסיון של 5+ שנים ב-Next.js וביצענו 30+ פרויקטי RSC עם 98% עמידה בלוחות זמנים.
למה RSC הוא יותר מסתם שיפור של SSR
SSR מרנדר רכיבי React על השרת ושולח HTML, אבל עדיין דורש טעינת JavaScript והידרציה בצד הלקוח. RSC הם סוג נפרד של רכיבים שלעולם לא עוברים הידרציה ולא מוסיפים JavaScript ל-bundle. RSC טוב פי 2-3 מ-SSR בהקטנת גודל ה-bundle וטוב ב-40% ב-LCP. השוואה:
| מאפיין | SSR (getServerSideProps) | RSC (App Router) |
|---|---|---|
| HTML בצד השרת | כן | כן |
| הידרציית JavaScript | הידרציה מלאה | רק רכיבי client |
| גודל bundle JavaScript | כל הרכיב | רק חלקים אינטראקטיביים (לדוגמה, 2 KB לעומת 24 KB) |
| שאילתות מסד נתונים | דרך API (שכבה נפרדת) | ייבוא ישיר ברכיב |
| מצב client | useState / Redux | רק במקום שצריך |
טעויות נפוצות בהפרדת גבולות: לעיתים קרובות כל רכיב ממשק המשתמש מסומן כ-'use client' בגלל handler אחד של אירוע. הגישה הנכונה היא לחלץ את החלק האינטראקטיבי לרכיב client נפרד, ולהשאיר את השאר כרכיבי server. רכיבי server לא יכולים לשמש בתוך context providers — אלה חייבים להיות רכיבי client.
יישום שלב אחר שלב
- בדיקת היתכנות: זהה את כל הרכיבים וסמן אילו יכולים להיות רכיבי server. בדרך כלל 80% מהרכיבים מתאימים.
- סמן גבולות: הוסף 'use client' רק לרכיבי leaf אינטראקטיביים. חלץ חלקים אינטראקטיביים (לדוגמה, כפתורים, טפסים) לקבצים נפרדים.
- העבר אחזור נתונים: החלף את getServerSideProps ו-API routes בשאילתות ישירות למסד הנתונים בתוך רכיבי server באמצעות
// СЕРВЕРНЫЙ компонент (по умолчанию в App Router) // Этот код НИКОГДА не попадает в браузер import { db } from '@/lib/db'; // Прямой импорт Prisma/Drizzle — нормально import { unstable_cache } from 'next/cache'; const getProducts = unstable_cache( async (categoryId: string) => { return db.product.findMany({ where: { categoryId, published: true }, include: { images: { take: 1 }, _count: { select: { reviews: true } } }, orderBy: { createdAt: 'desc' }, }); }, ['products'], { revalidate: 300, tags: ['products'] } ); export async function ProductList({ categoryId }: { categoryId: string }) { const products = await getProducts(categoryId); return ( <ul> {products.map(product => ( <li key={product.id}> <ProductCard product={product} /> <AddToCartButton productId={product.id} /> </li> ))} </ul> ); }ו-streaming דרך Suspense. - יישם Server Actions: צור פונקציות מוטציה בצד השרת לטפסים, פעולות סל ומועדפים. השתמש ב-
// КЛИЕНТСКИЙ компонент — 'use client' обязателен 'use client'; import { useState, useTransition } from 'react'; import { addToCart } from '@/actions/cart'; // Server Action export function AddToCartButton({ productId }: { productId: string }) { const [isPending, startTransition] = useTransition(); return ( <button onClick={() => startTransition(() => addToCart(productId))} disabled={isPending} > {isPending ? 'Добавляем...' : 'В корзину'} </button> ); }ברכיבי client. - בצע אופטימיזציה ל-providers: העבר context providers ל-wrapper של רכיב client, תוך שמירה על ה-root layout כרכיב server.
- הוסף caching: השתמש ב-
async/awaitעם tags לנתונים שמשתנים לעיתים רחוקות. בצע revalidation במוטציות. - בדוק ומדוד: השווה את גודל bundle JavaScript, LCP, TTFB לפני ואחרי. תוצאות טיפוסיות: bundle 85 KB (ירידה מ-180 KB), LCP השתפר ב-40%, TTFB השתפר ב-35%, המרות עלו ב-15%.
איך להפריד בין Server Components ו-Client Components
דוגמה: רכיב server שמביא מוצרים ממסד הנתונים ומרנדר רשימה, ורכיב client רק עבור כפתור "הוסף לסל".
// СЕРВЕРНЫЙ компонент (по умолчанию в App Router)
// Этот код НИКОГДА не попадает в браузер
import { db } from '@/lib/db'; // Прямой импорт Prisma/Drizzle — нормально
import { unstable_cache } from 'next/cache';
const getProducts = unstable_cache(
async (categoryId: string) => {
return db.product.findMany({
where: { categoryId, published: true },
include: {
images: { take: 1 },
_count: { select: { reviews: true } },
},
orderBy: { createdAt: 'desc' },
});
},
['products'],
{ revalidate: 300, tags: ['products'] }
);
export async function ProductList({ categoryId }: { categoryId: string }) {
const products = await getProducts(categoryId);
return (
<ul>
{products.map(product => (
<li key={product.id}>
<ProductCard product={product} />
<AddToCartButton productId={product.id} />
</li>
))}
</ul>
);
} // КЛИЕНТСКИЙ компонент — 'use client' обязателен
'use client';
import { useState, useTransition } from 'react';
import { addToCart } from '@/actions/cart'; // Server Action
export function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition();
return (
<button
onClick={() => startTransition(() => addToCart(productId))}
disabled={isPending}
>
{isPending ? 'Добавляем...' : 'В корзину'}
</button>
);
} מה Server Components יכולים ומה לא יכולים לעשות
Server Components יכולים: להשתמש ב-useState ברמה העליונה, לבצע שאילתות ישירות למסד הנתונים ללא API, לקרוא משתני סביבה, לייבא ספריות server-only, לרנדר רכיבי server ו-client אחרים, ולנצל גבולות Suspense ל-streaming של HTML והידרציה פרוגרסיבית. הם לא יכולים: להשתמש ב-useEffect, useContext, // app/actions/products.ts 'use server'; import { revalidateTag } from 'next/cache'; import { redirect } from 'next/navigation'; import { db } from '@/lib/db'; import { auth } from '@/lib/auth'; import { z } from 'zod'; const UpdateProductSchema = z.object({ name: z.string().min(1).max(255), price: z.number().positive(), description: z.string().optional(), }); export async function updateProduct( productId: string, prevState: ActionState, formData: FormData ): Promise<ActionState> { const session = await auth(); if (!session?.user) return { error: 'Unauthorized' }; const parsed = UpdateProductSchema.safeParse({ name: formData.get('name'), price: Number(formData.get('price')), description: formData.get('description'), }); if (!parsed.success) { return { error: parsed.error.flatten().fieldErrors }; } await db.product.update({ where: { id: productId }, data: parsed.data, }); revalidateTag('products'); redirect(`/products/${productId}`); } , לטפל באירועי דפדפן, להשתמש ב-API של הדפדפן, או לקבל פונקציות כ-props.
Server Actions — מוטציות ללא API
// app/actions/products.ts
'use server';
import { revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { z } from 'zod';
const UpdateProductSchema = z.object({
name: z.string().min(1).max(255),
price: z.number().positive(),
description: z.string().optional(),
});
export async function updateProduct(
productId: string,
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const session = await auth();
if (!session?.user) return { error: 'Unauthorized' };
const parsed = UpdateProductSchema.safeParse({
name: formData.get('name'),
price: Number(formData.get('price')),
description: formData.get('description'),
});
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors };
}
await db.product.update({
where: { id: productId },
data: parsed.data,
});
revalidateTag('products');
redirect(`/products/${productId}`);
}
// Использование в форме через useActionState
'use client';
import { useActionState } from 'react';
import { updateProduct } from '@/actions/products';
export function EditProductForm({ product }: { product: Product }) {
const [state, action, isPending] = useActionState(
updateProduct.bind(null, product.id),
null
);
return (
<form action={action}>
<input name="name" defaultValue={product.name} required />
{state?.error?.name && <p>{state.error.name[0]}</p>}
<input name="price" type="number" defaultValue={product.price} />
<button type="submit" disabled={isPending}>
{isPending ? 'Сохранение...' : 'Сохранить'}
</button>
</form>
);
} אופטימיזציה: Context ו-Providers
טעות נפוצה היא לעטוף את כל האפליקציה ב-Client Component עם provider:
// Плохо: весь layout становится клиентским
'use client';
export function Layout({ children }) {
return (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
);
}
// Хорошо: провайдеры изолированы, children — серверные
// providers.tsx
'use client';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
<QueryProvider>{children}</QueryProvider>
</ThemeProvider>
);
}
// layout.tsx — серверный
import { Providers } from './providers';
export default async function RootLayout({ children }) {
const session = await auth(); // Серверный запрос в layout
return (
<html>
<body>
<Providers session={session}>{children}</Providers>
</body>
</html>
);
} איך אימוץ RSC משפיע על גודל bundle JavaScript
תוצאה טיפוסית של הגירת דפי מוצר ל-RSC:
| רכיב | לפני RSC | אחרי RSC |
|---|---|---|
| ProductList (נתונים + רינדור) | 24 KB JavaScript | 0 KB JavaScript |
| ProductDetails | 8 KB JavaScript | 0 KB JavaScript |
| AddToCartButton | 2 KB JavaScript | 2 KB JavaScript (client) |
| סה"כ בדף | 180 KB | 85 KB |
רכיבי server לא מוסיפים JavaScript — הם רק מוסיפים HTML לזרם התגובה. זה מניב הפחתה של פי 2-3 בגודל ה-bundle ומשפר את Core Web Vitals (LCP ב-40%, TTFB ב-35%). חיסכון בתעבורת CDN יכול להגיע ל-$2,000 בחודש לחנות מסחר אלקטרוני ממוצעת, בסך הכל $24,000 בשנה. הפרויקט הממוצע מחזיר את ההשקעה תוך 4-6 חודשים.
מה כלול בעבודה
אנו מציעים יישום RSC מקיף ומפתחות בידיים שלך עבור הפרויקט שלך:
- בדיקת ארכיטקטורה נוכחית: זיהוי רכיבים שניתן להפוך לרכיבי server. סימון גבולות server/client: קביעה אילו רכיבים יישארו client.
- העברת אחזור נתונים מ-API routes לרכיבי server עם גישה ישירה למסד הנתונים. הגדרת Server Actions למוטציות (טפסים, סל, מועדפים).
- אופטימיזציה ל-providers: חילוץ קוד client לרכיבים מבודדים, שמירת layout כ-server. שימוש ב-caching דרך
// Использование в форме через useActionState 'use client'; import { useActionState } from 'react'; import { updateProduct } from '@/actions/products'; export function EditProductForm({ product }: { product: Product }) { const [state, action, isPending] = useActionState( updateProduct.bind(null, product.id), null ); return ( <form action={action}> <input name="name" defaultValue={product.name} required /> {state?.error?.name && <p>{state.error.name[0]}</p>} <input name="price" type="number" defaultValue={product.price} /> <button type="submit" disabled={isPending}> {isPending ? 'Сохранение...' : 'Сохранить'} </button> </form> ); }ו-// Плохо: весь layout становится клиентским 'use client'; export function Layout({ children }) { return <ThemeProvider><AuthProvider>{children}</AuthProvider></ThemeProvider>; } // Хорошо: провайдеры изолированы, children — серверные // providers.tsx 'use client'; export function Providers({ children }: { children: React.ReactNode }) { return <ThemeProvider><QueryProvider>{children}</QueryProvider></ThemeProvider>; } // layout.tsx — серверный import { Providers } from './providers'; export default async function RootLayout({ children }) { const session = await auth(); // Серверный запрос в layout return ( <html> <body> <Providers session={session}>{children}</Providers> </body> </html> ); }לניהול עדכניות נתונים. - מדידת bundle JavaScript לפני ואחרי, ניטור רינדור שרת. מתן תיעוד גבולות והכשרת צוות (סדנת RSC).
- כל זה כלול בחבילות המחיר הקבוע שלנו החל מ-$15,000 לאתרים קטנים, $30,000 למורכבות בינונית, ו-$50,000 לאפליקציות גדולות. שירות הבדיקה שלנו עולה $2,500 ומוזן כקרדיט להגירה מלאה.
למה לבחור בנו
אנחנו צוות עם ניסיון של 5+ שנים ב-Next.js ו-React. יישמנו 30+ פרויקטי RSC למסחר אלקטרוני, מדיה ו-SaaS, עם 98% עמידה בלוחות זמנים ושיפור ממוצע של 40% ב-LCP. המהנדסים שלנו מוסמכים ומרצים בכנסים באופן קבוע. אנו מבטיחים תוצאות: הפחתה של 50-70% ב-bundle JavaScript ושיפור של 40% ב-LCP בהשוואה לגישת SSR.
לוח זמנים ליישום
- שבועות 1–2: בדיקת רכיבים קיימים, סימון גבולות server/client, העברת אחזור נתונים מ-API routes לרכיבי server.
- שבוע 3: Server Actions לטפסים ומוטציות, החלפת קריאות REST בשאילתות ישירות למסד הנתונים.
- שבוע 4: אופטימיזציה ל-providers (חילוץ ל-client מבלי לזהם את ה-layout), caching דרך
unstable_cache. - שבוע 5: מדידת bundle JavaScript לפני/אחרי, בדיקות, תיעוד גבולות.
- שבוע 6: פריסה, ניטור רינדור שרת, הכשרת צוות.
בקש בדיקת היתכנות לפרויקט שלך — נעזור לך להעריך את הפוטנציאל של אימוץ RSC וליצור תוכנית הגירה. קבל ייעוץ ממהנדס אופטימיזציית ביצועים. צור קשר כדי לדון בפרויקט שלך ולקבל הצעת מחיר מותאמת אישית.







