رفتن به محتوا

نکتهٔ حرفه‌ای Nuxt: پِیلودِ SSR را با pick، transform و serialize: false کوچک کنید

۵ دقیقه مطالعه
nuxt
pro-tip
data-fetching
performance

در ۷۲ ساعت گذشته چیزی آن‌قدر مهم منتشر نشد که پایهٔ یک پست شود. Nuxt 4.6.3 پیش‌تر در پستی جداگانه پوشش داده شده، Vue 3.6 هنوز در مرحلهٔ release candidate است و تازه‌ترین نسخهٔ پایدار Next.js یعنی 16.4.3 یک backport رفع باگ از پنج روز پیش است. نکتهٔ حرفه‌ایِ قبلی دربارهٔ Next.js بود، پس امروز نوبت Nuxt است: اینکه واکشی داده چه چیزی را داخل HTML می‌گذارد.

همهٔ مطالب زیر با مرجع useAsyncData و راهنمای واکشی داده تطبیق داده شده است.

اشتباه رایج

یک رکورد کامل را از API می‌گیرید و فقط سه فیلدش را رندر می‌کنید. کار می‌کند، پس کسی HTML را نگاه نمی‌کند. اما به‌صورت پیش‌فرض داده‌ٔ resolve‌شده در پیلودِ Nuxt (__NUXT_DATA__) ذخیره می‌شود؛ همان چیزی که کلاینت را از واکشی دوباره در هنگام hydration بی‌نیاز می‌کند. راهنمای داده صریح است: پاسخ‌های بزرگ صفحه را بزرگ می‌کنند. هرچه handler برگرداند، چه رندر شود چه نه، به مرورگر می‌رسد.

vue
<script setup lang="ts">
// The endpoint returns the whole record: body, revisions, author profile, flags...
const { data: article } = await useFetch(`/api/articles/${useRoute().params.slug}`);
</script>

<template>
    <h1>{{ article?.title }}</h1>
    <p>{{ article?.summary }}</p>
</template>

دو فیلد رندر می‌شود. بقیه در سند serialize می‌شود، مرورگر آن را parse می‌کند و تا پایان عمر صفحه در حافظه می‌ماند.

الگوی بهتر ۱: pick

pick فقط کلیدهای فهرست‌شده را از نتیجه نگه می‌دارد. مثال راهنما آن را روی useFetch و با فهرستی از فیلدهایی که قالب واقعاً استفاده می‌کند نشان می‌دهد.

vue
<script setup lang="ts">
const { data: article } = await useFetch(`/api/articles/${useRoute().params.slug}`, {
    pick: ["title", "summary"],
});
</script>

صفحهٔ مرجع pick را فقط «نگه داشتن کلیدهای مشخص از نتیجه» توصیف می‌کند و جزئیات بیشتری نمی‌دهد؛ بنابراین دربارهٔ مسیرهای تو در تو چیزی فرض نکنید. شکل داده‌ٔ خودتان را آزمایش و خروجی را بررسی کنید.

الگوی بهتر ۲: transform

transform نتیجهٔ resolve‌شده را می‌گیرد و چیزی را که ذخیره می‌شود برمی‌گرداند. این ابزار برای فهرست‌هاست، جایی که می‌خواهید هر آیتم را بازشکل‌دهی کنید:

ts
const { data: articles } = await useFetch("/api/articles", {
    transform: (list) => list.map((a) => ({ title: a.title, slug: a.slug })),
});

این مثال از نمونهٔ map در خود راهنما پیروی می‌کند. طبق مرجع، transform می‌تواند async هم باشد، چون نوع آن DataT یا Promise از آن است.

آنچه هیچ‌کدام صرفه‌جویی نمی‌کنند

هر دو گزینه فقط روی پیلود اثر دارند. راهنما می‌گوید از افزوده شدن داده‌های ناخواسته به پیلودِ منتقل‌شده از سرور به کلاینت جلوگیری می‌کنند، و خود درخواست تغییری نمی‌کند. سرور همچنان پاسخ کامل را دانلود و parse می‌کند. اگر هزینهٔ واقعی در فراخوانی بالادستی است، آن را از مبدأ حل کنید: endpoint باریک‌تر، یک query parameter، یا یک server route که فقط فیلدهای لازم را برگرداند.

serialize: false (از v4.6)

مرجع گزینهٔ serialize را با پیش‌فرض true فهرست کرده و آن را از v4.6 به بعد علامت زده است. با serialize: false، داده‌ای که روی سرور واکشی شده اصلاً وارد پیلود نمی‌شود. اگر کامپوننتی آن را رندر کند، کلاینت پس از hydration دوباره آن را می‌گیرد. مستندات پیشنهاد می‌کنند آن را با hydration تنبل ترکیب کنید تا از mismatch و واکشی‌های اضافی کلاینت جلوگیری شود.

ts
const { data: recommendations } = await useAsyncData(
    "recommendations",
    (_nuxtApp, { signal }) => $fetch("/api/recommendations", { signal }),
    { serialize: false },
);

این یک معامله‌ٔ متفاوت است. pick و transform همچنان از واکشی دوبارهٔ کلاینت جلوگیری می‌کنند. serialize: false داده را از HTML حذف می‌کند و بهایش را با یک درخواست دوم می‌پردازد. برای داده‌های بزرگ و غیرحیاتی که نمی‌خواهید در سند باشند منطقی است، و برای هر چیزی که بالای خط تاشو است مناسب نیست، چون رفت‌وبرگشت اضافی به‌شکل رندر دیرهنگام یا mismatch دیده می‌شود. من آن را اندازه نگرفته‌ام و مستندات هم دربارهٔ اندازهٔ پیلود برای این گزینه حرفی نمی‌زنند؛ پیش از پذیرفتن آن اندازه بگیرید.

گزینه‌ها را در همهٔ فراخوانی‌ها یکسان نگه دارید

مرجع می‌گوید گزینه‌هایی مثل handler، deep، transform، pick، getCachedData، default و middleware باید در همهٔ فراخوانی‌های useAsyncData که یک key مشترک دارند یکسان باشند و در غیر این صورت در حالت توسعه هشدار داده می‌شود. این‌جا مهم است: اگر یک کامپوننت دو فیلد را pick کند و کامپوننت دیگری همان key را بدون pick صدا بزند، هر دو سر یک ورودی کش دعوا می‌کنند. یا key و گزینه‌ها را از طریق یک composable به اشتراک بگذارید، یا از keyهای جدا استفاده کنید. این ادامهٔ بحث پستی قبلی دربارهٔ keyها به‌عنوان قرارداد کش مشترک است.

ts
// composables/useArticleCard.ts
export function useArticleCard(slug: MaybeRefOrGetter<string>) {
    return useFetch(() => `/api/articles/${toValue(slug)}`, {
        key: () => `article-card:${toValue(slug)}`,
        pick: ["title", "summary"],
    });
}

گزینهٔ key و URL به‌شکل getter کاربرد استاندارد useFetch هستند. خود composable تصویرسازی من است و الگویی از مستندات نیست.

چطور مشکل را ببینیم

راهنما به تب Payload در Nuxt DevTools اشاره می‌کند. پیلود همان شیئی است که از useNuxtApp().payload در دسترس است، پس می‌توانید آن را در کنسول هم بررسی کنید. محتوای آن را با آنچه قالب می‌خواند مقایسه کنید. برای برآورد تقریبی اندازه، سورس صفحه را باز کنید و بلوک اسکریپت __NUXT_DATA__ را ببینید.

بده‌بستان‌ها و نکات احتیاطی

  • تایپ‌ها. اگر داده را با transform بازشکل‌دهی کنید، نوع data از مقدار بازگشتی پیروی می‌کند. کامپوننت‌هایی که رکورد کامل را انتظار دارند type check را رد می‌شوند، که یک مزیت است: می‌بینید چه کسی به فیلدهای حذف‌شده وابسته بود.
  • keyهای مشترک. همان‌طور که گفته شد، گزینه‌های ناهمسان روی یک key مشترک در توسعه هشدار می‌دهند. این هشدار را باگ واقعی بدانید، نه نویز.
  • getCachedData. طبق مرجع، پیش‌فرض فقط وقتی داده کش‌شده می‌دهد که experimental.payloadExtraction فعال باشد، و در غیر این صورت از پیلودِ hydration می‌خواند. کوچک کردن پیلود، چیزی را هم که hydration می‌تواند دوباره استفاده کند کوچک می‌کند؛ پس آنچه حذف می‌کنید نباید برای ناوبری سمت کلاینتی که رکورد کامل را انتظار دارد لازم باشد.
  • بهینه‌سازی زودهنگام. رکورد ۲ کیلوبایتی به این کار نیاز ندارد. از endpointهایی شروع کنید که بزرگ‌ترین بدنه‌ها را برمی‌گردانند، یا فهرست‌هایی که در صفحه‌های زیادی رندر می‌شوند.
  • واکشی اضافی می‌ماند. اگر API بسیار بیشتر از نیاز صفحه برمی‌گرداند، این ترفند علامت را در مسیر شبکه به مرورگر پنهان می‌کند، نه روی سرور.

کجا الگوی اولیه هنوز درست است

اگر بیشتر پاسخ را رندر می‌کنید یا پاسخ کوچک است، پیش‌فرض‌ها درست‌اند. استفادهٔ دوبارهٔ hydration هدف پیلود است و جنگیدن با آن با serialize: false روی داده‌های بالای خط تاشو، سند بزرگ‌تر را با صفحهٔ کندتر معاوضه می‌کند. برای رکوردهای بزرگ به‌طور پیش‌فرض از pick و transform استفاده کنید و serialize: false را برای داده‌ای نگه دارید که هم بزرگ است و هم واقعاً ثانویه.

منابع