استفاده از partial pre-rendering در Next.js

نسخه Next.js 14 ویژگی جدیدی به نام partial pre-rendering را معرفی کرده است. این ویژگی به توسعه‌دهندگان کمک می‌کند تا بتوانند این موضوع، که کدام قسمت از یک صفحه pre-render شده و یا از ابتدا رندر شود را کنترل کنند. partial pre-rendering از React Suspense API استفاده می‌کند، بنابراین اگر با React آشنا باشیم درک و استفاده از آن ساده خواهد بود.

ویژگی partial pre-rendering از ترکیبی از پردازش استاتیک، به ویژه incremental static regeneration (ISR) و server-side processing (SSR) استفاده می‌کند. استفاده از ISR در Next.js پیش رندر صفحات حاوی داده‌های داینامیک را در طول زمان ساخت، فعال می‌کند. به این ترتیب این صفحات در صورت نیاز، به تدریج در پس‌زمینه رندر می‌شوند و تجربه کاربری کارآمد و پویایی را ارائه می‌دهند.

در این مقاله قصد داریم تا نحوه عملکرد ویژگی partial pre-rendering و استفاده از آن در برنامه‌های Next.js را بررسی کنیم. البته باید این موضوع را به خاطر داشته باشیم با توجه به این که این ویژگی هنوز در مرحله آزمایشی می‌باشد، بنابراین استفاده از آن در محیط واقعی توصیه نمی‌شود.

partial pre-rendering چگونه کار می کند؟

partial pre-rendering در نسخه Next.js 14 به توسعه‌دهندگان اجازه می‌دهد تا مشخص کنند کدام بخش یا بخش‌های صفحه باید از قبل رندر شوند. بنابراین توسعه‌دهندگان می‌توانند کنترل بیشتری بر فرآیند بهینه‌سازی داشته باشند. این ویژگی تا زمانی که داده‌ها آماده و در دسترس باشند از Concurrent API و  Suspense مربوط به React برای «تعلیق» یا «مکث» عمل rendering استفاده می‌کند، که این موضوع منجر به عملکرد سریع‌تر و بهینه‌تر می‌شود.

React Suspense API به کامپوننت‌ها اجازه می‌دهد تا در هنگام انتظار برای داده‌ها، معمولاً در حین دریافت آن‌ها به صورت asynchronous، رندر را به حالت تعلیق درآورند یا متوقف کنند.

Suspense API یک رابط کاربری fallback فراهم می‌کند که هنگام لود داده‌ها ظاهر می‌شود. این رابط کاربری همراه با سایر محتویات استاتیک در صفحه لود شده و نمایش داده می‌شود. سپس تا زمانی که دریافت asynchronous داده‌ها کامل شود، رابط کاربری fallback قابل مشاهده باقی می‌ماند. در نهایت پس از آماده شدن داده‌ها، با آن‌ها جایگزین می‌گردد.

رابط کاربری fallback معمولاً loaderای است که می‌خواهیم به کاربران خود نشان دهیم تا بدانند محتوا در حال لود شدن است. به این ترتیب آن‌ها می‌توانند تا لود شدن کامل محتوا، قسمت‌های دیگر صفحه که قبلاً لود شده و یا از قبل رندر شده‌اند را مشاهده کنند.

استفاده از partial pre-rendering

برای استفاده از partial pre-rendering، ابتدا قسمت‌هایی را در برنامه خود تعیین می‌کنیم که در آن عملیات asynchronous انجام می‌شود. این قسمت ایده‌آلی است که می‌خواهیم Suspense API را در آن اعمال کنیم. برای مثال، کامپوننتی که داده‌ها را به‌صورت asynchronous دریافت می‌کند، می‌تواند داخل کامپوننت <Suspense>قرار بگیرد. این کار نشان می‌دهد که آن قسمت باید تا زمانی که داده‌ها آماده و در دسترس باشد، به تعویق بیفتد و یا اینکه به حالت تعلیق درآید.

import React, { Suspense } from "react"

const App = () => (
  <Suspense fallback={<Loader />}>
    ....your component
  </Suspense>
)

کامپوننت‌هایی که درون کامپوننت Suspenseقرار گرفته‌اند، آن‌هایی هستند که معلق خواهند بود. برای استفاده از partial pre-rendering، اصلاً نیازی به تغییر کد خود نداریم، فقط باید بخش یا صفحه مورد نظر خود را داخل کامپوننت Suspense قرار دهیم. Next.js می‌داند که کدام قسمت‌ها را به صورت استاتیک یا داینامیک رندر کند.

نحوه استفاده از partial pre-rendering

برای استفاده از ویژگی partial pre-rendering در Next.js، آخرین نسخه Canary را با استفاده از دستورات زیر نصب می‌کنیم:

/* using npm */
npm install next@canary

/* using yarn */
yarn add next@canary

سپس، در فایل next.config.jsخود، پیکربندی زیر را اضافه می‌کنیم:

experimental: {
  ppr: true,
},

فایل next.config.jsما باید به شکل زیر باشد:

/** @type {import('next').NextConfig} */
const nextConfig = {
    experimental: {
        ppr: true,
    },
}
module.exports = nextConfig

اکنون می‌توانیم از Suspense API استفاده کنیم. به عنوان مثال:

async function Posts() {
    const data = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
        cache: 'no-store',
    })
    const posts = await data.json()
    return (
        <>
            <h2>All Posts</h2>
            {posts.slice(0, 7).map((post) => (
                <div key={post.id}>
                    <h4>Title: {post.title}</h4>
                    <p>Content: {post.body}</p>
                </div>
            ))}
        </>
    )
}

export default function Home() {
    return (
        <main className="flex min-h-screen flex-col items-center justify-between p-24">
                <div>
                    <h1>Partial Pre-Rendering</h1>
                    <p>
                        Morbi eu ullamcorper urna, a condimentum massa. In
                        fermentum ante non turpis cursus fringilla. Praesent
                        neque eros, gravida vel ante sed, vehicula elementum
                        orci. Sed eu ipsum eget enim mattis mollis. Morbi eu
                        ullamcorper urna, a condimentum massa. In fermentum ante
                        non turpis cursus fringilla. Praesent neque eros,
                        gravida vel ante sed, vehicula elementum orci. Sed eu
                        ipsum eget enim mattis mollis.
                    </p>
                </div>

                <Posts />
        </main>
    )
}

در کد بالا، یک صفحه ساده برای دریافت و رندر کردن برخی از پست‌ها داریم. با استفاده از partial pre-rendering می‌توانیم محتوای پست‌ها را تا زمانی که داده‌ها در دسترس قرار بگیرند به تعویق بیاندازیم. در مدت زمانی که طول می‌کشد تا داده‌ها دریافت شوند، رابط کاربری fallback که ما مشخص می‌کنیم با سایر محتویات استاتیک رندر می‌شود:

import { Suspense } from 'react'

function LoadingPosts() {
    const shimmer = `relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent`
    return (
        <div className="col-span-4 space-y-4 lg:col-span-1 min-h-screen w-full mt-20">
            <div
                className={`relative h-[167px] rounded-xl bg-gray-900 ${shimmer}`}
            />
            <div className="h-4 w-full rounded-lg bg-gray-900" />
            <div className="h-6 w-1/3 rounded-lg bg-gray-900" />
            <div className="h-4 w-full rounded-lg bg-gray-900" />
            <div className="h-4 w-4/6 rounded-lg bg-gray-900" />
        </div>
    )
}

async function Posts() {
    const data = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
        cache: 'no-store',
    })
    const posts = await data.json()
    return (
        <>
            <h2>All Posts</h2>
            {posts.slice(0, 7).map((post) => (
                <div key={post.id}>
                    <h4>Title: {post.title}</h4>
                    <p>Content: {post.body}</p>
                </div>
            ))}
        </>
    )
}

export default function Home() {
    return (
        <main className="flex min-h-screen flex-col items-center justify-between p-24">
                <div>
                    <h1>Partial Pre-Rendering</h1>
                    <p>
                        Morbi eu ullamcorper urna, a condimentum massa. In
                        fermentum ante non turpis cursus fringilla. Praesent
                        neque eros, gravida vel ante sed, vehicula elementum
                        orci. Sed eu ipsum eget enim mattis mollis. Morbi eu
                        ullamcorper urna, a condimentum massa. In fermentum ante
                        non turpis cursus fringilla. Praesent neque eros,
                        gravida vel ante sed, vehicula elementum orci. Sed eu
                        ipsum eget enim mattis mollis.
                    </p>
                </div>

            <Suspense fallback={<LoadingPosts />}>
                <Posts />
            </Suspense>
        </main>
    )
}

ما Suspense API را از React وارد کرده و کامپوننت Postsرا درون آن قرار داده‌ایم. همینطور یک رابط کاربری fallback را نیز از کامپوننت LoadingPostsاضافه کرده‌ایم.

کامپوننت LoadingPostsطرح لود پست‌ها را نشان می‌دهد. این کامپوننت یک جلوه درخشان دارد و معمولاً به عنوان انیمیشن لود استفاده می‌شود و به گونه‌ای طراحی شده است که به کاربران بازخورد بصری مبنی بر لود محتوا ارائه می‌دهد. اگر صفحه خود را مجدداً لود کنیم، طرح loading را برای یک دقیقه قبل از رندر شدن محتوای پست‌ها مشاهده می‌کنیم.

موارد استفاده از partial pre-rendering

همانطور که قبلاً در این مورد بحث کردیم، صفحاتی که لود داده‌های داینامیک را دارند بهترین حالت استفاده برای partial pre-rendering هستند زیرا داده‌ها به صورت asynchronous دریافت می‌شوند. در ادامه یک مثال دیگر داریم که استفاده از ویژگی partial pre-rendering در آن می‌تواند بسیار مفید باشد.

ویژگی Partial pre-rendering در صفحه وبلاگ

در هر سایت وبلاگ، ما لیستی از وبلاگ‌های موجود را از سرور خود دریافت می‌کنیم. با استفاده از PPR، می‌توانیم زمانی که پست‌های وبلاگ را از سرور دریافت می‌کنیم، یک loader نمایش دهیم و پس از آماده شدن داده‌ها، آن‌ها را جایگزین کنیم.

با قرار دادن fetching داده asynchronous داخل Suspense، رندر کامپوننت را تا زمانی که داده‌ها در دسترس قرار بگیرند به حالت تعلیق در می‌آوریم. این رویکرد کارایی لود اولیه صفحه را با از پیش رندر کردن محتوای استاتیک، از جمله رابط کاربری fallback، بهینه می‌کند و تنها در صورت نیاز، داده‌های داینامیک را دریافت و رندر می‌نماید. به عنوان مثال:

/* pages.js */

import Home from './components/Home';
const Page = () => (
    <main className="flex min-h-screen flex-col justify-between p-12">
        <header className="mb-12 text-center">
            <h1 className="mb-6 font-bold text-3xl">MezieIV Blog</h1>
        </header>
        <Home />
        <footer className="mt-24 text-center">
            <p>©MezieIV 2023</p>
        </footer>
    </main>
);
export default Page;

ما در این مقاله از Approuter استفاده می‌کنیم. در کد بالا، یک header، body و footer در layout صفحه خود داریم. همینطور کامپوننت Homeکه حاوی پست‌های وبلاگ ما می‌باشد را هم import می‌کنیم.

در کامپوننت Home، کد زیر را می‌نویسیم:

/* /components/Home.js */

import { Suspense } from 'react';

function LoadingPosts() {
    const shimmer = `relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent`;
    return (
        <div className="col-span-4 space-y-4 lg:col-span-1 min-h-screen w-full mt-20">
            <div
                className={`relative h-[167px] rounded-xl bg-gray-900 ${shimmer}`}
            />
            <div className="h-4 w-full rounded-lg bg-gray-900" />
            <div className="h-6 w-1/3 rounded-lg bg-gray-900" />
            <div className="h-4 w-full rounded-lg bg-gray-900" />
            <div className="h-4 w-4/6 rounded-lg bg-gray-900" />
        </div>
    );
}

async function Posts() {
    const data = await fetch(`https://jsonplaceholder.typicode.com/posts`, {
        cache: 'no-store',
    });
    const posts = await data.json();
    return (
        <>
            <h2 className="mb-3 mt-8 font-bold text-2xl">All Posts</h2>
            {posts.slice(0, 7).map((post) => (
                <div
                    key={post.id}
                    className="mb-5"
                >
                    <h4 className="text-lg">Title: {post.title}</h4>
                    <p className="text-sm">Content: {post.body}</p>
                </div>
            ))}
        </>
    );
}

export default function Home() {
    return (
        <>
            <div>
                <h2 className="mb-3 font-bold text-2xl">
                    Partial Pre-Rendering
                </h2>
                <p>
                    Morbi eu ullamcorper urna, a condimentum massa. In fermentum
                    ante non turpis cursus fringilla. Praesent neque eros,
                    gravida vel ante sed, vehicula elementum orci. Sed eu ipsum
                    eget enim mattis mollis. Morbi eu ullamcorper urna, a
                    condimentum massa. In fermentum ante non turpis cursus
                    fringilla. Praesent neque eros, gravida vel ante sed,
                    vehicula elementum orci. Sed eu ipsum eget enim mattis
                    mollis.
                </p>
            </div>
            <Suspense fallback={<LoadingPosts />}>
                <Posts />
            </Suspense>
        </>
    );
}

header و پاراگراف‌های صفحه و همچنین رابط کاربری fallback از قبل رندر شده‌اند. هنگامی که صفحه خود را لود کنیم، می‌توانیم تا زمانی که دریافت اطلاعات کامل شده و پست‌های وبلاگ آماده نمایش شوند، loader را ببینیم.

در مثال بالا، ما فقط یک بخش داریم که از Suspense API مربوط به PPR استفاده می‌کند.

افزودن PPR به پست‌های وبلاگ

پس از این که کاربر بر روی یکی از پست‌های وبلاگ کلیک می‌کند، به صفحه آن وبلاگ هدایت می‌شود تا به خواندن پست وبلاگ ادامه دهد. ما می‌خواهیم که کاربر در صفحه وبلاگ، پست وبلاگ و همچنین وبلاگ‌های مشابه یا پست‌های اخیر وبلاگ را مشاهده کند. این اتفاق یک تجربه کاربری خوب در یک سناریوی واقعی است. در نتیجه کاربر برای این که پست دیگری را مشاهده کند نیازی ندارد که به صفحه قبل برگردد.

از آنجایی که هم وبلاگ اصلی و هم پست‌های دیگر به صورت داینامیک دریافت می‌شوند، می‌توانیم از Suspense API برای هر دو بخش استفاده کنیم.

می‌توانیم کد زیر را در داخل کامپوننت Home، کپی و جایگزین نماییم:

/* /components/Home.js */

import { Suspense } from 'react';

function LoadingPosts() {
    const shimmer = `relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent`;
    return (
        <div className="col-span-4 space-y-4 lg:col-span-1 w-full mt-20">
            <div
                className={`relative h-[167px] rounded-xl bg-gray-900 ${shimmer}`}
            />
            <div className="h-4 w-full rounded-lg bg-gray-900" />
            <div className="h-6 w-1/3 rounded-lg bg-gray-900" />
            <div className="h-4 w-full rounded-lg bg-gray-900" />
            <div className="h-4 w-4/6 rounded-lg bg-gray-900" />
        </div>
    );
}

async function fetchPosts() {
    return new Promise((resolve) => {
        setTimeout(async () => {
            const data = await fetch(
                `https://jsonplaceholder.typicode.com/posts`,
                {
                    cache: 'no-store',
                }
            );
            const posts = await data.json();
            resolve(posts);
        }, ۲۰۰۰);
    });
}

async function BlogPost() {
    const posts = await fetchPosts();
    const post = posts[0];
    return (
        <div className="w-full">
            <h4 className="text-lg mb-2">Title - {post.title}</h4>
            <p className="text-sm leading-6">
                {post.body} {post.body} {post.body} {post.body} {post.body}{' '}
                {post.body} {post.body} {post.body} {post.body} {post.body}
            </p>
        </div>
    );
}

async function Aside() {
    const posts = await fetchPosts();
    return (
        <aside className="w-full">
            <div>
                {posts.slice(0, 5).map((post) => (
                    <ol
                        key={post.id}
                        style={{ listStyle: 'inside' }}
                    >
                        <li className="text-lg w-full">
                            <a href="#">{post.title}</a>
                        </li>
                    </ol>
                ))}
            </div>
        </aside>
    );
}

export default function Home() {
    return (
        <div className="flex justify-between pl-12 pr-12">
            <div className="w-[70%]">
                <h2 className="text-2xl mb-6">Main Blog</h2>
                <Suspense fallback={<LoadingPosts />}>
                    <BlogPost />
                </Suspense>
            </div>

            <div className="w-[25%] pl-10">
                <h2 className="text-2xl mb-12">Latest Blog Posts</h2>
                <Suspense fallback={<LoadingPosts />}>
                    <Aside />
                </Suspense>
            </div>
        </div>
    );
}

همانطور که در کد بالا می‌بینیم، کامپوننت‌های BlogPostو Asideرا درون دو Suspense API مجزا قرار دادیم. ما از یک طرح loader یکسان برای هر دو کامپوننت استفاده می‌کنیم اما این امکان وجود دارد که بسته به طراحی رابط کاربری که داریم طرح‌های منحصربه‌فردی را ایجاد نماییم. این کار با رابط کاربری که داریم بسیار سازگارتر بوده و از نظر زیبایی شناسی دلپذیرتر به نظر می‌رسد.

یک نمونه دیگر استفاده از PPR، داشبورد ادمین است. داشبوردها دارای بخش‌های مختلفی هستند که شامل نمودارهایی مانند نمودارهای میله‌ای و دایره‌ای، فهرست اطلاعات و غیره می‌باشد. در چنین مواقعی می‌توانیم در بخش‌هایی که داده‌ها به صورت asynchronous دریافت می‌شوند، از قابلیت partial pre-rendering استفاده کنیم. به طور مشابه، بخش‌هایی که به صورت asynchronous دریافت نمی‌شوند در زمان ساخت به صورت استاتیک از قبل رندر خواهند شد.

مزایای partial pre-rendering

با توجه به مفاهیمی که در این مقاله به آن‌ها پرداختیم، می‌توانیم نتیجه بگیریم که partial pre-rendering مزایای زیر را دارد:

  • لود اولیه سریع‌تر صفحه: از آنجایی که محتویات استاتیک هنگام لود صفحه رندر می‌شوند، کاربران مجبور نیستند منتظر لود تمام محتوا، از جمله محتوای داینامیک باشند. این کار منجر به لود سریع‌تر صفحه می‌شود. در نتیجه کاربران می‌توانند در حالی که محتوای داینامیک در پس‌زمینه در حال دریافت شدن می‌باشد، به تعامل با بخش‌های استاتیک صفحه ادامه دهند.
  • بهبود تجربه کاربری: هنگامی که محتویات داینامیک آماده می‌شوند به صورت یکپارچه لود می‌گردند. در این بین، یک loader به کاربران نشان داده می‌شود که بیانگر این است که داده‌ها در حال دریافت هستند. این کار تجربه تعاملی سایت را برای کاربران بهبود می‌بخشد.
  • کاهش بار سرور: PPR بار روی سرور را کاهش می‌دهد. این اتفاق به این دلیل است که سرور فقط بخش‌های داینامیک صفحه را رندر می‌کند، زیرا PPR محتویات استاتیک را در زمان ساخت و متعاقباً از یک حافظه cache، از قبل رندر می‌کند.
  • استفاده از منابع: PPR ترکیبی از قابلیت‌های رندر استاتیک (ISR) و رندر داینامیک (SSR) می‌باشد. بنابراین، می‌توانیم انتخاب کنیم که کدام بخش از صفحه ما استاتیک و از قبل رندر شده باشد و کدام بخش به صورت داینامیک به کار خود ادامه دهد.

جمع‌بندی

در این مقاله به بررسی partial pre-rendering و نحوه عملکرد آن در برنامه Next.js پرداختیم.

استفاده از partial pre-rendering در سناریوهایی که بخش‌هایی از وب‌سایت ما به صورت داینامیک رندر می‌شوند یا داده‌ها به‌صورت asynchronous دریافت می‌شوند، سودمند است. با استفاده از PPR، ما انتخاب می‌کنیم که کدام بخش از صفحه‌ای که داریم باید از قبل رندر شود و کدام قسمت باید در صورت تقاضا لود گردد. این کار باعث می‌شود تا صفحه اولیه سریع‌تر لود شود. به این ترتیب، کاربران بلافاصله محتوای استایتک یا از پیش رندر شده را مشاهده می‌کنند و محتوای داینامیک هنگامی که نیاز باشد یا پس از این که آماده شود، به نمایش در می‌آید.

اگرچه این مفهوم هنوز در مرحله اولیه و آزمایشی خود است و استفاده از آن در پروژه‌های واقعی توصیه نمی‌شود. اما می‌توانیم آن را امتحان کنیم تا نگاهی به ظاهر یک نسخه پایدار داشته باشیم.

دیدگاه‌ها:

افزودن دیدگاه جدید