معماری غیررشته اصلی میتواند قابلیت اطمینان و تجربه کاربری برنامهتان را بهطور قابلتوجهی بهبود بخشد.
در ۲۰ سال گذشته، وب بهطور چشمگیری از اسناد ثابت با چند سبک و تصویر به برنامههای پیچیده و پویا تبدیل شده است. بااینحال، یک چیز تا حد زیادی بدون تغییر باقی مانده است: ما فقط یک رشته برای هر برگه مرورگر داریم (با چند استثنا) تا کار رندر کردن سایتهایمان و اجرای جاوا اسکریپت را انجام دهیم.
درنتیجه، رشته اصلی بهشدت پرکار شده است. و با پیچیدهتر شدن برنامههای وب، رشته اصلی به گلوگاه مهمی برای عملکرد تبدیل میشود. بدتر از همه اینکه مدتزمان لازم برای اجرای کد در رشته اصلی برای کاربر موردنظر تقریباً کاملاً غیرقابلپیشبینی است زیرا قابلیتهای دستگاه تأثیر بسیار زیادی بر عملکرد دارد. این غیرقابلپیشبینی بودن با دسترسی کاربران به وب ازطریق مجموعهای متنوع از دستگاهها، از تلفنهای همراه نیمههوشمند با محدودیتهای شدید گرفته تا دستگاههای پرچمدار قدرتمند با نرخ بازآوری بالا، بیشتر خواهد شد.
اگر میخواهیم برنامههای وب پیچیده بهطور قابلاعتمادی از دستورالعملهای عملکردی مانند Core Web Vitals پیروی کنند—که براساس دادههای تجربی درباره ادراک و روانشناسی انسان است—باید راههایی برای اجرای کد خود خارج از رشته اصلی (OMT) داشته باشیم.
چرا وبورکر؟
جاوا اسکریپت بهطور پیشفرض زبانی تکرشتهای است که تکالیف را در رشته اصلی اجرا میکند. بااینحال، وبورکرها نوعی راه فرار از رشته اصلی ارائه میدهند و به توسعهدهندگان اجازه میدهند رشتههای جداگانهای برای مدیریت کارها خارج از رشته اصلی ایجاد کنند. اگرچه دامنه وبورکرها محدود است و دسترسی مستقیم به DOM را ارائه نمیدهد، اما اگر کار قابلتوجهی وجود داشته باشد که باید انجام شود و درغیراینصورت رشته اصلی را تحتالشعاع قرار دهد، میتوانند بسیار مفید باشند.
وقتی معیارهای کلیدی اصلی وب مطرح است، اجرای کار خارج از رشته اصلی میتواند مفید باشد. بهویژه، انتقال کار از رشته اصلی به وبورکرها میتواند رقابت برای رشته اصلی را کاهش دهد، که میتواند سنجه پاسخدهی Interaction to Next Paint (INP) صفحه را بهبود بخشد. وقتی رشته اصلی کار کمتری برای پردازش داشته باشد، میتواند سریعتر به تعاملهای کاربر پاسخ دهد.
کار کمتر رشته اصلی (بهویژه درطول راهاندازی) با کاهش کارهای طولانی، مزیت بالقوهای نیز برای بزرگترین نقاشی دارای محتوا (LCP) دارد. پرداز عنصر LCP به زمان رشته اصلی نیاز دارد—چه برای پرداز نوشتار یا تصاویر که عناصر LCP رایج و متداول هستند—و با کاهش کلی کار رشته اصلی، میتوانید مطمئن شوید که احتمال مسدود شدن عنصر LCP صفحهتان توسط کارهای پرهزینهای که یک وبورکر میتواند بهجای آن انجام دهد کمتر است.
رشتهسازی با وبورکرها
پلاتفرمهای دیگر معمولاً با اجازه دادن به شما برای اختصاص دادن یک تابع به رشته، از کار موازی پشتیبانی میکنند که این تابع بهطور موازی با بقیه برنامه شما اجرا میشود. میتوانید از هر دو رشته به متغیرهای یکسانی دسترسی داشته باشید و دسترسی به این منابع مشترک را میتوان با mutex و semaphore همگامسازی کرد تا از شرایط رقابتی جلوگیری شود.
در جاوا اسکریپت، میتوانیم عملکرد تقریباً مشابهی را از وبورکرها دریافت کنیم که از سال ۲۰۰۷ وجود داشتهاند و از سال ۲۰۱۲ در همه مرورگرهای اصلی پشتیبانی میشوند. کارگران وب بهموازات رشته اصلی اجرا میشوند، اما برخلاف رشتهبندی سیستمعامل، نمیتوانند متغیرها را همرسانی کنند.
برای ایجاد یک وبورکر، فایلی را به سازنده ورکر ارسال کنید که اجرای آن فایل را در یک رشته جداگانه آغاز میکند:
const worker = new Worker("./worker.js");
با ارسال پیام بااستفاده از postMessage API، با وبورکر ارتباط برقرار کنید. مقدار پیام را بهعنوان پارامتر در فراخوانی postMessage ارسال کنید و سپس شنونده رویداد پیام را به کارگر اضافه کنید:
main.js
const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);
worker.js
addEventListener('message', event => {
const [a, b] = event.data;
// Do stuff with the message
// ...
});
برای ارسال پیام به رشته اصلی، از همان postMessage API در وبورکر استفاده کنید و یک شنونده رویداد در رشته اصلی راهاندازی کنید:
main.js
const worker = new Worker('./worker.js');
worker.postMessage([40, 2]);
worker.addEventListener('message', event => {
console.log(event.data);
});
worker.js
addEventListener('message', event => {
const [a, b] = event.data;
// Do stuff with the message
postMessage(a + b);
});
باید اعتراف کرد که این رویکرد تاحدی محدود است. در گذشته، وبورکرها عمدتاً برای انتقال یک کار سنگین از رشته اصلی استفاده میشدند. تلاش برای مدیریت چندین عملیات با یک وبورکر واحد بهسرعت غیرقابلکنترل میشود: شما باید نه تنها پارامترها بلکه عملیات را نیز در پیام کدگذاری کنید و باید برای مطابقت دادن پاسخها با درخواستها، دفترداری انجام دهید. این پیچیدگی احتمالاً دلیل این است که وبورکرها بهطور گستردهتر پذیرفته نشدهاند.
اما اگر بتوانیم برخیاز دشواریهای ارتباط بین رشته اصلی و وبورکرها را برطرف کنیم، این مدل میتواند برای بسیاری از موارد استفاده بسیار مناسب باشد. و خوشبختانه کتابخانهای وجود دارد که دقیقاً همین کار را میکند!
Comlink: Making web workers less work
Comlink کتابخانهای است که هدف آن این است که به شما اجازه دهد از وبورکرها استفاده کنید بدون اینکه لازم باشد به جزئیات postMessage فکر کنید. Comlink به شما امکان میدهد متغیرها را بین وبورکرها و رشته اصلی تقریباً مانند سایر زبانهای برنامهنویسی که از رشتهبندی پشتیبانی میکنند همرسانی کنید.
Comlink را با وارد کردن آن در یک وبورکر و تعریف مجموعهای از توابع برای نمایش در رشته اصلی راهاندازی میکنید. سپس Comlink را در رشته اصلی وارد میکنید، کارگر را میپیچید و به توابع در معرض دید دسترسی پیدا میکنید:
worker.js
import {expose} from 'comlink';
const api = {
someMethod() {
// ...
}
}
expose(api);
main.js
import {wrap} from 'comlink';
const worker = new Worker('./worker.js');
const api = wrap(worker);
متغیر api در رشته اصلی مانند متغیر موجود در وبورکر رفتار میکند، با این تفاوت که هر تابع بهجای خود مقدار، یک وعده برای مقدار برمیگرداند.
کدام کد را باید به وبورکر منتقل کنید؟
کارگران وب به DOM و بسیاری از میاناهای برنامهسازی کاربردی مانند WebUSB، WebRTC، یا Web Audio دسترسی ندارند، بنابراین نمیتوانید بخشهایی از برنامهتان را که به چنین دسترسیای متکی هستند در کارگر قرار دهید. بااینحال، هر قطعه کوچک کد که به یک worker منتقل میشود، فضای بیشتری را در رشته اصلی برای چیزهایی که باید آنجا باشند (مانند بهروزرسانی میانای کاربر) فراهم میکند.
یکی از مشکلات توسعهدهندگان وب این است که اکثر برنامههای وب برای هماهنگ کردن همه چیز در برنامه به چارچوب واسط کاربر مانند Vue یا React متکی هستند؛ همه چیز جزئی از چارچوب است و بنابراین ذاتاً به DOM گره خورده است. بهنظر میرسد این موضوع انتقال به معماری OMT را دشوار میکند.
بااینحال، اگر به مدلی تغییر دهیم که در آن نگرانیهای مربوط به واسط کاربر از نگرانیهای دیگر، مانند مدیریت وضعیت، جدا شده باشد، وبورکرها میتوانند حتی با برنامههای مبتنی بر چارچوب بسیار مفید باشند. این دقیقاً همان رویکردی است که در PROXX اتخاذ شده است.
PROXX: مطالعه موردی OMT
تیم Google Chrome بازی PROXX را بهعنوان کلونی از «مینروب» توسعه داد که الزامات برنامه وب پیشرو را برآورده میکند، ازجمله کار کردن بهصورت آفلاین و داشتن تجربه کاربری جذاب. متأسفانه نسخههای اولیه بازی در دستگاههای محدود مانند تلفنهای همراه نیمههوشمند عملکرد ضعیفی داشتند، که باعث شد تیم متوجه شود رشته اصلی یک گلوگاه است.
تیم تصمیم گرفت از وبورکرها برای جدا کردن وضعیت بصری بازی از منطق آن استفاده کند:
- رشته اصلی پردازش پویانماییها و گذارها را انجام میدهد.
- یک وبورکر منطق بازی را که کاملاً محاسباتی است مدیریت میکند.
OMT تأثیرات جالبی بر عملکرد تلفن همراه نیمههوشمند PROXX داشت. در نسخه غیر OMT، واسط کاربر بهمدت شش ثانیه پساز تعامل کاربر با آن ثابت میماند. هیچ بازخوردی وجود ندارد و کاربر باید شش ثانیه کامل صبر کند تا بتواند کار دیگری انجام دهد.
اما در نسخه OMT، بازی دوازده ثانیه طول میکشد تا بهروزرسانی واسط کاربر را تکمیل کند. اگرچه این امر بهنظر میرسد که باعث کاهش عملکرد میشود، اما درواقع منجر به افزایش بازخورد به کاربر میشود. این کندی به این دلیل رخ میدهد که برنامه قابهای بیشتری نسبتبه نسخه غیر OMT ارسال میکند، که اصلاً هیچ قابی ارسال نمیکند. بنابراین کاربر میداند که اتفاقی درحال رخ دادن است و میتواند به بازی کردن ادامه دهد تا واسط کاربر بهروز شود، که این امر باعث میشود بازی بهطور قابلتوجهی بهتر بهنظر برسد.
این یک معاوضه آگاهانه است: ما به کاربران دستگاههای محدود تجربهای میدهیم که احساس بهتری دارد، بدون اینکه کاربران دستگاههای پیشرفته را جریمه کنیم.
پیامدهای معماری OMT
همانطور که مثال PROXX نشان میدهد، OMT باعث میشود برنامهتان بهطور قابلاعتمادی در طیف وسیعتری از دستگاهها اجرا شود، اما برنامهتان را سریعتر نمیکند:
- شما فقط کار را از رشته اصلی منتقل میکنید، نه اینکه کار را کاهش دهید.
- سربار ارتباطی اضافی بین وبورکر و رشته اصلی گاهی اوقات میتواند کارها را کمی کندتر کند.
معاوضهها را درنظر بگیرید
ازآنجاییکه رشته اصلی برای پردازش تعاملات کاربر مانند پیمایش درحین اجرای جاوا اسکریپت آزاد است، فریمهای ازدسترفته کمتری وجود دارد، حتی اگر کل زمان انتظار کمی طولانیتر باشد. کمی منتظر گذاشتن کاربر بهتر از حذف فریم است، زیرا حاشیه خطا برای فریمهای حذفشده کوچکتر است: حذف فریم در چند میلیثانیه اتفاق میافتد، درحالیکه صدها میلیثانیه فرصت دارید تا کاربر متوجه زمان انتظار شود.
بهدلیل غیرقابلپیشبینی بودن عملکرد در دستگاههای مختلف، هدف معماری OMT واقعاً کاهش ریسک است—یعنی مقاومتر کردن برنامه شما دربرابر شرایط زمان اجرای بسیار متغیر—نه مزایای عملکردی موازیسازی. افزایش مقاومت و بهبود تجربه کاربری ارزش این را دارد که کمی سرعت را فدا کنیم.
نکتهای درباره ابزارها
کارگران وب هنوز رایج نیستند، بنابراین اکثر ابزارهای ماژول - مانند webpack و Rollup - از آنها بهطور پیشفرض پشتیبانی نمیکنند. (اما Parcel این کار را میکند!) خوشبختانه، افزایههایی وجود دارد که باعث میشود وبورکرها با webpack و Rollup کار کنند:
- worker-plugin برای webpack
- rollup-plugin-off-main-thread برای Rollup
درحال جمعبندی کردن
برای اطمینان از اینکه برنامههای ما تا حد امکان قابلاعتماد و دردسترس هستند، بهویژه در بازاری که بهطور فزایندهای جهانی میشود، باید از دستگاههای محدود پشتیبانی کنیم—این دستگاهها نحوه دسترسی اکثر کاربران به وب در سطح جهانی هستند. OMT روشی امیدوارکننده برای افزایش عملکرد در چنین دستگاههایی ارائه میدهد، بدون اینکه تأثیر منفی بر کاربران دستگاههای پیشرفته داشته باشد.
همچنین، OMT مزایای ثانویه دارد:
- این کار هزینههای اجرای جاوا اسکریپت را به رشتهای جداگانه منتقل میکند.
- این کار هزینههای تجزیه را جابهجا میکند، یعنی ممکن است واسط کاربر سریعتر راهاندازی شود. این کار ممکن است اولین رنگآمیزی محتوایی یا حتی زمان تعاملی را کاهش دهد، که بهنوبه خود میتواند امتیاز Lighthouse شما را افزایش دهد.
وبورکرها نباید ترسناک باشند. ابزارهایی مانند Comlink کار را از دوش کارگران برمیدارند و آنها را به گزینهای مناسب برای طیف گستردهای از برنامههای وب تبدیل میکنند.