สถาปัตยกรรมแบบ Off-Main-Thread สามารถปรับปรุงความน่าเชื่อถือและประสบการณ์ของผู้ใช้แอปได้อย่างมาก
ในช่วง 20 ปีที่ผ่านมา เว็บได้พัฒนาไปอย่างมากจากเอกสารแบบคงที่ซึ่งมีสไตล์และรูปภาพเพียงไม่กี่รายการไปเป็นแอปพลิเคชันแบบไดนามิกที่ซับซ้อน อย่างไรก็ตาม มีสิ่งหนึ่งที่ยังคงไม่เปลี่ยนแปลงไปมากนัก นั่นคือเรามีเพียงเทรดเดียวต่อแท็บเบราว์เซอร์ (โดยมีข้อยกเว้นบางประการ) เพื่อทำงานแสดงผลเว็บไซต์และเรียกใช้ JavaScript
ด้วยเหตุนี้ เทรดหลักจึงทำงานหนักเกินไปอย่างมาก และเมื่อเว็บแอปมีความซับซ้อนมากขึ้น เทรดหลักจะกลายเป็นจุดคอขวดที่สำคัญต่อประสิทธิภาพ ที่แย่ไปกว่านั้นคือระยะเวลาที่ใช้ในการเรียกใช้โค้ดในชุดข้อความหลักสำหรับผู้ใช้หนึ่งๆ นั้นแทบจะคาดเดาไม่ได้เลย เนื่องจากความสามารถของอุปกรณ์มีผลอย่างมากต่อประสิทธิภาพ ความไม่แน่นอนดังกล่าวจะเพิ่มขึ้นเมื่อผู้ใช้เข้าถึงเว็บจากอุปกรณ์ที่หลากหลายมากขึ้น ตั้งแต่ฟีเจอร์โฟนที่มีข้อจำกัดสูงไปจนถึงอุปกรณ์เรือธงที่มีประสิทธิภาพสูงและอัตราการรีเฟรชสูง
หากต้องการให้เว็บแอปที่ซับซ้อนเป็นไปตามหลักเกณฑ์ด้านประสิทธิภาพอย่างน่าเชื่อถือ เช่น Core Web Vitals ซึ่งอิงตามข้อมูลเชิงประจักษ์เกี่ยวกับการรับรู้และจิตวิทยาของมนุษย์ เราจำเป็นต้องมีวิธีเรียกใช้โค้ดนอกเทรดหลัก (OMT)
ทำไมจึงควรใช้ Web Worker
โดยค่าเริ่มต้น JavaScript เป็นภาษาแบบเทรดเดียวที่เรียกใช้งานในเทรดหลัก อย่างไรก็ตาม Web Worker มีวิธีหลีกเลี่ยงเทรดหลักโดยอนุญาตให้นักพัฒนาซอฟต์แวร์สร้างเทรดแยกต่างหากเพื่อจัดการงานนอกเทรดหลัก แม้ว่าขอบเขตของ Web Worker จะจำกัดและไม่ได้ให้สิทธิ์เข้าถึง DOM โดยตรง แต่ก็มีประโยชน์อย่างมากหากมีงานจำนวนมากที่ต้องทำซึ่งอาจทำให้เทรดหลักทำงานหนักเกินไป
ในกรณีที่เกี่ยวข้องกับ Core Web Vitals การเรียกใช้โค้ดนอกเทรดหลักอาจเป็นประโยชน์ โดยเฉพาะอย่างยิ่ง การถ่ายโอนงานจากเทรดหลักไปยัง Web Worker จะช่วยลดการแย่งชิงทรัพยากรในเทรดหลัก ซึ่งจะช่วยปรับปรุงเมตริกการตอบสนอง Interaction to Next Paint (INP) ของหน้าเว็บได้ เมื่อเทรดหลักมีงานที่ต้องประมวลผลน้อยลง ก็จะตอบสนองต่อการโต้ตอบของผู้ใช้ได้เร็วขึ้น
การทำงานของเทรดหลักที่น้อยลง โดยเฉพาะในระหว่างการเริ่มต้นระบบ ยังอาจเป็นประโยชน์ต่อ Largest Contentful Paint (LCP) ด้วยการลดงานที่ใช้เวลานาน การแสดงผลองค์ประกอบ LCP ต้องใช้เวลาในเทรดหลัก ไม่ว่าจะเป็นการแสดงผลข้อความหรือรูปภาพ ซึ่งเป็นองค์ประกอบ LCP ที่พบบ่อยและพบได้ทั่วไป และการลดงานในเทรดหลักโดยรวมจะช่วยให้มั่นใจได้ว่าองค์ประกอบ LCP ของหน้าเว็บมีโอกาสน้อยลงที่จะถูกบล็อกโดยงานที่ใช้ทรัพยากรมากซึ่ง Web Worker สามารถจัดการแทนได้
การทำ Threading ด้วย Web Worker
แพลตฟอร์มอื่นๆ มักจะรองรับการทำงานแบบขนานโดยอนุญาตให้คุณกำหนดฟังก์ชันให้กับเทรด ซึ่งจะทำงานควบคู่ไปกับส่วนที่เหลือของโปรแกรม คุณสามารถเข้าถึงตัวแปรเดียวกันจากทั้ง 2 เทรด และการเข้าถึงทรัพยากรที่ใช้ร่วมกันเหล่านี้สามารถซิงค์กับ Mutex และ Semaphore เพื่อป้องกันภาวะแข่งขันได้
ใน JavaScript เราสามารถใช้ฟังก์ชันการทำงานที่คล้ายกันได้จาก Web Worker ซึ่งมีมาตั้งแต่ปี 2007 และรองรับในเบราว์เซอร์หลักๆ ทั้งหมดตั้งแต่ปี 2012 Web Worker ทำงานควบคู่ไปกับเทรดหลัก แต่ไม่เหมือนกับการทำเทรดดิ้งของระบบปฏิบัติการตรงที่แชร์ตัวแปรไม่ได้
หากต้องการสร้าง Web Worker ให้ส่งไฟล์ไปยังคอนสตรัคเตอร์ของ Worker ซึ่งจะเริ่มเรียกใช้ไฟล์นั้นในชุดข้อความแยกต่างหาก ดังนี้
const worker = new Worker("./worker.js");
สื่อสารกับ Web Worker โดยส่งข้อความโดยใช้ postMessage API ส่งค่าข้อความเป็นพารามิเตอร์ในการเรียก postMessage แล้วเพิ่ม Listener เหตุการณ์ข้อความลงใน Worker ดังนี้
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 และตั้งค่า 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);
});
ต้องยอมรับว่าวิธีนี้มีข้อจำกัดอยู่บ้าง ที่ผ่านมา Web Worker มักใช้เพื่อย้ายงานหนักชิ้นเดียวออกจากเทรดหลัก การพยายามจัดการการดำเนินการหลายอย่างด้วย Web Worker รายเดียวจะยุ่งยากอย่างรวดเร็ว เนื่องจากคุณต้องเข้ารหัสไม่เพียงพารามิเตอร์เท่านั้น แต่ยังรวมถึงการดำเนินการในข้อความด้วย และคุณต้องทำบัญชีเพื่อให้คำตอบตรงกับคำขอ ความซับซ้อนดังกล่าวอาจเป็นสาเหตุที่ทำให้ Web Worker ไม่ได้รับการนำไปใช้อย่างแพร่หลายมากขึ้น
แต่หากเราสามารถลดความยากในการสื่อสารระหว่างเทรดหลักกับ Web Worker ได้ โมเดลนี้ก็อาจเหมาะกับ Use Case หลายๆ อย่าง และโชคดีที่มีไลบรารีที่ทำแบบนั้นได้
Comlink: ลดภาระงานของ Web Worker
Comlink เป็นไลบรารีที่มีเป้าหมายเพื่อให้คุณใช้ Web Worker ได้โดยไม่ต้องคำนึงถึงรายละเอียดของ postMessage Comlink ช่วยให้คุณแชร์ตัวแปรระหว่าง Web Worker กับเทรดหลักได้เกือบเหมือนกับภาษาโปรแกรมอื่นๆ ที่รองรับเทรดดิ้ง
คุณตั้งค่า Comlink โดยนำเข้าใน Web Worker และกำหนดชุดฟังก์ชันที่จะแสดงต่อเทรดหลัก จากนั้นคุณก็นำเข้า Comlink ในเทรดหลัก รวม 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 ในเทรดหลักจะทำงานเหมือนกับตัวแปรใน Web Worker ยกเว้นว่าทุกฟังก์ชันจะแสดงผล Promise สำหรับค่าแทนที่จะเป็นค่าเอง
คุณควรย้ายโค้ดใดไปยัง Web Worker
Web Worker ไม่มีสิทธิ์เข้าถึง DOM และ API จำนวนมาก เช่น WebUSB, WebRTC หรือ Web Audio คุณจึงไม่สามารถใส่ส่วนต่างๆ ของแอปที่อาศัยการเข้าถึงดังกล่าวใน Worker ได้ อย่างไรก็ตาม โค้ดเล็กๆ ทุกส่วนที่ย้ายไปยังเวิร์กเกอร์จะช่วยเพิ่มพื้นที่ว่างในเทรดหลักสำหรับสิ่งที่ต้องอยู่ในเทรดหลัก เช่น การอัปเดตอินเทอร์เฟซผู้ใช้
ปัญหาหนึ่งสำหรับนักพัฒนาเว็บคือเว็บแอปส่วนใหญ่ใช้เฟรมเวิร์ก UI เช่น Vue หรือ React เพื่อจัดการทุกอย่างในแอป ทุกอย่างเป็นคอมโพเนนต์ของเฟรมเวิร์ก จึงเชื่อมโยงกับ DOM โดยเนื้อแท้ ซึ่งดูเหมือนว่าจะทำให้การย้ายข้อมูลไปยังสถาปัตยกรรม OMT เป็นเรื่องยาก
อย่างไรก็ตาม หากเราเปลี่ยนไปใช้โมเดลที่แยกข้อกังวลเกี่ยวกับ UI ออกจากข้อกังวลอื่นๆ เช่น การจัดการสถานะ Web Worker จะมีประโยชน์มากแม้จะใช้แอปที่อิงตามเฟรมเวิร์กก็ตาม ซึ่งเป็นแนวทางที่ใช้กับ PROXX
PROXX: กรณีศึกษา OMT
ทีม Google Chrome ได้พัฒนา PROXX เป็นโคลนของ Minesweeper ที่ตรงตามข้อกำหนดของ Progressive Web App ซึ่งรวมถึงการทำงานแบบออฟไลน์และมีประสบการณ์ของผู้ใช้ที่น่าสนใจ แต่เวอร์ชันแรกๆ ของเกมกลับทำงานได้ไม่ดีนักในอุปกรณ์ที่มีข้อจำกัด เช่น ฟีเจอร์โฟน ซึ่งทำให้ทีมตระหนักว่าเธรดหลักเป็นจุดคอขวด
ทีมตัดสินใจใช้ Web Worker เพื่อแยกสถานะภาพของเกมออกจากตรรกะของเกม ดังนี้
- เทรดหลักจะจัดการการแสดงผลภาพเคลื่อนไหวและการเปลี่ยนสไลด์
- Web Worker จะจัดการตรรกะของเกม ซึ่งเป็นการคำนวณล้วนๆ
OMT มีผลลัพธ์ที่น่าสนใจต่อประสิทธิภาพของฟีเจอร์โฟนของ PROXX ในเวอร์ชันที่ไม่ใช่ OMT UI จะค้างเป็นเวลา 6 วินาทีหลังจากที่ผู้ใช้โต้ตอบกับ UI ไม่มีความคิดเห็น และผู้ใช้ต้องรอจนครบ 6 วินาทีก่อนจึงจะทำอย่างอื่นได้
อย่างไรก็ตาม ในเวอร์ชัน OMT เกมจะใช้เวลา 12 วินาทีในการอัปเดต UI ให้เสร็จสมบูรณ์ แม้ว่าการดำเนินการดังกล่าวจะดูเหมือนทำให้ประสิทธิภาพลดลง แต่จริงๆ แล้วจะช่วยเพิ่มความคิดเห็นให้กับผู้ใช้ การชะลอตัวเกิดขึ้นเนื่องจากแอปส่งเฟรมมากกว่าเวอร์ชันที่ไม่ใช่ OMT ซึ่งไม่ได้ส่งเฟรมเลย ผู้ใช้จึงทราบว่ามีบางอย่างเกิดขึ้นและเล่นต่อได้เมื่อ UI อัปเดต ซึ่งทำให้เกมรู้สึกดีขึ้นมาก
นี่เป็นการแลกเปลี่ยนที่ผ่านการพิจารณามาแล้ว เรามอบประสบการณ์ที่ให้ความรู้สึกดีขึ้นแก่ผู้ใช้อุปกรณ์ที่มีข้อจำกัดโดยไม่ส่งผลกระทบต่อผู้ใช้อุปกรณ์ระดับไฮเอนด์
ผลกระทบของสถาปัตยกรรม OMT
ดังที่ตัวอย่าง PROXX แสดงให้เห็น OMT ช่วยให้แอปทำงานได้อย่างเสถียรในอุปกรณ์ที่หลากหลายมากขึ้น แต่ไม่ได้ทำให้แอปเร็วขึ้น
- คุณเพียงแค่ย้ายงานออกจากเทรดหลัก ไม่ได้ลดงาน
- ค่าใช้จ่ายในการสื่อสารเพิ่มเติมระหว่าง Web Worker กับเทรดหลักอาจทำให้การทำงานช้าลงเล็กน้อยในบางครั้ง
พิจารณาข้อดีข้อเสีย
เนื่องจากเทรดหลักสามารถประมวลผลการโต้ตอบของผู้ใช้ เช่น การเลื่อนขณะที่ JavaScript ทำงานอยู่ จึงมีเฟรมที่ถูกข้ามไปน้อยลงแม้ว่าเวลารอทั้งหมดอาจนานขึ้นเล็กน้อยก็ตาม การทำให้ผู้ใช้รอสักครู่จะดีกว่าการข้ามเฟรม เนื่องจากขอบเขตของข้อผิดพลาดจะน้อยกว่าสำหรับเฟรมที่ข้ามไป โดยการข้ามเฟรมจะเกิดขึ้นในหน่วยมิลลิวินาที ในขณะที่คุณมีเวลาหลายร้อยมิลลิวินาทีก่อนที่ผู้ใช้จะรับรู้ถึงเวลารอ
เนื่องจากประสิทธิภาพในอุปกรณ์ต่างๆ ไม่สามารถคาดเดาได้ เป้าหมายของสถาปัตยกรรม OMT จึงเป็นเรื่องของการลดความเสี่ยง ซึ่งก็คือการทำให้แอปมีประสิทธิภาพมากขึ้นเมื่อเผชิญกับสภาวะรันไทม์ที่ผันแปรสูง ไม่ใช่เรื่องของประโยชน์ด้านประสิทธิภาพของการทำงานพร้อมกัน ความยืดหยุ่นที่เพิ่มขึ้นและการปรับปรุง UX นั้นคุ้มค่ามากกว่าการแลกกับความเร็วที่ลดลงเล็กน้อย
หมายเหตุเกี่ยวกับเครื่องมือ
Web Worker ยังไม่เป็นที่นิยม เครื่องมือโมดูลส่วนใหญ่ เช่น webpack และ Rollup จึงไม่รองรับ Web Worker โดยค่าเริ่มต้น (แต่ Parcel มีนะ) โชคดีที่มีปลั๊กอินที่ทำให้ Web Worker ทำงานกับ Webpack และ Rollup ได้
- worker-plugin สำหรับ webpack
- rollup-plugin-off-main-thread สำหรับ Rollup
สรุป
เราต้องรองรับอุปกรณ์ที่มีข้อจำกัดเพื่อให้แอปของเรามีความน่าเชื่อถือและเข้าถึงได้มากที่สุด โดยเฉพาะในตลาดที่มีความเป็นสากลมากขึ้นเรื่อยๆ เนื่องจากผู้ใช้ส่วนใหญ่ทั่วโลกเข้าถึงเว็บด้วยอุปกรณ์เหล่านี้ OMT เป็นวิธีที่มีแนวโน้มว่าจะช่วยเพิ่มประสิทธิภาพในอุปกรณ์ดังกล่าวได้โดยไม่ส่งผลเสียต่อผู้ใช้อุปกรณ์ระดับไฮเอนด์
นอกจากนี้ OMT ยังมีประโยชน์รองดังนี้
- โดยจะย้ายต้นทุนการดำเนินการ JavaScript ไปยังเทรดแยกต่างหาก
- ซึ่งจะย้ายค่าใช้จ่ายการแยกวิเคราะห์ ซึ่งหมายความว่า UI อาจเปิดเครื่องได้เร็วขึ้น ซึ่งอาจลด First Contentful Paint หรือแม้แต่เวลาในการตอบสนอง ซึ่งจะช่วยเพิ่มคะแนน Lighthouse ได้
Web Worker ไม่ใช่เรื่องน่ากลัว เครื่องมืออย่าง Comlink ช่วยลดภาระงานของ Worker และทำให้ Worker เป็นตัวเลือกที่เหมาะสมสำหรับเว็บแอปพลิเคชันที่หลากหลาย