سیداحمد
سیداحمد
خواندن ۲۵ دقیقه·۱ سال پیش

بهبود عملکرد با HTTP Streaming

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

بیایید ابتدا بفهمیم استریم یعنی چه. تصور کنید ما یک میله و دو گزینه داشتیم:یک فنجان بزرگ را پر کنید، و سپس همه آن را در لوله بریزید (استراتژی "بافر")اسپیگوت را مستقیماً به لوله وصل کنید (استراتژی "جریان")در استراتژی بافر، همه چیز به صورت متوالی اتفاق می افتد - سرورهای ما ابتدا کل پاسخ را در یک بافر (پر کردن فنجان) تولید می کنند و سپس زمان بیشتری برای ارسال آن از طریق شبکه (ریختن آن) صرف می شود. استراتژی استریم به صورت موازی اتفاق می افتد. ما پاسخ را به قطعات تقسیم می کنیم که به محض آماده شدن ارسال می شود. سرور می تواند در حالی که تکه های قبلی هنوز در حال ارسال هستند، روی قسمت بعدی کار کند و مشتری (مثلاً یک مرورگر) می تواند قبل از دریافت کامل پاسخ، رسیدگی به آن را آغاز کند.

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

با این حال، یک مورد استفاده وجود دارد که به طور جهانی قابل اجرا است. ما می توانیم از جریان برای کاهش آبشارهای شبکه استفاده کنیم . این اصطلاح به زمانی اشاره دارد که یک درخواست شبکه دیگری را راه‌اندازی می‌کند و منجر به یک سری درخواست‌های متوالی می‌شود. این به راحتی در ابزاری مانند Chrome's Waterfall قابل مشاهده است :بیشتر صفحات وب به فایل‌های جاوا اسکریپت و CSS خارجی که در داخل HTML پیوند داده شده‌اند متکی هستند، که منجر به آبشار شبکه می‌شود — دانلود HTML باعث دانلود JavaScript و CSS می‌شود. در نتیجه، قرار دادن تمام تگ های CSS و جاوا اسکریپت در نزدیکی ابتدای HTML در تگ بهترین روش است <head>. این تضمین می کند که مرورگر آنها را زودتر ببیند. با پخش جریانی، می‌توانیم با ارسال <head>ابتدا آن بخش از برچسب، این تاخیر را بیشتر کاهش دهیم.فلاش زودرسساده ترین راه برای ارسال یک <head>برچسب اولیه، شکستن یک پاسخ استاندارد به دو بخش است. این تکنیک Early Flush نامیده می شود ، زیرا یک قسمت قبل از دیگری ارسال می شود ("flushed").بخش اول شامل مواردی است که سریع محاسبه می شوند و می توان آنها را به سرعت ارسال کرد. در Airbnb، برچسب‌هایی را برای فونت‌ها، CSS و جاوا اسکریپت اضافه می‌کنیم تا از مزایای مرورگر ذکر شده در بالا بهره مند شویم. بخش دوم شامل بقیه صفحه، از جمله محتوایی است که برای محاسبه به API یا جستجوهای پایگاه داده متکی است. نتیجه نهایی به این صورت است:ما مجبور شدیم برنامه خود را بازسازی کنیم تا این امکان وجود داشته باشد. برای زمینه، Airbnb از یک سرور NodeJS مبتنی بر Express برای ارائه صفحات وب با استفاده از React استفاده می کند. ما قبلاً یک مؤلفه React داشتیم که وظیفه ارائه سند کامل HTML را بر عهده داشت. با این حال، این دو مشکل ایجاد کرد:تولید تکه های افزایشی محتوا به این معنی است که باید با تگ های HTML جزئی/بازشده کار کنیم. به عنوان مثال، نمونه هایی که در بالا مشاهده کردید HTML نامعتبر هستند. تگ‌های <html>و <head>در قسمت Early باز می‌شوند، اما در قسمت Late بسته می‌شوند. هیچ راهی برای تولید این نوع خروجی با استفاده از توابع استاندارد رندر React وجود ندارد.تا زمانی که همه داده‌های مربوط به آن را نداشته باشیم، نمی‌توانیم این مؤلفه را رندر کنیم.ما این مشکلات را با شکستن جزء یکپارچه خود به سه حل کردیم:یک مؤلفه "Early <head>".یک مؤلفه "Late <head>" برای تگ های <head> که به داده ها بستگی داردیک جزء "<body>".هر جزء محتویات برچسب سر یا بدن را ارائه می دهد. سپس آنها را با نوشتن تگ‌های باز/بستن مستقیماً در جریان پاسخ HTTP به هم می‌چسبانیم. به طور کلی، روند به صورت زیر است:نوشتن<html><head>رندر کنید و Early <head> را در پاسخ بنویسیدمنتظر داده باشید<head> را رندر کنید و روی پاسخ بنویسیدنوشتن</head><body>رندر کنید و <body> را در پاسخ بنویسیدبا نوشتن کار را تمام کنید</body></html>جریان دادهEarly Flush آبشارهای شبکه CSS و JavaScript را بهینه می کند. با این حال، کاربران همچنان به یک صفحه خالی خیره خواهند شد تا زمانی که <body>برچسب وارد شود. ما می‌خواهیم این حالت را با ارائه یک حالت بارگیری زمانی که داده‌ای وجود ندارد، بهبود دهیم، که پس از رسیدن داده‌ها جایگزین می‌شود. به‌راحتی، ما در حال حاضر وضعیت‌های بارگیری را در این وضعیت برای مسیریابی سمت مشتری داریم، بنابراین می‌توانیم این کار را فقط با رندر کردن برنامه بدون انتظار برای داده انجام دهیم!

متأسفانه، این باعث آبشار شبکه دیگری می شود. مرورگرها باید SSR (رندر سمت سرور) را دریافت کنند و سپس جاوا اسکریپت درخواست شبکه دیگری را برای واکشی داده های واقعی راه اندازی می کند:در آزمایش ما، این منجر به کاهش زمان بارگذاری کل شد .چه می شود اگر بتوانیم این داده ها را در HTML قرار دهیم؟ این اجازه می دهد تا رندر سمت سرور و واکشی داده ها به صورت موازی اتفاق بیفتد:

با توجه به اینکه قبلاً صفحه را با Early Flush به دو قسمت تقسیم کرده بودیم، معرفی یک قطعه سوم برای آنچه ما Deferred Data می نامیم نسبتاً ساده است . این قسمت به دنبال همه محتوای قابل مشاهده می رود و رندر را مسدود نمی کند. ما درخواست های شبکه را روی سرور اجرا می کنیم و پاسخ ها را در قسمت Deferred Data قرار می دهیم. در پایان، سه تکه ما به این صورت است:

با اجرای این مورد روی سرور، تنها وظیفه باقی مانده نوشتن مقداری جاوا اسکریپت است تا تشخیص دهد که چه زمانی قطعه داده معوق ما می رسد. ما این کار را با MutationObserver انجام دادیم ، که روشی کارآمد برای مشاهده تغییرات DOM است. هنگامی که عنصر Deferred Data JSON شناسایی شد، نتیجه را تجزیه می کنیم و آن را به فروشگاه داده شبکه برنامه خود تزریق می کنیم. از منظر برنامه، گویی یک درخواست شبکه عادی تکمیل شده است.مراقب «به تعویق انداختن» باشیدممکن است متوجه شوید که برخی از برچسب ها از مثال Early Flush دوباره مرتب شده اند. تگ های اسکریپت از قسمت Early به قسمت Body منتقل شدند و دیگر ویژگی defer را ندارند . این ویژگی با به تعویق انداختن اسکریپت‌ها تا زمانی که HTML دانلود و تجزیه شده است، از اجرای اسکریپت مسدود کردن رندر جلوگیری می‌کند. در هنگام استفاده از داده‌های معوق، این مقدار کمتر از حد مطلوب است، زیرا تمام محتوای قابل مشاهده قبلاً در انتهای قسمت Body دریافت شده است، و دیگر نگران مسدود کردن رندر در آن نقطه نیستیم. می‌توانیم این مشکل را با انتقال تگ‌های اسکریپت به انتهای قسمت Body و حذف ویژگی defer برطرف کنیم. جابجایی تگ‌ها بعداً در سند یک آبشار شبکه را معرفی می‌کند که با اضافه کردن تگ‌های پیش‌بارگذاری به قسمت اولیه آن را حل کردیم.چالش های پیاده سازیکدهای وضعیت و هدرهاEarly Flush از تغییرات بعدی در هدرها جلوگیری می کند (مثلاً برای تغییر مسیر یا تغییر کد وضعیت). در دنیای React + NodeJS، واگذاری تغییر مسیرها و خطاها به برنامه React که پس از واکشی داده‌ها ارائه می‌شود، معمول است. <head>اگر قبلاً یک برچسب اولیه و وضعیت OK 200 ارسال کرده باشید، این کار کار نخواهد کرد .ما این مشکل را با انتقال خطا و تغییر مسیر منطق به خارج از برنامه React خود حل کردیم. این منطق اکنون در میان افزار سرور اکسپرس قبل از اینکه بخواهیم Early Flush کنیم انجام می شود.بافر کردنما متوجه شدیم که nginx بافر به طور پیش فرض پاسخ می دهد. این دارای مزایای استفاده از منابع است، اما زمانی که هدف ارسال پاسخ های افزایشی باشد، نتیجه معکوس دارد. ما مجبور شدیم این سرویس ها را برای غیرفعال کردن بافر کردن پیکربندی کنیم. ما انتظار داشتیم با این تغییر استفاده از منابع افزایش یابد، اما تأثیر آن ناچیز بود.تاخیر در پاسخگوییما متوجه شدیم که پاسخ‌های Flush اولیه ما تاخیر غیرمنتظره‌ای حدود 200 میلی‌ثانیه داشتند که با غیرفعال کردن فشرده‌سازی gzip ناپدید شد. معلوم شد که این یک تعامل بین الگوریتم Nagle و Delayed ACK است . این بهینه‌سازی‌ها تلاش می‌کنند تا داده‌های ارسال شده در هر بسته را به حداکثر برسانند و در هنگام ارسال مقادیر کم داده، تأخیر ایجاد می‌کنند. برخورد با این مشکل با فریم های جامبو بسیار آسان است ، که حداکثر اندازه بسته ها را افزایش می دهد. به نظر می رسد که gzip اندازه نوشته های ما را به حدی کاهش داده است که نمی توانند یک بسته را پر کنند و راه حل این است که الگوریتم Nagle را در متعادل کننده بار هاپروکسی غیرفعال کنیم .نتیجه اکنون HTTP Streaming یک استراتژی بسیار موفق برای بهبود عملکرد وب در Airbnb بوده است. آزمایش‌های ما نشان داد که Early Flush در هر صفحه آزمایش‌شده، از جمله صفحه اصلی Airbnb، کاهش یکنواختی در First Contentful Paint (FCP) حدود 100 میلی‌ثانیه ایجاد کرد. جریان داده هزینه های FCP مربوط به پرس و جوهای آهسته پشتیبان را بیشتر حذف کرد. در حالی که چالش‌هایی در این مسیر وجود داشت، ما متوجه شدیم که تطبیق برنامه React موجود ما برای پشتیبانی از جریان بسیار عملی و قوی است، علیرغم اینکه در ابتدا برای آن طراحی نشده بود. ما همچنین از دیدن روند گسترده‌تر اکوسیستم frontend در جهت اولویت‌بندی جریان، از @defer و @stream در GraphQL تا جریان SSR در Next.js هیجان‌زده هستیم.. خواه از این فناوری‌های جدید استفاده می‌کنید، یا یک پایگاه کد موجود را گسترش می‌دهید، امیدواریم جریان‌سازی را بررسی کنید تا جلوه‌ای سریع‌تر برای همه بسازید!معرفیشاید این جوک را شنیده باشید که اینترنت یک سری لوله است . در این پست وبلاگ، ما قصد داریم در مورد اینکه چگونه با استفاده از HTTP Streaming در سریعترین زمان ممکن، یک جریان جالب و با طراوت از بایت های Airbnb.com را به مرورگر شما وارد می کنیم.

بیایید ابتدا بفهمیم استریم یعنی چه. تصور کنید ما یک میله و دو گزینه داشتیم:یک فنجان بزرگ را پر کنید، و سپس همه آن را در لوله بریزید (استراتژی "بافر")اسپیگوت را مستقیماً به لوله وصل کنید (استراتژی "جریان")در استراتژی بافر، همه چیز به صورت متوالی اتفاق می افتد - سرورهای ما ابتدا کل پاسخ را در یک بافر (پر کردن فنجان) تولید می کنند و سپس زمان بیشتری برای ارسال آن از طریق شبکه (ریختن آن) صرف می شود. استراتژی استریم به صورت موازی اتفاق می افتد. ما پاسخ را به قطعات تقسیم می کنیم که به محض آماده شدن ارسال می شود. سرور می تواند در حالی که تکه های قبلی هنوز در حال ارسال هستند، روی قسمت بعدی کار کند و مشتری (مثلاً یک مرورگر) می تواند قبل از دریافت کامل پاسخ، رسیدگی به آن را آغاز کند.

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

با این حال، یک مورد استفاده وجود دارد که به طور جهانی قابل اجرا است. ما می توانیم از جریان برای کاهش آبشارهای شبکه استفاده کنیم . این اصطلاح به زمانی اشاره دارد که یک درخواست شبکه دیگری را راه‌اندازی می‌کند و منجر به یک سری درخواست‌های متوالی می‌شود. این به راحتی در ابزاری مانند Chrome's Waterfall قابل مشاهده است :آبشار شبکه کروم که آبشاری از درخواست‌های متوالی را نشان می‌دهدبیشتر صفحات وب به فایل‌های جاوا اسکریپت و CSS خارجی که در داخل HTML پیوند داده شده‌اند متکی هستند، که منجر به آبشار شبکه می‌شود — دانلود HTML باعث دانلود JavaScript و CSS می‌شود. در نتیجه، قرار دادن تمام تگ های CSS و جاوا اسکریپت در نزدیکی ابتدای HTML در تگ بهترین روش است <head>. این تضمین می کند که مرورگر آنها را زودتر ببیند. با پخش جریانی، می‌توانیم با ارسال <head>ابتدا آن بخش از برچسب، این تاخیر را بیشتر کاهش دهیم.فلاش زودرسساده ترین راه برای ارسال یک <head>برچسب اولیه، شکستن یک پاسخ استاندارد به دو بخش است. این تکنیک Early Flush نامیده می شود ، زیرا یک قسمت قبل از دیگری ارسال می شود ("flushed").بخش اول شامل مواردی است که سریع محاسبه می شوند و می توان آنها را به سرعت ارسال کرد. در Airbnb، برچسب‌هایی را برای فونت‌ها، CSS و جاوا اسکریپت اضافه می‌کنیم تا از مزایای مرورگر ذکر شده در بالا بهره مند شویم. بخش دوم شامل بقیه صفحه، از جمله محتوایی است که برای محاسبه به API یا جستجوهای پایگاه داده متکی است. نتیجه نهایی به این صورت اس:< html >
< head >
< اسکریپت src = … به تعویق انداختن />
< پیوند rel = ”stylesheet” href = … />
<!--تعداد زیادی <meta> و تگ های دیگر… ->برچسب های <!-- <head> که به داده ها بستگی دارند به اینجا بروید ->
</head>
<body>
<! — محتوای متن اینجا →
</body>
</html>ما مجبور شدیم برنامه خود را بازسازی کنیم تا اینمتن وجود داشته باشد. برای زمینه، Airbnb از یک سرور NodeJS مبتنی بر Express برای ارائه صفحات وب با استفاده از React استفاده می کند. ما قبلاً یک مؤلفه React داشتیم که وظیفه ارائه سند کامل HTML را بر عهده داشت. با این حال، این دو مشکل ایجاد کرد:تولید تکه های افزایشی محتوا به این معنی است که باید با تگ های HTML جزئی/بازشده کار کنیم. به عنوان مثال، نمونه هایی که در بالا مشاهده کردید HTML نامعتبر هستند. تگ‌های <html>و <head>در قسمت Early باز می‌شوند، اما در قسمت Late بسته می‌شوند. هیچ راهی برای تولید این نوع خروجی با استفاده از توابع استاندارد رندر React وجود ندارد.تا زمانی که همه داده‌های مربوط به آن را نداشته باشیم، نمی‌توانیم این مؤلفه را رندر کنیم.ما این مشکلات را با شکستن جزء یکپارچه خود به سه حل کردیم:یک مؤلفه "Early <head>".یک مؤلفه "Late <head>" برای تگ های <head> که به داده ها بستگی داردیک جزء "<body>".هر جزء محتویات برچسب سر یا بدن را ارائه می دهد. سپس آنها را با نوشتن تگ‌های باز/بستن مستقیماً در جریان پاسخ HTTP به هم می‌چسبانیم. به طور کلی، روند به صورت زیر است:نوشتن<html><head>رندر کنید و Early <head> را در پاسخ بنویسیدمنتظر داده باشید<head> را رندر کنید و روی پاسخ بنویسیدنوشتن</head><body>رندر کنید و <body> را در پاسخ بنویسیدبا نوشتن کار را تمام کنید</body></html>جریان دادهEarly Flush آبشارهای شبکه CSS و JavaScript را بهینه می کند. با این حال، کاربران همچنان به یک صفحه خالی خیره خواهند شد تا زمانی که <body>برچسب وارد شود. ما می‌خواهیم این حالت را با ارائه یک حالت بارگیری زمانی که داده‌ای وجود ندارد، بهبود دهیم، که پس از رسیدن داده‌ها جایگزین می‌شود. به‌راحتی، ما در حال حاضر وضعیت‌های بارگیری را در این وضعیت برای مسیریابی سمت مشتری داریم، بنابراین می‌توانیم این کار را فقط با رندر کردن برنامه بدون انتظار برای داده انجام دهیم!

متأسفانه، این باعث آبشار شبکه دیگری می شود. مرورگرها باید SSR (رندر سمت سرور) را دریافت کنند و سپس جاوا اسکریپت درخواست شبکه دیگری را برای واکشی داده های واقعی راه اندازی می کند:در آزمایش ما، این منجر به کاهش زمان بارگذاری کل شد .چه می شود اگر بتوانیم این داده ها را در HTML قرار دهیم؟ این اجازه می دهد تا رندر سمت سرور و واکشی داده ها به صورت موازی اتفاق بیفتد:

با توجه به اینکه قبلاً صفحه را با Early Flush به دو قسمت تقسیم کرده بودیم، معرفی یک قطعه سوم برای آنچه ما Deferred Data می نامیم نسبتاً ساده است . این قسمت به دنبال همه محتوای قابل مشاهده می رود و رندر را مسدود نمی کند. ما درخواست های شبکه را روی سرور اجرا می کنیم و پاسخ ها را در قسمت Deferred Data قرار می دهیم. در پایان، سه تکه ما به این صورت است:

با اجرای این مورد روی سرور، تنها وظیفه باقی مانده نوشتن مقداری جاوا اسکریپت است تا تشخیص دهد که چه زمانی قطعه داده معوق ما می رسد. ما این کار را با MutationObserver انجام دادیم ، که روشی کارآمد برای مشاهده تغییرات DOM است. هنگامی که عنصر Deferred Data JSON شناسایی شد، نتیجه را تجزیه می کنیم و آن را به فروشگاه داده شبکه برنامه خود تزریق می کنیم. از منظر برنامه، گویی یک درخواست شبکه عادی تکمیل شده است.مراقب «به تعویق انداختن» باشیدممکن است متوجه شوید که برخی از برچسب ها از مثال Early Flush دوباره مرتب شده اند. تگ های اسکریپت از قسمت Early به قسمت Body منتقل شدند و دیگر ویژگی defer را ندارند . این ویژگی با به تعویق انداختن اسکریپت‌ها تا زمانی که HTML دانلود و تجزیه شده است، از اجرای اسکریپت مسدود کردن رندر جلوگیری می‌کند. در هنگام استفاده از داده‌های معوق، این مقدار کمتر از حد مطلوب است، زیرا تمام محتوای قابل مشاهده قبلاً در انتهای قسمت Body دریافت شده است، و دیگر نگران مسدود کردن رندر در آن نقطه نیستیم. می‌توانیم این مشکل را با انتقال تگ‌های اسکریپت به انتهای قسمت Body و حذف ویژگی defer برطرف کنیم. جابجایی تگ‌ها بعداً در سند یک آبشار شبکه را معرفی می‌کند که با اضافه کردن تگ‌های پیش‌بارگذاری به قسمت اولیه آن را حل کردیم.چالش های پیاده سازیکدهای وضعیت و هدرهاEarly Flush از تغییرات بعدی در هدرها جلوگیری می کند (مثلاً برای تغییر مسیر یا تغییر کد وضعیت). در دنیای React + NodeJS، واگذاری تغییر مسیرها و خطاها به برنامه React که پس از واکشی داده‌ها ارائه می‌شود، معمول است. <head>اگر قبلاً یک برچسب اولیه و وضعیت OK 200 ارسال کرده باشید، این کار کار نخواهد کرد .ما این مشکل را با انتقال خطا و تغییر مسیر منطق به خارج از برنامه React خود حل کردیم. این منطق اکنون در میان افزار سرور اکسپرس قبل از اینکه بخواهیم Early Flush کنیم انجام می شود.بافر کردنما متوجه شدیم که nginx بافر به طور پیش فرض پاسخ می دهد. این دارای مزایای استفاده از منابع است، اما زمانی که هدف ارسال پاسخ های افزایشی باشد، نتیجه معکوس دارد. ما مجبور شدیم این سرویس ها را برای غیرفعال کردن بافر کردن پیکربندی کنیم. ما انتظار داشتیم با این تغییر استفاده از منابع افزایش یابد، اما تأثیر آن ناچیز بود.تاخیر در پاسخگوییما متوجه شدیم که پاسخ‌های Flush اولیه ما تاخیر غیرمنتظره‌ای حدود 200 میلی‌ثانیه داشتند که با غیرفعال کردن فشرده‌سازی gzip ناپدید شد. معلوم شد که این یک تعامل بین الگوریتم Nagle و Delayed ACK است . این بهینه‌سازی‌ها تلاش می‌کنند تا داده‌های ارسال شده در هر بسته را به حداکثر برسانند و در هنگام ارسال مقادیر کم داده، تأخیر ایجاد می‌کنند. برخورد با این مشکل با فریم های جامبو بسیار آسان است ، که حداکثر اندازه بسته ها را افزایش می دهد. به نظر می رسد که gzip اندازه نوشته های ما را به حدی کاهش داده است که نمی توانند یک بسته را پر کنند و راه حل این است که الگوریتم Nagle را در متعادل کننده بار هاپروکسی غیرفعال کنیم .نتیجه اکنون HTTP Streaming یک استراتژی بسیار موفق برای بهبود عملکرد وب در Airbnb بوده است. آزمایش‌های ما نشان داد که Early Flush در هر صفحه آزمایش‌شده، از جمله صفحه اصلی Airbnb، کاهش یکنواختی در First Contentful Paint (FCP) حدود 100 میلی‌ثانیه ایجاد کرد. جریان داده هزینه های FCP مربوط به پرس و جوهای آهسته پشتیبان را بیشتر حذف کرد. در حالی که چالش‌هایی در این مسیر وجود داشت، ما متوجه شدیم که تطبیق برنامه React موجود ما برای پشتیبانی از جریان بسیار عملی و قوی است، علیرغم اینکه در ابتدا برای آن طراحی نشده بود. ما همچنین از دیدن روند گسترده‌تر اکوسیستم frontend در جهت اولویت‌بندی جریان، از @defer و @stream در GraphQL تا جریان SSR در Next.js هیجان‌زده هستیم.. خواه از این فناوری‌های جدید استفاده می‌کنید، یا یک پایگاه کد موجود را گسترش می‌دهید، امیدواریم جریان‌سازی را بررسی کنید تا جلوه‌ای سریع‌تر برای همه بسازید!


معرفی

شاید این جوک را شنیده باشید که اینترنت یک سری لوله است . در این پست وبلاگ، ما قصد داریم در مورد اینکه چگونه با استفاده از HTTP Streaming در سریعترین زمان ممکن، یک جریان جالب و با طراوت از بایت های Airbnb.com را به مرورگر شما وارد می کنیم.


بیایید ابتدا بفهمیم استریم یعنی چه. تصور کنید ما یک میله و دو گزینه داشتیم:

یک فنجان بزرگ را پر کنید، و سپس همه آن را در لوله بریزید (استراتژی "بافر")

اسپیگوت را مستقیماً به لوله وصل کنید (استراتژی "جریان")

در استراتژی بافر، همه چیز به صورت متوالی اتفاق می افتد - سرورهای ما ابتدا کل پاسخ را در یک بافر (پر کردن فنجان) تولید می کنند و سپس زمان بیشتری برای ارسال آن از طریق شبکه (ریختن آن) صرف می شود. استراتژی استریم به صورت موازی اتفاق می افتد. ما پاسخ را به قطعات تقسیم می کنیم که به محض آماده شدن ارسال می شود. سرور می تواند در حالی که تکه های قبلی هنوز در حال ارسال هستند، روی قسمت بعدی کار کند و مشتری (مثلاً یک مرورگر) می تواند قبل از دریافت کامل پاسخ، رسیدگی به آن را آغاز کند.


اجرای جریان در Airbnb

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


با این حال، یک مورد استفاده وجود دارد که به طور جهانی قابل اجرا است. ما می توانیم از جریان برای کاهش آبشارهای شبکه استفاده کنیم . این اصطلاح به زمانی اشاره دارد که یک درخواست شبکه دیگری را راه‌اندازی می‌کند و منجر به یک سری درخواست‌های متوالی می‌شود. این به راحتی در ابزاری مانند Chrome's Waterfall قابل مشاهده است :

آبشار شبکه کروم که آبشاری از درخواست‌های متوالی را نشان می‌دهد
آبشار شبکه کروم که آبشاری از درخواست‌های متوالی را نشان می‌دهد



بیشتر صفحات وب به فایل‌های جاوا اسکریپت و CSS خارجی که در داخل HTML پیوند داده شده‌اند متکی هستند، که منجر به آبشار شبکه می‌شود — دانلود HTML باعث دانلود JavaScript و CSS می‌شود. در نتیجه، قرار دادن تمام تگ های CSS و جاوا اسکریپت در نزدیکی ابتدای HTML در تگ بهترین روش است <head>. این تضمین می کند که مرورگر آنها را زودتر ببیند. با پخش جریانی، می‌توانیم با ارسال <head>ابتدا آن بخش از برچسب، این تاخیر را بیشتر کاهش دهیم.


فلاش زودرس

ساده ترین راه برای ارسال یک <head>برچسب اولیه، شکستن یک پاسخ استاندارد به دو بخش است. این تکنیک Early Flush نامیده می شود ، زیرا یک قسمت قبل از دیگری ارسال می شود ("flushed").


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


< html >
< head >
< اسکریپت src = … به تعویق انداختن />
< پیوند rel = ”stylesheet” href = … />
<!--تعداد زیادی <meta> و تگ های دیگر… ->


برچسب های <!-- <head> که به داده ها بستگی دارند به اینجا بروید ->
</head>
<body>
<! — محتوای متن اینجا →
</body>
</html>


ما مجبور شدیم برنامه خود را بازسازی کنیم تا اینمتن وجود داشته باشد. برای زمینه، Airbnb از یک سرور NodeJS مبتنی بر Express برای ارائه صفحات وب با استفاده از React استفاده می کند. ما قبلاً یک مؤلفه React داشتیم که وظیفه ارائه سند کامل HTML را بر عهده داشت. با این حال، این دو مشکل ایجاد کرد:


تولید تکه های افزایشی محتوا به این معنی است که باید با تگ های HTML جزئی/بازشده کار کنیم. به عنوان مثال، نمونه هایی که در بالا مشاهده کردید HTML نامعتبر هستند. تگ‌های <html>و <head>در قسمت Early باز می‌شوند، اما در قسمت Late بسته می‌شوند. هیچ راهی برای تولید این نوع خروجی با استفاده از توابع استاندارد رندر React وجود ندارد.

تا زمانی که همه داده‌های مربوط به آن را نداشته باشیم، نمی‌توانیم این مؤلفه را رندر کنیم.

ما این مشکلات را با شکستن جزء یکپارچه خود به سه حل کردیم:


یک مؤلفه "Early <head>".

یک مؤلفه "Late <head>" برای تگ های <head> که به داده ها بستگی دارد

یک جزء "<body>".

هر جزء محتویات برچسب سر یا بدن را ارائه می دهد. سپس آنها را با نوشتن تگ‌های باز/بستن مستقیماً در جریان پاسخ HTTP به هم می‌چسبانیم. به طور کلی، روند به صورت زیر است:


نوشتن<html><head>

رندر کنید و Early <head> را در پاسخ بنویسید

منتظر داده باشید

<head> را رندر کنید و روی پاسخ بنویسید

نوشتن</head><body>

رندر کنید و <body> را در پاسخ بنویسید

با نوشتن کار را تمام کنید</body></html>


جریان داده

Early Flush آبشارهای شبکه CSS و JavaScript را بهینه می کند. با این حال، کاربران همچنان به یک صفحه خالی خیره خواهند شد تا زمانی که <body>برچسب وارد شود. ما می‌خواهیم این حالت را با ارائه یک حالت بارگیری زمانی که داده‌ای وجود ندارد، بهبود دهیم، که پس از رسیدن داده‌ها جایگزین می‌شود. به‌راحتی، ما در حال حاضر وضعیت‌های بارگیری را در این وضعیت برای مسیریابی سمت مشتری داریم، بنابراین می‌توانیم این کار را فقط با رندر کردن برنامه بدون انتظار برای داده انجام دهیم!


متأسفانه، این باعث آبشار شبکه دیگری می شود. مرورگرها باید SSR (رندر سمت سرور) را دریافت کنند و سپس جاوا اسکریپت درخواست شبکه دیگری را برای واکشی داده های واقعی راه اندازی می کند:

نموداری که یک آبشار شبکه را نشان می‌دهد که در آن داده‌های SSR و سمت مشتری به‌طور متوالی واکشی می‌شوند
نموداری که یک آبشار شبکه را نشان می‌دهد که در آن داده‌های SSR و سمت مشتری به‌طور متوالی واکشی می‌شوند



در آزمایش ما، این منجر به کاهش زمان بارگذاری کل شد .


چه می شود اگر بتوانیم این داده ها را در HTML قرار دهیم؟ این اجازه می دهد تا رندر سمت سرور و واکشی داده ها به صورت موازی اتفاق بیفتد:

با توجه به اینکه قبلاً صفحه را با Early Flush به دو قسمت تقسیم کرده بودیم، معرفی یک قطعه سوم برای آنچه ما Deferred Data می نامیم نسبتاً ساده است . این قسمت به دنبال همه محتوای قابل مشاهده می رود و رندر را مسدود نمی کند. ما درخواست های شبکه را روی سرور اجرا می کنیم و پاسخ ها را در قسمت Deferred Data قرار می دهیم. در پایان، سه تکه ما به این صورت است:

با اجرای این مورد روی سرور، تنها وظیفه باقی مانده نوشتن مقداری جاوا اسکریپت است تا تشخیص دهد که چه زمانی قطعه داده معوق ما می رسد. ما این کار را با MutationObserver انجام دادیم ، که روشی کارآمد برای مشاهده تغییرات DOM است. هنگامی که عنصر Deferred Data JSON شناسایی شد، نتیجه را تجزیه می کنیم و آن را به فروشگاه داده شبکه برنامه خود تزریق می کنیم. از منظر برنامه، گویی یک درخواست شبکه عادی تکمیل شده است.


مراقب «به تعویق انداختن» باشید


ممکن است متوجه شوید که برخی از برچسب ها از مثال Early Flush دوباره مرتب شده اند. تگ های اسکریپت از قسمت Early به قسمت Body منتقل شدند و دیگر ویژگی defer را ندارند . این ویژگی با به تعویق انداختن اسکریپت‌ها تا زمانی که HTML دانلود و تجزیه شده است، از اجرای اسکریپت مسدود کردن رندر جلوگیری می‌کند. در هنگام استفاده از داده‌های معوق، این مقدار کمتر از حد مطلوب است، زیرا تمام محتوای قابل مشاهده قبلاً در انتهای قسمت Body دریافت شده است، و دیگر نگران مسدود کردن رندر در آن نقطه نیستیم. می‌توانیم این مشکل را با انتقال تگ‌های اسکریپت به انتهای قسمت Body و حذف ویژگی defer برطرف کنیم. جابجایی تگ‌ها بعداً در سند یک آبشار شبکه را معرفی می‌کند که با اضافه کردن تگ‌های پیش‌بارگذاری به قسمت اولیه آن را حل کردیم.


چالش های پیاده سازی

کدهای وضعیت و هدرها

Early Flush از تغییرات بعدی در هدرها جلوگیری می کند (مثلاً برای تغییر مسیر یا تغییر کد وضعیت). در دنیای React + NodeJS، واگذاری تغییر مسیرها و خطاها به برنامه React که پس از واکشی داده‌ها ارائه می‌شود، معمول است. <head>اگر قبلاً یک برچسب اولیه و وضعیت OK 200 ارسال کرده باشید، این کار کار نخواهد کرد .


ما این مشکل را با انتقال خطا و تغییر مسیر منطق به خارج از برنامه React خود حل کردیم. این منطق اکنون در میان افزار سرور اکسپرس قبل از اینکه بخواهیم Early Flush کنیم انجام می شود.


بافر کردن

ما متوجه شدیم که nginx بافر به طور پیش فرض پاسخ می دهد. این دارای مزایای استفاده از منابع است، اما زمانی که هدف ارسال پاسخ های افزایشی باشد، نتیجه معکوس دارد. ما مجبور شدیم این سرویس ها را برای غیرفعال کردن بافر کردن پیکربندی کنیم. ما انتظار داشتیم با این تغییر استفاده از منابع افزایش یابد، اما تأثیر آن ناچیز بود.


تاخیر در پاسخگویی

ما متوجه شدیم که پاسخ‌های Flush اولیه ما تاخیر غیرمنتظره‌ای حدود 200 میلی‌ثانیه داشتند که با غیرفعال کردن فشرده‌سازی gzip ناپدید شد. معلوم شد که این یک تعامل بین الگوریتم Nagle و Delayed ACK است . این بهینه‌سازی‌ها تلاش می‌کنند تا داده‌های ارسال شده در هر بسته را به حداکثر برسانند و در هنگام ارسال مقادیر کم داده، تأخیر ایجاد می‌کنند. برخورد با این مشکل با فریم های جامبو بسیار آسان است ، که حداکثر اندازه بسته ها را افزایش می دهد. به نظر می رسد که gzip اندازه نوشته های ما را به حدی کاهش داده است که نمی توانند یک بسته را پر کنند و راه حل این است که الگوریتم Nagle را در متعادل کننده بار هاپروکسی غیرفعال کنیم .


نتیجه

اکنون HTTP Streaming یک استراتژی بسیار موفق برای بهبود عملکرد وب در Airbnb بوده است. آزمایش‌های ما نشان داد که Early Flush در هر صفحه آزمایش‌شده، از جمله صفحه اصلی Airbnb، کاهش یکنواختی در First Contentful Paint (FCP) حدود 100 میلی‌ثانیه ایجاد کرد. جریان داده هزینه های FCP مربوط به پرس و جوهای آهسته پشتیبان را بیشتر حذف کرد. در حالی که چالش‌هایی در این مسیر وجود داشت، ما متوجه شدیم که تطبیق برنامه React موجود ما برای پشتیبانی از جریان بسیار عملی و قوی است، علیرغم اینکه در ابتدا برای آن طراحی نشده بود. ما همچنین از دیدن روند گسترده‌تر اکوسیستم frontend در جهت اولویت‌بندی جریان، از @defer و @stream در GraphQL تا جریان SSR در Next.js هیجان‌زده هستیم.. خواه از این فناوری‌های جدید استفاده می‌کنید، یا یک پایگاه کد موجود را گسترش می‌دهید، امیدواریم جریان‌سازی را بررسی کنید تا جلوه‌ای سریع‌تر برای همه بسازید!

بهبود عملکردجاوا اسکریپتstream
امیدوارم به بهتر شدن کمک کنم. در تلگرام و اینستاگرام پیام بفرست، SeyedAhmaddv - ارشد نرم افزار، توسعه دهنده ری اکت و نکست
شاید از این پست‌ها خوشتان بیاید