المقدمة
مع إطلاق Next.js 16، يشهد عالم تطوير الويب نقلة نوعية. الميزة الأبرز، العرض المسبق الجزئي (PPR)، تعد بتغيير الطريقة التي نفكر بها في أداء الويب وتجربة المستخدم. لقد ولت أيام الاختيار الصعب بين سرعة التوليد الساكن للمواقع (SSG) ومرونة العرض من جانب الخادم (SSR). يدمج PPR بين أفضل ما في العالمين، مما يتيح لنا تقديم واجهات مستخدم فائقة السرعة وديناميكية في نفس الوقت.
في هذا المقال التقني، سنتعمق في كيفية عمل PPR، وسنطبقها عمليًا من خلال بناء مثال واقعي: صفحة منتج تحتوي على قسم تعليقات فوري. سنستخدم قوة Supabase للتعامل مع البيانات في الوقت الفعلي ومصادقة الخادم، و Next.js 16 Server Actions لإجراء التعديلات على البيانات بسلاسة.
ما هو العرض المسبق الجزئي (PPR) ولماذا هو مهم؟
العرض المسبق الجزئي هو نهج عرض هجين. عند الطلب، يقوم Next.js بتقديم "هيكل" ثابت للصفحة مسبقًا، والذي يمكن تخزينه مؤقتًا على شبكات توصيل المحتوى (CDN) وتقديمه للمستخدمين على الفور. وفي الوقت نفسه، يتم ترك أجزاء الصفحة الديناميكية (مثل بيانات المستخدم، أو التعليقات المباشرة) كـ "ثقوب". يتم بعد ذلك جلب بيانات هذه الأجزاء الديناميكية وعرضها على الخادم في نفس الطلب، ويتم بث المحتوى المكتمل إلى العميل.
النتيجة؟ يحصل المستخدم على استجابة أولية فورية (الهيكل الثابت)، مما يحسن بشكل كبير مقاييس Core Web Vitals مثل First Contentful Paint (FCP). بعد ذلك مباشرة، يتم ملء الأجزاء الديناميكية بسلاسة.
يتم تحقيق ذلك من خلال الاستخدام الذكي لحدود React.Suspense. أي مكون يتم لفه في <Suspense> يعتبر حدًا ديناميكيًا بواسطة Next.js، مما يسمح للهيكل المحيط به بأن يكون ثابتًا.
التنفيذ: صفحة منتج مع تعليقات فورية
لنفترض أننا نبني صفحة منتج لتطبيق تجارة إلكترونية. تفاصيل المنتج ثابتة إلى حد كبير، ولكن قسم التعليقات ديناميكي للغاية ويجب تحديثه في الوقت الفعلي.
الخطوة 1: إعداد عميل Supabase
أولاً، قم بإعداد عميل Supabase للعمل مع مكونات الخادم والعرض من جانب الخادم (SSR) باستخدام حزمة @supabase/ssr.
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export const createSupabaseServerClient = () => {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SECRET_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
},
}
)
}
الخطوة 2: بناء هيكل الصفحة الساكن مع حدود Suspense
في صفحة المنتج الرئيسية، سنعرض تفاصيل المنتج بشكل ثابت ونلف مكون التعليقات الديناميكي في <Suspense>.
// app/products/[id]/page.tsx
import { Suspense } from 'react';
import ProductDetails from '@/components/product-details';
import Comments from '@/components/comments';
import CommentSkeleton from '@/components/comment-skeleton';
// هذه الدالة تجلب بيانات المنتج الثابتة
async function getProduct(id: string) {
// منطق جلب بيانات المنتج من قاعدة البيانات أو CMS
return { id, name: 'Premium Mechanical Keyboard', description: '...' };
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<div className="container mx-auto p-4">
<ProductDetails product={product} />
<h2 className="text-2xl font-bold mt-8 mb-4">Live Comments</h2>
<Suspense fallback={<CommentSkeleton />}>
{/* @ts-expect-error Async Server Component */}
<Comments productId={product.id} />
</Suspense>
</div>
);
}
سيعرض Next.js 16 الآن ProductDetails كجزء من الهيكل الثابت الفوري، بينما يعرض CommentSkeleton في البداية، ثم يبث محتوى Comments بمجرد أن يكتمل عرضه على الخادم.
الخطوة 3: المكون الديناميكي مع جلب البيانات من Supabase
مكون Comments هو مكون خادم غير متزامن يجلب الدفعة الأولية من التعليقات من Supabase. كما أنه يعرض مكون عميل للاستماع إلى التحديثات الفورية ونموذج لإضافة تعليقات جديدة.
// components/comments.tsx
import { createSupabaseServerClient } from '@/lib/supabase/server';
import RealtimeComments from './realtime-comments';
import AddCommentForm from './add-comment-form';
export default async function Comments({ productId }: { productId: string }) {
const supabase = createSupabaseServerClient();
const { data: comments } = await supabase
.from('comments')
.select('*')
.eq('product_id', productId)
.order('created_at', { ascending: false });
return (
<div>
<AddCommentForm productId={productId} />
<RealtimeComments serverComments={comments ?? []} />
</div>
);
}
الخطوة 4: التحديثات الفورية و Server Action
نحن نقسم المنطق أكثر. RealtimeComments هو مكون عميل يستمع إلى إدخالات جديدة في جدول التعليقات عبر Supabase Realtime. AddCommentForm يستخدم Server Action لنشر تعليق جديد.
// components/realtime-comments.tsx
'use client';
import { useEffect, useState } from 'react';
import { createClient } from '@supabase/supabase-js';
// ملاحظة: نستخدم هنا عميل الواجهة الأمامية للاشتراكات
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
type Comment = { id: string; content: string; created_at: string };
export default function RealtimeComments({ serverComments }: { serverComments: Comment[] }) {
const [comments, setComments] = useState(serverComments);
useEffect(() => {
const channel = supabase
.channel('realtime-comments')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'comments' },
(payload) => {
setComments((prevComments) => [payload.new as Comment, ...prevComments]);
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [supabase]);
return (
<ul>
{comments.map((comment) => (
<li key={comment.id} className="border-b p-2">{comment.content}</li>
))}
</ul>
);
}
// components/add-comment-form.tsx
import { createSupabaseServerClient } from '@/lib/supabase/server';
import { revalidatePath } from 'next/cache';
export default function AddCommentForm({ productId }: { productId: string }) {
const addComment = async (formData: FormData) => {
'use server';
const content = formData.get('comment') as string;
if (!content) return;
const supabase = createSupabaseServerClient();
// يمكنك هنا التحقق من هوية المستخدم
// const { data: { user } } = await supabase.auth.getUser();
await supabase.from('comments').insert([{ content, product_id: productId }]);
// لا حاجة لـ revalidatePath هنا لأن Realtime سيتولى التحديث!
// ومع ذلك، قد يكون مفيدًا كاحتياطي أو لتحديثات أخرى.
};
return (
<form action={addComment} className="mb-4">
<textarea name="comment" className="w-full p-2 border rounded" required />
<button type="submit" className="bg-blue-500 text-white p-2 rounded">Add Comment</button>
</form>
);
}
حالات الاستخدام في العالم الحقيقي
هذا النمط قوي للغاية ومناسب لـ:
- صفحات منتجات التجارة الإلكترونية: تفاصيل المنتج ثابتة؛ المخزون المباشر، المراجعات، والأسعار الديناميكية يتم بثها.
- منصات التواصل الاجتماعي: هيكل الصفحة وملف المستخدم ثابتان؛ آخر المشاركات والإشعارات ديناميكية.
- لوحات المعلومات: التخطيط والتنقل ثابتان؛ الرسوم البيانية والإحصائيات الحية يتم تحميلها في
Suspense. - المواقع الإخبارية: محتوى المقال ثابت؛ قسم التعليقات المباشرة والأخبار العاجلة ذات الصلة ديناميكية.
الخاتمة
يمثل العرض المسبق الجزئي في Next.js 16 قفزة هائلة إلى الأمام في تطوير الويب. من خلال دمجه مع قوة Supabase للبيانات في الوقت الفعلي و Server Actions للتعديلات السلسة، يمكن للمطورين الآن بناء تطبيقات ليست فقط سريعة بشكل لا يصدق ولكنها أيضًا تفاعلية وديناميكية للغاية. هذا المزيج يحل التنازلات القديمة بين أداء وقت التحميل والتفاعل المباشر، مما يمهد الطريق لجيل جديد من تجارب الويب.
