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