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

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

ในช่วง 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 เว็บเวิร์กเกอร์จะทํางานควบคู่ไปกับเธรดหลัก แต่ไม่สามารถแชร์ตัวแปรได้ ซึ่งแตกต่างจากการแยกเธรดของระบบปฏิบัติการ

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

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 เป็นไลบรารีที่มีเป้าหมายเพื่อให้คุณใช้ Web Worker ได้โดยไม่ต้องคำนึงถึงรายละเอียดของ postMessage Comlink ช่วยให้คุณแชร์ตัวแปรระหว่าง Web Worker กับเธรดหลักได้เกือบเหมือนกับภาษาโปรแกรมอื่นๆ ที่รองรับการแยกเธรด

คุณตั้งค่า 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 เพื่อแยกสถานะภาพจากตรรกะของเกม ดังนี้

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

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 นั้นคุ้มค่ากว่าการเสียสละความเร็วเพียงเล็กน้อย

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

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

สรุป

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

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

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

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