ใช้ Web Worker เพื่อเรียกใช้ JavaScript ออกจากเทรดหลักของเบราว์เซอร์

สถาปัตยกรรมแบบแยกจากเทรดหลักช่วยปรับปรุงความน่าเชื่อถือและประสบการณ์ของผู้ใช้แอปได้อย่างมาก

ในช่วง 20 ปีที่ผ่านมา เว็บได้พัฒนาไปอย่างมากจากเอกสารแบบคงที่ที่มีสไตล์และรูปภาพเพียงไม่กี่รายการไปจนถึงแอปพลิเคชันแบบไดนามิกที่ซับซ้อน อย่างไรก็ตาม สิ่งหนึ่งที่ยังคงไม่เปลี่ยนแปลงมากนักคือ เรามีเพียง 1 เธรดต่อแท็บเบราว์เซอร์ (มีข้อยกเว้นบางประการ) เพื่อทำงานในการแสดงผลเว็บไซต์และเรียกใช้ JavaScript

ด้วยเหตุนี้ เทรดหลักจึงทำงานหนักมาก และเมื่อเว็บแอปมีความซับซ้อนมากขึ้น เทรดหลักจะกลายเป็นคอขวดที่สำคัญสำหรับประสิทธิภาพ ที่แย่ไปกว่านั้นคือระยะเวลาที่ใช้ในการเรียกใช้โค้ดในชุดข้อความหลักสำหรับผู้ใช้ที่เฉพาะเจาะจงนั้นคาดเดาไม่ได้เกือบทั้งหมด เนื่องจากความสามารถของอุปกรณ์มีผลอย่างมากต่อประสิทธิภาพ ความไม่แน่นอนนี้จะเพิ่มขึ้นเรื่อยๆ เมื่อผู้ใช้เข้าถึงเว็บจากอุปกรณ์ที่หลากหลายมากขึ้น ตั้งแต่ฟีเจอร์โฟนที่มีข้อจำกัดสูงไปจนถึงอุปกรณ์เรือธงที่มีประสิทธิภาพสูงและอัตราการรีเฟรชสูง

หากต้องการให้เว็บแอปที่ซับซ้อนเป็นไปตามหลักเกณฑ์ด้านประสิทธิภาพอย่างน่าเชื่อถือ เช่น Core Web Vitals ซึ่งอิงตามข้อมูลเชิงประจักษ์เกี่ยวกับความรับรู้และจิตวิทยาของมนุษย์ เราจำเป็นต้องมีวิธีเรียกใช้โค้ดนอกเทรดหลัก (OMT)

ทำไมถึงควรใช้ Web Worker

โดยค่าเริ่มต้น JavaScript เป็นภาษาแบบ Single-Thread ที่เรียกใช้งานในเทรดหลัก อย่างไรก็ตาม Web Worker เป็นเหมือนทางออกสำหรับเทรดหลักโดยอนุญาตให้นักพัฒนาแอปสร้างเทรดแยกต่างหากเพื่อจัดการงานนอกเทรดหลัก แม้ว่าขอบเขตของ Web Worker จะจำกัดและไม่ได้ให้สิทธิ์เข้าถึง DOM โดยตรง แต่ก็อาจเป็นประโยชน์อย่างมากหากมีงานจำนวนมากที่ต้องทำ ซึ่งอาจทำให้เทรดหลักทำงานหนักเกินไป

ในส่วนของ Core Web Vitals การเรียกใช้เวิร์กเกอร์นอกเทรดหลักอาจเป็นประโยชน์ โดยเฉพาะอย่างยิ่ง การย้ายงานจากชุดข้อความหลักไปยัง Web Worker จะช่วยลดการแย่งชิงชุดข้อความหลัก ซึ่งจะช่วยปรับปรุงเมตริกการตอบสนอง Interaction to Next Paint (INP) ของหน้าเว็บ เมื่อเทรดหลักมีงานที่ต้องประมวลผลน้อยลง ก็จะตอบสนองต่อการโต้ตอบของผู้ใช้ได้เร็วขึ้น

การทำงานของเทรดหลักที่น้อยลง โดยเฉพาะอย่างยิ่งในระหว่างการเริ่มต้น ยังอาจเป็นประโยชน์ต่อ Largest Contentful Paint (LCP) ด้วยการลดงานที่ใช้เวลานาน การแสดงผลองค์ประกอบ LCP ต้องใช้เวลาของเทรดหลัก ไม่ว่าจะเป็นการแสดงผลข้อความหรือรูปภาพ ซึ่งเป็นองค์ประกอบ LCP ที่พบบ่อยและเป็นเรื่องปกติ และการลดงานของเทรดหลักโดยรวมจะช่วยให้มั่นใจได้ว่าองค์ประกอบ LCP ของหน้าเว็บมีโอกาสน้อยที่จะถูกบล็อกโดยงานที่มีค่าใช้จ่ายสูงซึ่ง Web Worker สามารถจัดการแทนได้

การใช้เธรดกับ Web Worker

โดยปกติแล้วแพลตฟอร์มอื่นๆ จะรองรับการทำงานแบบคู่ขนานด้วยการอนุญาตให้คุณกำหนดฟังก์ชันให้กับเธรด ซึ่งจะทำงานควบคู่ไปกับส่วนอื่นๆ ของโปรแกรม คุณสามารถเข้าถึงตัวแปรเดียวกันจากทั้ง 2 เธรด และซิงค์การเข้าถึงทรัพยากรที่แชร์เหล่านี้กับ Mutex และ Semaphore เพื่อป้องกัน Race Condition ได้

ใน JavaScript เราสามารถรับฟังก์ชันการทำงานที่คล้ายกันได้จาก Web Worker ซึ่งมีมาตั้งแต่ปี 2007 และรองรับในเบราว์เซอร์หลักๆ ทั้งหมดตั้งแต่ปี 2012 Web Worker จะทำงานควบคู่ไปกับเทรดหลัก แต่จะแชร์ตัวแปรไม่ได้เหมือนกับการใช้เทรดในระบบปฏิบัติการ

หากต้องการสร้าง Web Worker ให้ส่งไฟล์ไปยังตัวสร้าง Worker ซึ่งจะเริ่มเรียกใช้ไฟล์นั้นในเธรดแยกต่างหาก

const worker = new Worker("./worker.js");

สื่อสารกับ Web Worker โดยส่งข้อความโดยใช้ postMessage API ส่งค่าข้อความเป็นพารามิเตอร์ในpostMessageการเรียกใช้ แล้วเพิ่มเครื่องฟังสัญญาณเหตุการณ์ข้อความไปยัง 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
  // ...
});

หากต้องการส่งข้อความกลับไปยังเธรดหลัก ให้ใช้ postMessage API เดียวกันใน Web Worker และตั้งค่าเครื่องมือฟังเหตุการณ์ในเธรดหลัก

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 ได้ โมเดลนี้ก็อาจเหมาะกับกรณีการใช้งานหลายๆ อย่าง และโชคดีที่มีห้องสมุดที่ทำหน้าที่นี้

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 ได้ อย่างไรก็ตาม โค้ดขนาดเล็กทุกชิ้นที่ย้ายไปยัง 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 วินาทีก่อนจึงจะทำอย่างอื่นได้

เวลาตอบสนองของ UI ใน PROXX เวอร์ชันที่ไม่ใช่ OMT

แต่ในเวอร์ชัน OMT เกมจะใช้เวลา 12 วินาทีในการอัปเดต UI ให้เสร็จสมบูรณ์ แม้ว่าการดำเนินการดังกล่าวอาจดูเหมือนเป็นการสูญเสียประสิทธิภาพ แต่จริงๆ แล้วจะช่วยเพิ่มความคิดเห็นที่ส่งถึงผู้ใช้ ประสิทธิภาพลดลงเนื่องจากแอปส่งเฟรมมากกว่าเวอร์ชันที่ไม่ใช่ OMT ซึ่งไม่ได้ส่งเฟรมเลย ดังนั้นผู้ใช้จึงทราบว่ามีบางอย่างเกิดขึ้นและสามารถเล่นเกมต่อได้เมื่อ UI อัปเดต ซึ่งจะทำให้เกมดูดีขึ้นอย่างมาก

เวลาตอบสนองของ UI ใน PROXX เวอร์ชัน OMT

นี่คือการแลกเปลี่ยนที่ตั้งใจทำ เรามอบประสบการณ์ที่รู้สึกดีขึ้นแก่ผู้ใช้อุปกรณ์ที่มีข้อจำกัดโดยไม่ลงโทษผู้ใช้อุปกรณ์ระดับไฮเอนด์

ผลกระทบของสถาปัตยกรรม OMT

ดังที่ตัวอย่าง PROXX แสดงให้เห็น OMT ช่วยให้แอปของคุณทำงานได้อย่างน่าเชื่อถือในอุปกรณ์ที่หลากหลายมากขึ้น แต่ไม่ได้ทำให้แอปเร็วขึ้น

  • คุณเพียงแค่ย้ายงานจากเทรดหลัก ไม่ได้ลดงาน
  • บางครั้งค่าใช้จ่ายในการสื่อสารเพิ่มเติมระหว่าง Web Worker กับเทรดหลักอาจทำให้การทำงานช้าลงเล็กน้อย

พิจารณาข้อดีข้อเสีย

เนื่องจากเทรดหลักพร้อมประมวลผลการโต้ตอบของผู้ใช้ เช่น การเลื่อน ขณะที่ JavaScript ทำงาน จึงมีเฟรมหลุดน้อยลงแม้ว่าเวลารอทั้งหมดอาจนานขึ้นเล็กน้อย การทำให้ผู้ใช้รอสักครู่ดีกว่าการทิ้งเฟรมเนื่องจากอัตราความคลาดเคลื่อนของการทิ้งเฟรมมีน้อยกว่า โดยการทิ้งเฟรมเกิดขึ้นในหน่วยมิลลิวินาที ขณะที่คุณมีเวลาหลายร้อยมิลลิวินาทีก่อนที่ผู้ใช้จะรับรู้ถึงเวลารอ

เนื่องจากประสิทธิภาพในอุปกรณ์ต่างๆ คาดเดาได้ยาก เป้าหมายของสถาปัตยกรรม OMT จึงมุ่งเน้นไปที่การลดความเสี่ยง ซึ่งจะช่วยให้แอปของคุณมีความแข็งแกร่งมากขึ้นเมื่อต้องเผชิญกับสภาวะรันไทม์ที่มีความผันผวนสูง ไม่ใช่เกี่ยวกับประโยชน์ด้านประสิทธิภาพของการประมวลผลแบบคู่ขนาน การเพิ่มความยืดหยุ่นและการปรับปรุง UX นั้นคุ้มค่ามากกว่าการแลกเปลี่ยนความเร็วเพียงเล็กน้อย

หมายเหตุเกี่ยวกับเครื่องมือ

Web Worker ยังไม่เป็นที่นิยม ดังนั้นเครื่องมือโมดูลส่วนใหญ่ เช่น webpack และ Rollup จึงไม่รองรับฟีเจอร์นี้โดยค่าเริ่มต้น (แต่Parcel ทำได้) โชคดีที่มีปลั๊กอินที่ช่วยให้ Web Worker ทำงานกับ Webpack และ Rollup ได้

สรุป

เราต้องรองรับอุปกรณ์ที่มีข้อจำกัดเพื่อให้มั่นใจว่าแอปของเราจะมีความน่าเชื่อถือและเข้าถึงได้มากที่สุด โดยเฉพาะในตลาดที่มีการแข่งขันสูงขึ้นเรื่อยๆ ทั่วโลก เนื่องจากอุปกรณ์เหล่านี้เป็นวิธีที่ผู้ใช้ส่วนใหญ่เข้าถึงเว็บทั่วโลก OMT เป็นวิธีที่มีแนวโน้มในการเพิ่มประสิทธิภาพในอุปกรณ์ดังกล่าวโดยไม่ส่งผลเสียต่อผู้ใช้อุปกรณ์ระดับไฮเอนด์

นอกจากนี้ OMT ยังมีประโยชน์รองดังนี้

  • โดยจะย้ายต้นทุนการดำเนินการ JavaScript ไปยังเธรดแยก
  • ซึ่งจะช่วยลดค่าใช้จ่ายในการแยกวิเคราะห์ ซึ่งหมายความว่า UI อาจบูตได้เร็วขึ้น ซึ่งอาจช่วยลด First Contentful Paint หรือแม้แต่ Time to Interactive ซึ่งจะช่วยเพิ่มคะแนน Lighthouse ได้

Web Worker ไม่ได้น่ากลัวอย่างที่คิด เครื่องมืออย่าง Comlink ช่วยให้ผู้ปฏิบัติงานทำงานได้ง่ายขึ้นและเป็นตัวเลือกที่ใช้งานได้จริงสำหรับเว็บแอปพลิเคชันที่หลากหลาย