كيف نُصحّح أخطاء عدم تطابق الترطيب في موجّه تطبيقات Next.js
جدول المحتويات
عن الكاتب

Ahmed Mahmoud
كاتب ومطوّر
مهندس برمجيات شغوف بتطوير الويب وتصميم تجربة المستخدم.
مؤسّس ديفيا · eng-ahmed.com ↗أبرز النقاط
عدم تطابق الترطيب (Hydration mismatch) يحدث عندما لا يطابق HTML الذي يُصيّره خادم Next.js ناتج أول تصيير من React في المتصفح، مما يدفع React 19 إلى التخلّص من ذلك الجزء وإعادة تصييره في المتصفح.
أكثر الأسباب شيوعًا هي القيم غير الحتمية مثل Date.now() وMath.random()، وواجهات المتصفح فقط مثل localStorage وwindow عند قراءتها أثناء التصيير، وتداخل HTML غير الصالح، والتعديلات التي تحقنها إضافات المتصفح في DOM.
الخاصية suppressHydrationWarning ليست حلًّا عامًّا؛ فهي تكتم التحذير لعنصر واحد يُسمح لنصه بالاختلاف بشكل مشروع، مثل الطابع الزمني.
لعرض القيم الخاصة بالمتصفح بأمان، استخدم علامة mounted مع useEffect أو useSyncExternalStore مع لقطة خادم حتى يتفق الخادم وأول تصيير في المتصفح.
عدم تطابق الترطيب يظهر فقط عند أول تحميل مُصيَّر من الخادم، ولا يظهر أبدًا أثناء التنقّل داخل المتصفح في App Router.
ما هو عدم تطابق الترطيب ولماذا يُعطّل الصفحة؟
عدم تطابق الترطيب هو أن يختلف HTML الذي يُصيّره Next.js على الخادم عن ناتج أول تصيير من React في المتصفح. أثناء الترطيب، يمرّ React على DOM القادم من الخادم ويربط مستمعي الأحداث به بدلًا من إعادة إنشائه، على افتراض أن الشجرة التي يحسبها تطابق DOM الموجود في الصفحة. وعندما يختلف الاثنان، يتخلّص React 19 من الجزء غير المتطابق ويعيد تصييره من الصفر في المتصفح. هذا التصيير المُعاد هو الوميض الذي نطارده، وهو يُهدر فائدة التصيير من الخادم لذلك الجزء. وفي بيئة التطوير، صار React 19 يطبع فرقًا (diff) واضحًا يذكر العنصر والنص المختلف، وهو تحسّن كبير عن الرسالة العامة القديمة.
لماذا تسبّب Date.now() وlocalStorage خطأ ترطيب؟
قراءة قيمة غير حتمية أو خاصة بالمتصفح أثناء التصيير هي أكثر الأسباب التي نراها. فـ Date.now() وMath.random() وnew Date().toLocaleString() تُنتج ناتجًا مختلفًا على الخادم عنه في المتصفح، لأن الخادم يعمل قبل لحظات وبمنطقة زمنية ولغة مختلفة. وقراءة localStorage أو window أو navigator أثناء التصيير أسوأ، لأن الخادم لا يملك هذا الكائن فيصيّر فرعًا مختلفًا عن المتصفح. الخادم يسجّل الوقت الذي صيّر فيه بينما يسجّل المتصفح وقتًا لاحقًا بلغة المستخدم، فيرى React نصّين مختلفين ويُطلق الخطأ.
متى نستخدم suppressHydrationWarning؟
نلجأ إلى suppressHydrationWarning فقط عندما يُسمح لنص عنصر واحد بالاختلاف بشكل مشروع بين الخادم والمتصفح، والطابع الزمني هو الحالة النموذجية. فهي تكتم التحذير لذلك العنصر الواحد بمستوى واحد فقط؛ ولا تُسكِت أبناءه وليست حلًّا شاملًا. واستخدامها لإخفاء عدم تطابق منطقي حقيقي يستبدل خطأً مرئيًّا بآخر خفيّ، لذا إن انتشرت إلى أكثر من عقدتين طرفيّتين فالمشكلة الحقيقية في مكان آخر.
كيف نعرض قيمة خاصة بالمتصفح دون عدم تطابق؟
لعرض قيمة لا توجد إلا في المتصفح، مثل السمة أو عرض الشاشة أو تفضيل مخزَّن، نصيّر عنصرًا نائبًا ثابتًا على الخادم ثم نستبدله بالقيمة الحقيقية بعد التحميل. نمط المرورين يستخدم علامة mounted: قيمة حالة تبدأ بـ false يجعلها useEffect تساوي true، فيعيد الخادم وأول تصيير في المتصفح الهيكل النائب نفسه ولا تظهر القيمة الحقيقية إلا في التصيير الثاني. والخيار الأنظف لحالة المتصفح الخارجية هو useSyncExternalStore، إذ أن وسيطه الثالث لقطة خادم تُبقي القيمة معرَّفة على الجانبين. أما المكوّن الذي لا يمكن تصييره على الخادم إطلاقًا، فنستخدم next/dynamic مع ضبط خيار ssr على false لتخطّي التصيير من الخادم كليًّا.
لماذا تسبّب إضافات المتصفح أخطاء ترطيب في عنصر body؟
تسبّب إضافات المتصفح أخطاء الترطيب لأنها تعدّل DOM قبل أن يُرطّبه React. فـ Grammarly تحقن سمات data-gramm، ومديرو كلمات المرور يضيفون أغلفة، وإضافات الوضع الداكن تعيد كتابة الأنماط داخل عنصري html وbody. أرسل الخادم ترميزًا نظيفًا، لكن حين يُرطّب React يكون DOM قد حمل سمات إضافية فيحدث عدم التطابق. ولأننا لا نتحكم في إضافات المستخدم، فالحل المقبول هو إضافة suppressHydrationWarning إلى عنصري html وbody في التخطيط الجذري، وهو ما توصي به وثائق Next.js. وهو آمن هناك لأن هذين العنصرين لا يحملان نصًّا ديناميكيًّا خاصًّا بنا نحميه.
كيف نصلح حالات عدم تطابق تداخل HTML غير الصالح؟
ينتج تداخل HTML غير الصالح حالات عدم تطابق تبدو غامضة لأن JSX يبدو صحيحًا. فالمتصفح يصلح التداخل غير القانوني بصمت بنقل الوسوم أو إغلاقها، فلا يعود DOM الذي يبنيه مطابقًا للشجرة التي يتوقعها React، فيُبلّغ React عن عدم تطابق في عنصر لم يلمسه المطوّر.
عنصر div داخل p يفشل لأن p يُغلَق تلقائيًّا قبل عنصر كتلي، لذا استخدم div أو span غلافًا بدلًا منه.
عنصر p داخل p يفشل لأن p المتداخل غير مسموح ويُغلَق الخارجي مبكرًا، لذا سطّحه إلى p واحد.
رابط داخل رابط يفشل لأن العناصر التفاعلية لا يمكن أن تتداخل، لذا أعد ترتيب الترميز لتصبح الروابط أشقّاء.
الحل دائمًا هو جعل JSX يُنتج HTML صالحًا، وفرق React 19 غالبًا يشير مباشرة إلى العقدة المُعاد ترتيبها.
الأسئلة الشائعة
ما الفرق بين عدم تطابق الترطيب وخطأ التصيير؟
عدم تطابق الترطيب هو تحديدًا اختلاف HTML الخادم عن أول تصيير في المتصفح، ويظهر فقط عند التحميل الأول ولا يظهر أبدًا أثناء التنقّل داخل المتصفح. أما خطأ التصيير فهو أي استثناء يُطلَق أثناء التصيير في أي وقت.
هل يُصلح suppressHydrationWarning الوميض؟
لا. فهو يكتم تحذير الطرفية لعنصر واحد لكن React ما زال يعيد تصيير المحتوى غير المتطابق في المتصفح. استخدمه فقط عندما يكون الاختلاف مقصودًا، مثل الطابع الزمني.
لماذا يحدث الخطأ عند التحديث فقط وليس عند التنقّل؟
يجري الترطيب مرة واحدة، عند أول تحميل مُصيَّر من الخادم. أما التنقّل داخل المتصفح في App Router فيُصيَّر بالكامل في المتصفح، فلا يوجد HTML خادم لتقع مقارنته.
هل يمكن أن يسبّب useEffect عدم تطابق ترطيب؟
لا. يعمل useEffect بعد الترطيب، فالحالة التي تُضبط داخله لا يمكن أن تؤثر في أول تصيير يقارنه React بـ HTML الخادم، وهذا بالضبط سبب أمان نمط علامة mounted.
كيف نعرف أي مكوّن يسبّب عدم التطابق؟
اقرأ فرق طرفية التطوير في React 19، الذي يذكر العنصر ويعرض النص المختلف. وإن اشتُبه في إضافة، فأعد إنتاج الصفحة في نافذة تصفّح خفي مع تعطيل الإضافات.
قراءات إضافية
هندسة الواجهات
الوضع الداكن دون وميض في Next.js 16: كيف نستخدم سكربت سمة حاجبًا وcolor-scheme وزر تبديل آمنًا مع الـ Hydration
وميض الوضع الداكن عند إعادة تحميل الصفحة مشكلة توقيت: الخادم لا يستطيع قراءة localStorage. نشرح سكربت السمة الحاجب الذي نستخدمه في Next.js 16، ولماذا لا نضع السمة في cookie حفاظًا على الرسم الثابت، وكيف نبني زر تبديل يمر بالـ hydration دون أي عدم تطابق.
هندسة الواجهات
صور Open Graph الديناميكية في Next.js 16: كيف نبني opengraph-image.tsx ونتحقق من إصدار Satori الذي تضمّنه next/og ونحافظ على صحة معاينات الروابط
واجهت بطاقات المشاركة المولّدة لدينا ثلاث مفاجآت: خط WOFF2 لم يستطع المحرّك قراءته، وملف README لـ Satori يصف محرّكًا أحدث من المضمّن في Next.js، وصفحة ظلّت تعرض صورة الـ layout. هذه ملاحظاتنا الميدانية حول opengraph-image.tsx في Next.js 16 وأولوية البيانات الوصفية وفحص og:image قبل مشاركة أي رابط.