وقتی قابلیت مخفیسازی ایمیل در CDN هیدریشن React را شکست
بعد از فعالسازی CDN، بخشهایی از فروشگاه Next.js تعاملی نمیشدند. ماجرای مخفیسازی ایمیل، عدم تطابق hydration و راهحل آن.
بعد از اینکه CDN را جلوی یکی از سایتهای فروشگاهی Next.js خودمان فعال کردیم، چیزی عجیب پیش آمد: صفحه درست دیده میشد، اما بعضی بخشهایش بعد از لود تعاملی نمیشدند و کنسول مرورگر خطای عدم تطابق hydration میداد. کد برنامه را عوض نکرده بودیم؛ فقط یک لایه بین سرور و مرورگر اضافه شده بود. این یادداشت روایت همان ماجرا و توضیح این است که چرا هر چیزی که HTML را وسط راه تغییر بدهد، با SSR و hydration سازگار نیست.
ماجرا و علامتها
سایت فروشگاهی ما با Next.js و رندر سمت سرور کار میکند. تا قبل از فعالسازی CDN همهچیز عادی بود. بعد از فعالسازی، ظاهر صفحهها سالم به نظر میرسید، اما بخشهایی از آنها بعد از لود به رفتار کاربر واکنشی نشان نمیدادند؛ یعنی HTML آمده بود ولی جاوااسکریپت به آنها «وصل» نشده بود.
در کنسول مرورگر خطای عدم تطابق hydration میدیدیم. این خطا یعنی درختی که React در مرورگر میسازد با HTMLای که از سرور گرفته یکی نیست. چیزی که آزارمان میداد این بود که روی سرور و در توسعه محلی مشکل تکرار نمیشد و فقط وقتی ترافیک از مسیر CDN میگذشت دیده میشد.
این الگو، یعنی «محلی سالم و پشت CDN خراب»، خودش سرنخ بزرگی بود، هرچند همان لحظه آن را بهعنوان سرنخ نخواندیم.
نکات کلیدی
- صفحه از نظر ظاهری درست بود، اما بخشهایی بعد از لود تعاملی نمیشدند
- کنسول خطای عدم تطابق hydration میداد
- مشکل فقط بعد از قرار گرفتن CDN جلوی سایت دیده میشد
ردگیری علت
اولین واکنش ما طبیعی بود: سراغ کد رفتیم. دنبال چیزهایی گشتیم که معمولاً این خطا را میسازند؛ مثلاً استفاده از مقدار وابسته به زمان در رندر، شرطهایی که به وجود window وابستهاند، یا تو در تویی نامعتبر تگهای HTML. مستندات رسمی Next.js و React همینها را بهعنوان علتهای رایج فهرست میکنند. این فرضیهها صادقانه غلط بودند: کد بدون CDN بدون خطا کار میکرد، پس مشکل از منطق رندر نبود.
فرضیه بعدی افزونههای مرورگر بود، چون آنها هم گاهی HTML را تغییر میدهند. اما خطا در مرورگرهای مختلف و بدون افزونه هم دیده میشد، پس آن را کنار گذاشتیم.
نقطه عطف وقتی بود که بهجای نگاه کردن به کد، HTML خام را در دو حالت مقایسه کردیم: یک بار مستقیم از سرور مبدأ و یک بار از مسیر CDN. این دو یکسان نبودند. در نسخهای که از CDN میآمد، آدرسهای ایمیل داخل صفحه دیگر بهصورت متن ساده نبودند و بهجایشان تگها و اسکریپتی دیگر نشسته بود. از اینجا مسئله روشن شد.
# مقایسه HTML مستقیم از مبدأ با HTML عبوری از CDN (آدرسها نمونهاند)
curl -s --resolve example.com:443:203.0.113.10 https://example.com/ > origin.html
curl -s https://example.com/ > via-cdn.html
diff origin.html via-cdn.htmlعلت اصلی: HTML دیگر همان HTML سرور نبود
علت قابلیت «مخفیسازی ایمیل» در CDN بود. این قابلیت برای مقابله با رباتهایی طراحی شده که ایمیلها را از HTML جمع میکنند. شیوه کارش کلی است: پاسخ HTML را در مسیر میخواند، آدرسهای ایمیل را پیدا میکند، بهجایشان یک نشانگر یا تگ جایگزین میگذارد و یک اسکریپت کوچک هم به صفحه اضافه میکند تا در مرورگر آن را دوباره به آدرس واقعی برگرداند.
برای یک صفحه ایستا این معامله بیضرر است. اما برای SSR با hydration، مسئله اساسی دیگری دارد. فرایند hydration اینطور است: سرور HTML را رندر میکند، مرورگر آن را نشان میدهد، و بعد React در مرورگر همان درخت را دوباره در حافظه میسازد و فرض میکند DOM موجود دقیقاً با آن میخواند. اگر بخواند، فقط event handlerها را به عناصر موجود وصل میکند و DOM را از نو نمیسازد.
مستندات React صریح است: hydrateRoot انتظار دارد محتوای رندرشده با خروجی سرور یکسان باشد، عدم تطابقها باید مثل باگ جدی گرفته شوند، و تضمینی نیست که تفاوتها اصلاح شوند. در همان مستندات آمده که در بدترین حالت ممکن است event handlerها به عنصرهای اشتباه وصل شوند. مستندات Next.js هم صراحتاً یکی از علتها را «Edge یا CDN پیکربندیشدهای که پاسخ HTML را تغییر میدهد» میداند و حتی بهینهسازهایی مانند minify خودکار را مثال میزند.
در مورد ما، سرور متنی تولید میکرد و CDN آن را عوض میکرد، پس چیزی که مرورگر دریافت میکرد با چیزی که React انتظارش را داشت فرق داشت. نتیجه همان بود که دیدیم: خطای hydration و بخشهایی که تعاملی نشدند. نکته مهم این است که هیچجای زنجیره «خراب» نبود. سرور درست کار میکرد، React درست کار میکرد و CDN هم دقیقاً کاری را میکرد که برایش تنظیم شده بود. خرابی از ترکیب این سه درآمد.
نکات کلیدی
- hydration فرض میکند DOM موجود با خروجی رندر مرورگر یکی است
- هر لایهای که HTML را بین سرور و مرورگر بازنویسی کند، این فرض را میشکند
- هیچکدام از اجزا بهتنهایی معیوب نبودند؛ مشکل در ترکیبشان بود
راهحل
راهحل ساده بود: قابلیت مخفیسازی ایمیل را برای این سایت در تنظیمات CDN خاموش کردیم. بعد از آن HTML عبوری از CDN با HTML مبدأ همخوان شد و خطای hydration از کنسول رفت.
اما خاموش کردن یک گزینه کافی نبود. وقتی فهمیدیم یک بهینهساز HTML مشکلساز بوده، بقیه بهینهسازهای مشابه CDN را هم مرور کردیم: چیزهایی مثل minify خودکار HTML و تزریق اسکریپت. هر کدام از اینها هم HTML را وسط راه تغییر میدهند و میتوانند همین ریسک را داشته باشند، حتی اگر در آن لحظه مشکلی نشان نداده بودند.
برای کنترل بعد از تغییر، همان مقایسهای که در بخش قبل آمد را دوباره اجرا کردیم تا مطمئن شویم تفاوتی در بدنه HTML نمانده است. اگر نیاز دارید یک آدرس ایمیل را عمداً داخل صفحه نگه دارید و CDN به آن دست نزند، بهتر است این کار را با قابلیتهای مستند خود CDN انجام دهید، نه با حدس زدن.
نکته جانبی: در مستندات Next.js به مورد مشابهی در iOS هم اشاره شده که ایمیل و شماره تلفن را خودکار به لینک تبدیل میکند و همین مغایرت میسازد. راهش یک متای مشخص است که این تشخیص خودکار را خاموش میکند.
<!-- جلوگیری از لینکسازی خودکار ایمیل و تلفن در iOS -->
<meta name="format-detection" content="telephone=no, date=no, email=no, address=no" />درسها و چکلیست
درس اصلی این بود که هر چیزی بین سرور و مرورگر که HTML را تغییر بدهد، دشمن SSR و hydration است. وقتی یک پروژه SSR دارید، HTML خروجی سرور یک قرارداد است، نه فقط یک قالب برای نمایش. هر لایهای که آن را بخواند و بازنویسی کند، قرارداد را نقض میکند.
یک درس عملی دیگر این بود که علت را زودتر از جای درست بجوییم. وقتی مشکل فقط پشت یک لایه خاص رخ میدهد، قبل از خواندن کد برنامه باید خروجی خام را قبل و بعد از آن لایه مقایسه کرد. اگر این کار را از اول کرده بودیم، چند فرضیه غلط را رد نکرده کنار میگذاشتیم.
اگر روی فروشگاه یا سایتی با Next.js کار میکنید و CDN اضافه میکنید، این چکلیست را پیشنهاد میکنم. برای نگاه گستردهتر به عملکرد و خزش سایت، سئوی فنی فروشگاه اینترنتی با Next.js را هم ببینید و اگر پروژه شما اساساً یک فروشگاه است، طراحی فروشگاه اینترنتی و وباپلیکیشن همین حوزه است. برای نمونهای از همین نوع سایتها، پروژه فروشگاه اینترنتی عینکسازی را هم میتوانید ببینید. اگر مشکلهای دیگری پشت CDN برایتان پیش آمده، ماجرای گواهی SSL منقضیشده سرور مبدأ پشت CDN مثال دیگری از رفتار پنهان این لایه است.
نکات کلیدی
- قبل از فعالسازی CDN، HTML خام مبدأ را با HTML عبوری از CDN مقایسه کنید
- قابلیتهایی که HTML را بازنویسی میکنند (مخفیسازی ایمیل، minify خودکار، تزریق اسکریپت) را یکییکی بررسی و در صورت نیاز خاموش کنید
- بعد از هر تغییر تنظیمات CDN، کنسول مرورگر را برای هشدار hydration چک کنید
- در خطای hydration فقط کد را مقصر ندانید؛ مسیر بین سرور و مرورگر را هم بررسی کنید
- بهینهسازی HTML را بهتر است در خود بیلد و سرور انجام دهید، نه در لایه میانی
جمعبندی
عدم تطابق hydration همیشه باگ کد نیست؛ گاهی HTML سرور در راه عوض میشود. در ماجرای ما، قابلیت مخفیسازی ایمیل CDN آدرسها را با تگ و اسکریپت دیگری جایگزین میکرد و همین باعث شد بخشهایی از سایت تعاملی نشوند. با خاموش کردن آن و بررسی بقیه بهینهسازهای HTML، مشکل حل شد. قاعدهای که از آن گرفتیم ساده است: بین سرور و مرورگر، HTML نباید دستکاری شود.