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

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

ซูร์มา
ซูร์มา

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

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

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

ทำไมต้องทำงานด้านเว็บ

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

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

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

การจัดชุดข้อความด้วย Web Worker

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

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

ในการสร้าง Web Worker ให้ส่งไฟล์ไปยังเครื่องมือสร้างผู้ปฏิบัติงาน ซึ่งจะเริ่มต้นเรียกใช้ไฟล์นั้นในเธรดที่แยกต่างหาก:

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

สื่อสารกับ Web Worker โดยการส่งข้อความผ่าน postMessage API ส่งค่าข้อความเป็นพารามิเตอร์ในการเรียกใช้ postMessage แล้วเพิ่ม Listener เหตุการณ์ข้อความให้กับผู้ปฏิบัติงาน ดังนี้

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

Comlink เป็นไลบรารีที่มีเป้าหมายเพื่อให้คุณใช้ Web Worker ได้โดยไม่ต้องคิดถึงรายละเอียดของ postMessage Comlink ให้คุณแชร์ตัวแปรระหว่าง Web Worker และเทรดหลักได้แทบจะเหมือนกับภาษาโปรแกรมอื่นๆ ที่รองรับ Threading

คุณสามารถตั้งค่า Comlink ด้วยการนำเข้า Comlink ใน Web Worker และกำหนดชุดฟังก์ชันที่จะแสดงกับเทรดหลัก จากนั้นคุณนำเข้า 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 ในเทรดหลักจะทำงานเหมือนกับใน Web Worker ยกเว้นว่าทุกฟังก์ชันจะแสดงผลสัญญาสำหรับค่า ไม่ใช่ค่า

คุณควรย้ายโค้ดใดไปยัง Web Worker

ผู้ปฏิบัติงานเกี่ยวกับเว็บไม่มีสิทธิ์เข้าถึง DOM และ API หลายรายการ เช่น WebUSB, WebRTC หรือ Web Audio คุณจึงไม่สามารถวางแอปที่ต้องอาศัยการเข้าถึงดังกล่าวให้แก่ผู้ปฏิบัติงานได้ แต่ถึงอย่างนั้นโค้ดทุกๆ ส่วนก็ถูกย้ายไปยังผู้ปฏิบัติงานเพื่อซื้อพื้นที่ในเทรดหลักมากขึ้นสำหรับสิ่งที่ต้องมีอยู่ที่นั่น เช่น การอัปเดตอินเทอร์เฟซผู้ใช้

ปัญหาหนึ่งสำหรับนักพัฒนาเว็บคือเว็บแอปส่วนใหญ่ต้องใช้เฟรมเวิร์ก UI เช่น Vue หรือ React เพื่อจัดการทุกอย่างในแอป ทุกอย่างเป็นคอมโพเนนต์ของเฟรมเวิร์ก จึงผูกอยู่กับ DOM อยู่แล้ว ซึ่งจะทำให้การย้ายข้อมูลไปยังสถาปัตยกรรม OMT นั้นทำได้ยาก

อย่างไรก็ตาม หากเราเปลี่ยนไปใช้โมเดลที่แยกข้อกังวลเกี่ยวกับ UI ออกจากข้อกังวลอื่นๆ เช่น การจัดการของรัฐ ผู้ปฏิบัติงานเว็บก็อาจมีประโยชน์มากแม้จะใช้แอปที่อิงตามเฟรมเวิร์ก ซึ่งนั่นคือวิธีที่ใช้ PROXX

PROXX: กรณีศึกษาของ OMT

ทีม Google Chrome พัฒนา PROXX เป็นโคลนของ Minesweeper ที่เป็นไปตามข้อกำหนดของ Progressive Web App รวมถึงการทำงานแบบออฟไลน์และการสร้างประสบการณ์ที่น่าดึงดูดของผู้ใช้ น่าเสียดายที่เกมเวอร์ชันแรกๆ ทำงานได้ไม่ดีในอุปกรณ์ที่มีข้อจำกัดอย่างฟีเจอร์โฟน ซึ่งทำให้ทีมทราบว่าเทรดหลักเป็นจุดคอขวด

ทีมงานตัดสินใจใช้ Web Worker เพื่อแยกสถานะภาพของเกมออกจากตรรกะดังนี้

  • เทรดหลักจะจัดการการแสดงผลภาพเคลื่อนไหวและการเปลี่ยนภาพ
  • Web Worker ต้องจัดการกับตรรกะของเกม ซึ่งเป็นเพียงการคำนวณเท่านั้น

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

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

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

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

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

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

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

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

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

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

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

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

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

สรุป

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

นอกจากนี้ OMT ยังมีข้อดีข้อเสียดังนี้

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

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

รูปภาพหลักจาก Unsplash โดย James Peacock