یک معماری خارج از نخ اصلی میتواند به طور قابل توجهی قابلیت اطمینان و تجربه کاربری برنامه شما را بهبود بخشد.
در ۲۰ سال گذشته، وب به طرز چشمگیری از اسناد ایستا با چند سبک و تصویر به برنامههای پیچیده و پویا تکامل یافته است. با این حال، یک چیز تا حد زیادی بدون تغییر باقی مانده است: ما فقط یک رشته در هر برگه مرورگر (با برخی استثنائات) برای انجام کار رندر سایتهایمان و اجرای جاوا اسکریپت داریم.
در نتیجه، رشته اصلی به طرز باورنکردنی بیش از حد بارگذاری شده است. و با افزایش پیچیدگی برنامههای وب، رشته اصلی به یک گلوگاه قابل توجه برای عملکرد تبدیل میشود. بدتر از همه، مدت زمانی که برای اجرای کد روی رشته اصلی برای یک کاربر خاص لازم است تقریباً کاملاً غیرقابل پیشبینی است زیرا قابلیتهای دستگاه تأثیر زیادی بر عملکرد دارد. این غیرقابل پیشبینی بودن تنها با دسترسی کاربران به وب از طریق مجموعهای از دستگاههای متنوع، از تلفنهای دارای محدودیت بیش از حد گرفته تا دستگاههای پرچمدار با قدرت بالا و نرخ تازهسازی بالا، افزایش خواهد یافت.
اگر میخواهیم برنامههای وب پیچیده به طور قابل اعتمادی دستورالعملهای عملکردی مانند Core Web Vitals - که مبتنی بر دادههای تجربی در مورد ادراک و روانشناسی انسان است - را رعایت کنند، به روشهایی برای اجرای کد خود خارج از نخ اصلی (OMT) نیاز داریم.
چرا وب ورکر؟
جاوا اسکریپت، به طور پیشفرض، یک زبان تکرشتهای است که وظایف را روی نخ اصلی اجرا میکند. با این حال، وب ورکرها نوعی راه فرار از نخ اصلی را فراهم میکنند و به توسعهدهندگان اجازه میدهند نخهای جداگانهای برای مدیریت کارها خارج از نخ اصلی ایجاد کنند. اگرچه دامنه وب ورکرها محدود است و دسترسی مستقیم به DOM را ارائه نمیدهد، اما اگر کار قابل توجهی وجود داشته باشد که باید انجام شود و در غیر این صورت نخ اصلی را تحت الشعاع قرار میدهد، میتوانند بسیار مفید باشند.
در مورد Core Web Vitals ، اجرای کار از نخ اصلی میتواند مفید باشد. به طور خاص، انتقال کار از نخ اصلی به وب ورکر میتواند رقابت برای نخ اصلی را کاهش دهد، که میتواند معیار پاسخگویی تعامل تا رنگ بعدی (INP) یک صفحه را بهبود بخشد. وقتی نخ اصلی کار کمتری برای پردازش داشته باشد، میتواند سریعتر به تعاملات کاربر پاسخ دهد.
کار کمتر نخ اصلی - به خصوص در هنگام راهاندازی - همچنین با کاهش وظایف طولانی، یک مزیت بالقوه برای Largest Contentful Paint (LCP) به همراه دارد. رندر کردن یک عنصر LCP به زمان نخ اصلی نیاز دارد - چه برای رندر کردن متن یا تصاویر، که عناصر LCP مکرر و رایج هستند - و با کاهش کلی کار نخ اصلی، میتوانید اطمینان حاصل کنید که عنصر LCP صفحه شما کمتر احتمال دارد توسط کار پرهزینهای که یک وب ورکر میتواند به جای آن انجام دهد، مسدود شود.
نخکشی با کارگران وب
سایر پلتفرمها معمولاً از کار موازی پشتیبانی میکنند و به شما این امکان را میدهند که به یک نخ، تابعی بدهید که به صورت موازی با بقیه برنامه شما اجرا شود. میتوانید از هر دو نخ به متغیرهای یکسان دسترسی داشته باشید و دسترسی به این منابع مشترک را میتوان با mutexes و semaphores برای جلوگیری از شرایط رقابتی هماهنگ کرد.
در جاوا اسکریپت، میتوانیم تقریباً عملکرد مشابهی را از وب ورکرها دریافت کنیم که از سال ۲۰۰۷ وجود داشتهاند و از سال ۲۰۱۲ در تمام مرورگرهای اصلی پشتیبانی میشوند. وب ورکرها به صورت موازی با نخ اصلی اجرا میشوند، اما برخلاف نخبندی سیستمعامل، نمیتوانند متغیرها را به اشتراک بگذارند.
برای ایجاد یک ورکر وب، یک فایل را به سازنده ورکر ارسال کنید، که اجرای آن فایل را در یک نخ جداگانه شروع میکند:
const worker = new Worker("./worker.js");
با ارسال پیام با استفاده از API postMessage با وب ورکر ارتباط برقرار کنید. مقدار پیام را به عنوان پارامتر در فراخوانی 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
// ...
});
برای ارسال پیام به نخ اصلی، از همان API postMessage در web worker استفاده کنید و یک شنونده رویداد (event listener) در نخ اصلی تنظیم کنید:
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);
});
باید اعتراف کرد که این رویکرد تا حدودی محدود است. از نظر تاریخی، وب ورکرها عمدتاً برای انتقال یک بخش از کار سنگین از نخ اصلی استفاده میشدند. تلاش برای مدیریت چندین عملیات با یک وب ورکر به سرعت دشوار میشود: شما باید نه تنها پارامترها، بلکه عملیات موجود در پیام را نیز رمزگذاری کنید و برای تطبیق پاسخها با درخواستها، باید حسابداری انجام دهید. احتمالاً همین پیچیدگی دلیل عدم پذیرش گستردهتر وب ورکرهاست.
اما اگر بتوانیم بخشی از دشواری برقراری ارتباط بین نخ اصلی و کارگران وب را از بین ببریم، این مدل میتواند برای بسیاری از موارد استفاده بسیار مناسب باشد. و خوشبختانه، کتابخانهای وجود دارد که دقیقاً همین کار را انجام میدهد!
کاملینک: کاهش کار کارمندان وب
کاملینک کتابخانهای است که هدف آن این است که به شما امکان دهد بدون نیاز به فکر کردن به جزئیات postMessage از وبورکرها استفاده کنید. کاملینک به شما امکان میدهد متغیرها را بین وبورکرها و نخ اصلی تقریباً مانند سایر زبانهای برنامهنویسی که از نخبندی پشتیبانی میکنند، به اشتراک بگذارید.
شما Comlink را با وارد کردن آن در یک web worker و تعریف مجموعهای از توابع برای نمایش در thread اصلی، راهاندازی میکنید. سپس Comlink را در thread اصلی وارد میکنید، worker را پوشش میدهید و به توابع نمایش داده شده دسترسی پیدا میکنید:
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 در نخ اصلی مانند متغیر موجود در وب ورکر رفتار میکند، با این تفاوت که هر تابع به جای خود مقدار، یک promise برای آن مقدار برمیگرداند.
چه کدی را باید به یک وب ورکر منتقل کنید؟
وب ورکرها به DOM و بسیاری از APIها مانند WebUSB ، WebRTC یا Web Audio دسترسی ندارند، بنابراین نمیتوانید بخشهایی از برنامه خود را که به چنین دسترسیهایی نیاز دارند، در یک ورکر قرار دهید. با این حال، هر قطعه کد کوچکی که به یک ورکر منتقل میشود، فضای بیشتری را در ترد اصلی برای مواردی که باید آنجا باشند - مانند بهروزرسانی رابط کاربری - ایجاد میکند.
یکی از مشکلات توسعهدهندگان وب این است که اکثر برنامههای وب برای هماهنگ کردن همه چیز در برنامه به یک چارچوب رابط کاربری مانند Vue یا React متکی هستند؛ همه چیز جزئی از چارچوب است و بنابراین ذاتاً به DOM گره خورده است. به نظر میرسد که این امر مهاجرت به معماری OMT را دشوار میکند.
با این حال، اگر به مدلی روی آوریم که در آن دغدغههای رابط کاربری از سایر دغدغهها، مانند مدیریت وضعیت، جدا شوند، وب ورکرها حتی با برنامههای مبتنی بر فریمورک نیز میتوانند بسیار مفید باشند. این دقیقاً همان رویکردی است که با PROXX اتخاذ شده است.
PROXX: مطالعه موردی OMT
تیم گوگل کروم ، PROXX را به عنوان یک کپی از Minesweeper توسعه داد که الزامات برنامه وب پیشرفته ، از جمله کار به صورت آفلاین و داشتن یک تجربه کاربری جذاب را برآورده میکند. متأسفانه، نسخههای اولیه بازی در دستگاههای محدود مانند تلفنهای همراه معمولی عملکرد ضعیفی داشتند، که باعث شد تیم متوجه شود که رشته اصلی یک گلوگاه است.
تیم تصمیم گرفت از web workerها برای جدا کردن حالت بصری بازی از منطق آن استفاده کند:
- رشته اصلی (main thread) رندر انیمیشنها و انتقالها (transitions) را مدیریت میکند.
- یک وب ورکر منطق بازی را مدیریت میکند که کاملاً محاسباتی است.
OMT تأثیرات جالبی بر عملکرد گوشی ساده PROXX داشت. در نسخه بدون OMT، رابط کاربری پس از تعامل کاربر با آن، به مدت شش ثانیه متوقف میشود. هیچ بازخوردی وجود ندارد و کاربر باید شش ثانیه کامل صبر کند تا بتواند کار دیگری انجام دهد.
با این حال، در نسخه OMT، بازی دوازده ثانیه طول میکشد تا بهروزرسانی رابط کاربری را تکمیل کند. اگرچه این به نظر میرسد که افت عملکرد داشته باشد، اما در واقع منجر به افزایش بازخورد به کاربر میشود. این کاهش سرعت به این دلیل رخ میدهد که برنامه فریمهای بیشتری نسبت به نسخه غیر OMT ارسال میکند، که اصلاً فریمی ارسال نمیکند. بنابراین کاربر میداند که اتفاقی در حال رخ دادن است و میتواند با بهروزرسانی رابط کاربری به بازی ادامه دهد و این باعث میشود بازی به طور قابل توجهی بهتر به نظر برسد.
این یک بدهبستان آگاهانه است: ما به کاربران دستگاههای محدود، تجربهای ارائه میدهیم که حس بهتری دارد، بدون اینکه کاربران دستگاههای پیشرفته را جریمه کنیم.
پیامدهای معماری OMT
همانطور که مثال PROXX نشان میدهد، OMT باعث میشود برنامه شما به طور قابل اعتمادی روی طیف وسیعتری از دستگاهها اجرا شود، اما برنامه شما را سریعتر نمیکند:
- شما فقط دارید کار را از نخ اصلی منتقل میکنید، نه اینکه حجم کار را کم کنید.
- سربار ارتباطی اضافی بین وب ورکر و نخ اصلی گاهی اوقات میتواند سرعت را کمی کاهش دهد.
بدهبستانها را در نظر بگیرید
از آنجایی که نخ اصلی (main thread) آزاد است تا تعاملات کاربر مانند پیمایش (scrolling) را در حین اجرای جاوا اسکریپت پردازش کند، فریمهای از دست رفته کمتری وجود دارد، اگرچه زمان انتظار کل ممکن است کمی طولانیتر باشد. کمی منتظر گذاشتن کاربر نسبت به از دست دادن یک فریم ارجحیت دارد زیرا حاشیه خطا برای فریمهای از دست رفته کمتر است: از دست دادن یک فریم در میلیثانیه اتفاق میافتد، در حالی که شما صدها میلیثانیه فرصت دارید تا کاربر زمان انتظار را درک کند.
به دلیل غیرقابل پیشبینی بودن عملکرد در بین دستگاهها، هدف معماری OMT در واقع کاهش ریسک است - مقاومتر کردن برنامه شما در مواجهه با شرایط زمان اجرای بسیار متغیر - نه در مورد مزایای عملکرد موازیسازی. افزایش انعطافپذیری و بهبود تجربه کاربری، ارزش هر گونه مصالحه کوچکی در سرعت را دارد.
نکتهای در مورد ابزارسازی
وب ورکرها هنوز به جریان اصلی تبدیل نشدهاند، بنابراین اکثر ابزارهای ماژول - مانند وبپک و رولآپ - از ابتدا از آنها پشتیبانی نمیکنند. (البته پارسل این کار را میکند!) خوشبختانه، افزونههایی وجود دارد که وب ورکرها را قادر میسازد با وبپک و رولآپ کار کنند :
- افزونه کارگر برای وبپک
- افزونه rollup-off-main-thread برای Rollup
جمعبندی
برای اطمینان از اینکه برنامههای ما تا حد امکان قابل اعتماد و در دسترس هستند، به خصوص در یک بازار جهانی که به طور فزایندهای در حال جهانی شدن است، باید از دستگاههای محدود پشتیبانی کنیم - آنها نحوه دسترسی اکثر کاربران به وب در سطح جهان هستند. OMT روشی امیدوارکننده برای افزایش عملکرد در چنین دستگاههایی بدون تأثیر منفی بر کاربران دستگاههای پیشرفته ارائه میدهد.
همچنین، OMT مزایای ثانویهای نیز دارد:
- هزینههای اجرای جاوا اسکریپت را به یک نخ جداگانه منتقل میکند.
- این کار هزینههای تجزیه و تحلیل را تغییر میدهد، به این معنی که رابط کاربری ممکن است سریعتر بوت شود. این ممکن است First Contentful Paint یا حتی Time to Interactive را کاهش دهد، که به نوبه خود میتواند امتیاز Lighthouse شما را افزایش دهد.
لازم نیست وب ورکرها ترسناک باشند. ابزارهایی مانند Comlink کار را از دوش ورکرها برمیدارند و آنها را به انتخابی مناسب برای طیف وسیعی از برنامههای وب تبدیل میکنند.