تخطَّ إلى المحتوى

أين يكلّفك `use client` فعلًا

حدّ العميل ليس إعداد أداء، بل حدّ حزمة. إليك كيف تكتشف إلى أين انزلق حدّك صعودًا، وما قيمة إعادته إلى مكانه.

3 دقيقة قراءة

لا تعني use client أن «هذا المكوّن يعمل على العميل». فكل مكوّن في تطبيق Next.js يُعرض على الخادم مرة واحدة على الأقل.

ما تُعلنه use client في الحقيقة هو حدّ: من هذه الوحدة نزولًا، يُترجَم كل شيء داخل حزمة جافاسكريبت ويُرسَل إلى المتصفّح كي يتمكّن من الترطيب (hydration). تلك هي الكلفة. ليست في العرض، بل في الإرسال.

ما إن تستقرّ هذه التعريفة في ذهنك حتى تصبح الأخطاء الشائعة بديهية.

الانزلاق صعودًا

تنزلق الحدود نحو الجذر، قرارًا معقولًا تلو الآخر.

يطلب التصميم قائمة منسدلة في الترويسة. تحتاج الترويسة إلى useState، فتوضع use client في أعلى Header.tsx. والترويسة تستورد Nav، الذي يستورد NavItem، الذي يستورد أداة formatDate، التي تستورد مكتبة تواريخ. لم يكن أيٌّ من ذلك بحاجة إلى التفاعل. وكلّه الآن داخل الحزمة، في كل صفحة، لكل زائر.

وبعد ستة أشهر يصبح التخطيط مكوّن عميل ولا أحد يتذكّر السبب.

كيف تجد حدّك الحقيقي

مخرجات البناء تدلّك على موضع الخلل. انظر إلى First Load JS لكل مسار:

Route (app)                      Size  First Load JS
┌ ○ /                          1.2 kB         184 kB
├ ○ /about                     0.8 kB         184 kB
└ ○ /pricing                   2.1 kB         186 kB

حين تحمل كل المسارات القاعدة الكبيرة نفسها، فالوزن في التخطيط المشترك لا في الصفحات. تلك بصمة حدّ عميل تسلّق إلى شجرة التخطيط.

وللتفاصيل، شغّل محلّل الحزم:

npm install --save-dev @next/bundle-analyzer
ANALYZE=true npm run build

أنت تبحث عن أمرين: مكتبات لم تكن تتوقّعها، ومكتبات تظهر في الجزء المشترك بينما مكانها جزء مسار واحد.

دفع الحدّ إلى الأسفل

يكاد الحل يأخذ الشكل نفسه دائمًا — تُبقي الورقة التفاعلية مكوّن عميل، وتُبقي كل ما حولها على الخادم.

بدل هذا:

'use client';
 
import { heavyMarkdownRenderer } from 'some-large-lib';
 
export function Article({ content, comments }) {
  const [showComments, setShowComments] = useState(false);
 
  return (
    <article>
      {heavyMarkdownRenderer(content)}
      <button onClick={() => setShowComments(true)}>Comments</button>
      {showComments ? <Comments data={comments} /> : null}
    </article>
  );
}

افعل هذا:

// Article.tsx — server component, no 'use client'
import { heavyMarkdownRenderer } from 'some-large-lib';
import { CommentsToggle } from './CommentsToggle';
 
export function Article({ content, comments }) {
  return (
    <article>
      {heavyMarkdownRenderer(content)}
      <CommentsToggle>
        <Comments data={comments} />
      </CommentsToggle>
    </article>
  );
}
// CommentsToggle.tsx — the only client component
'use client';
 
export function CommentsToggle({ children }: { children: ReactNode }) {
  const [open, setOpen] = useState(false);
 
  return (
    <>
      <button onClick={() => setOpen(true)}>Comments</button>
      {open ? children : null}
    </>
  );
}

تبقى مكتبة markdown على الخادم. ويبقى Comments مكوّن خادم رغم عرضه داخل مكوّن عميل — لأنه مُمرَّر كـchildren، فقد عُرض على الخادم مسبقًا ويصل مخرجًا مُسلسلًا لا شيفرة.

الأبناء المُمرَّرون عبر مكوّن عميل لا يعبرون الحدّ. هذه القاعدة وحدها تحلّ معظم الحالات التي يظن فيها الفريق أنه مضطرّ للانتقال إلى جهة العميل.

لماذا يظهر هذا في INP لا في زمن التحميل فقط

يقيس مؤشر Interaction to Next Paint الزمن الذي يستغرقه الخيط الرئيسي للاستجابة حين ينقر الزائر شيئًا. فكل كيلوبايت جافاسكريبت في الحزمة يُحلَّل ويُترجَم ويُنفَّذ قبل أن تتمكّن الصفحة من الاستجابة لأي شيء، والترطيب يزاحم أول نقرة للزائر.

على جهاز أندرويد متوسّط الفئة — وهو جهاز معظم العالم — تعادل 300 كيلوبايت من جافاسكريبت نحو ثانية من العمل على الخيط الرئيسي قبل اكتمال الترطيب. والنقرة خلال تلك الثانية تدخل طابورًا. ذلك فشل في INP، ولن يصلحه أي قدر من العمل على CSS.

مكوّنات الخادم ليست أساسًا تحسينًا للعرض، بل طريقة لعدم إرسال الشيفرة من الأصل.

أشياء لا تحتاج أن تكون مكوّنات عميل

من التدقيقات التي نُجريها، هذه الإنذارات الكاذبة المتكرّرة:

  • تنسيق التواريخ والأرقام. تعمل Intl على الخادم. وإن كان الهمّ المنطقة الزمنية للزائر، مرّر النص المنسّق إلى الأسفل، أو اعرض عنصر <time> بخاصية dateTime واترك العرض لـCSS أو لمكوّن ورقيّ صغير.
  • قراءة السمة (theme). تكفي خاصية data-theme مع CSS دون جافاسكريبت.
  • جلب البيانات عند التركيب. إن لم تكن خاصة بالمستخدم فاجلبها على الخادم، وإن كانت كذلك فاجلبها على الخادم خلف Suspense.
  • التحليلات. حمّلها داخل <Script> بـstrategy="afterInteractive" أو "lazyOnload"، لا داخل مكوّن.
  • مكتبات الأيقونات. الأيقونات ترميز. استورد ملف SVG المفرد، لا ملف البرميل الذي يجرّ ألفين منها.

قاعدة المراجعة التي نتبعها

في كل قاعدة شيفرة نعمل فيها، تُعامَل use client معاملة البادئة dangerously: مسموح بها، وصحيحة أحيانًا، ولا تمرّ مراجعةً قط دون جملة تشرح لماذا ينتمي الحدّ إلى هذا الملف بالذات لا إلى مستوى أدنى.

هذا العُرف وحده أثمن من أي أداة لقياس حجم الحزمة، لأنه يلتقط المشكلة لحظة نشوئها لا بعد ثمانية عشر شهرًا حين يفتح أحدهم المحلّل أخيرًا.

العودة إلى كل المقالات