เทรดเว็บด้วยผู้ปฏิบัติงานโมดูล

ตอนนี้การย้ายงานหนักๆ ไปยังชุดข้อความเบื้องหลังทำได้ง่ายขึ้นด้วยโมดูล JavaScript ใน Web Worker

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

ในแพลตฟอร์มเว็บ องค์ประกอบพื้นฐานหลักสำหรับการทำเธรดและการทำงานพร้อมกันคือ Web Workers API Worker เป็นนามธรรมแบบ Lightweight ที่อยู่ด้านบนของเทรดระบบปฏิบัติการซึ่งแสดง API การส่งข้อความสำหรับการสื่อสารระหว่างเทรด ซึ่งจะเป็นประโยชน์อย่างมากเมื่อทำการคำนวณที่มีค่าใช้จ่ายสูงหรือดำเนินการกับชุดข้อมูลขนาดใหญ่ ซึ่งช่วยให้เทรดหลักทำงานได้อย่างราบรื่นขณะดำเนินการที่มีค่าใช้จ่ายสูงในเทรดที่ทำงานอยู่เบื้องหลังอย่างน้อย 1 เทรด

ต่อไปนี้คือตัวอย่างการใช้งาน Worker ทั่วไป โดยสคริปต์ Worker จะคอยฟังข้อความจากเทรดหลักและตอบกลับโดยส่งข้อความของตนเองกลับไป

page.js:

const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
  console.log(e.data);
});
worker.postMessage('hello');

worker.js:

addEventListener('message', e => {
  if (e.data === 'hello') {
    postMessage('world');
  }
});

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

ประวัติ: ผู้ปฏิบัติงานแบบคลาสสิก

ตัวสร้าง Worker จะใช้ URL ของสคริปต์แบบคลาสสิก ซึ่งสัมพันธ์กับ URL ของเอกสาร โดยจะแสดงผลการอ้างอิงไปยังอินสแตนซ์ Worker ใหม่ทันที ซึ่งจะแสดงอินเทอร์เฟซการรับส่งข้อความ รวมถึงเมธอด terminate() ที่จะหยุดและทำลาย Worker ทันที

const worker = new Worker('worker.js');

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

worker.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

// global to the whole worker
function sayHello() {
  return 'world';
}

ด้วยเหตุนี้ Web Worker จึงส่งผลกระทบอย่างมากต่อสถาปัตยกรรมของแอปพลิเคชันมาโดยตลอด นักพัฒนาแอปต้องสร้างเครื่องมือและวิธีแก้ปัญหาที่ชาญฉลาดเพื่อให้สามารถใช้ Web Worker ได้โดยไม่ต้องละทิ้งแนวทางปฏิบัติด้านการพัฒนาที่ทันสมัย ตัวอย่างเช่น Bundler อย่าง webpack จะฝังการติดตั้งใช้งานตัวโหลดโมดูลขนาดเล็กลงในโค้ดที่สร้างขึ้นซึ่งใช้ importScripts() สำหรับการโหลดโค้ด แต่จะรวมโมดูลไว้ในฟังก์ชันเพื่อหลีกเลี่ยงการชนกันของตัวแปรและจำลองการนำเข้าและส่งออกทรัพยากร Dependency

ป้อนผู้ปฏิบัติงานในโมดูล

โหมดใหม่สำหรับ Web Worker ที่มีประโยชน์ด้านการยศาสตร์และประสิทธิภาพของโมดูล JavaScript จะเปิดตัวใน Chrome 80 โดยเรียกว่า Module Worker ตอนนี้เครื่องมือสร้าง Worker ยอมรับตัวเลือก {type:"module"} ใหม่ ซึ่งจะเปลี่ยนการโหลดและการเรียกใช้สคริปต์ให้ตรงกับ <script type="module">

const worker = new Worker('worker.js', {
  type: 'module'
});

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

การเปลี่ยนไปใช้โมดูล JavaScript ยังช่วยให้สามารถใช้การนําเข้าแบบไดนามิกสําหรับโค้ดการโหลดแบบ Lazy Loading โดยไม่บล็อกการดําเนินการของ Worker การนำเข้าแบบไดนามิกมีความชัดเจนมากกว่าการใช้ importScripts() เพื่อโหลดทรัพยากร Dependency เนื่องจากระบบจะแสดงผลการส่งออกของโมดูลที่นำเข้าแทนที่จะอาศัยตัวแปรส่วนกลาง

worker.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

import greetings from './data.js';
export function sayHello() {
  return greetings.hello;
}

เพื่อให้มั่นใจว่าจะมีประสิทธิภาพที่ดี เมธอด importScripts() แบบเก่าจึงไม่พร้อมใช้งานภายในเวิร์กเกอร์โมดูล การเปลี่ยนให้เวิร์กเกอร์ใช้โมดูล JavaScript หมายความว่าระบบจะโหลดโค้ดทั้งหมดในโหมดเข้มงวด การเปลี่ยนแปลงที่สำคัญอีกอย่างหนึ่งคือค่าของ this ในขอบเขตระดับบนสุดของโมดูล JavaScript คือ undefined ส่วนในเวิร์กเกอร์แบบคลาสสิก ค่าจะเป็นขอบเขตส่วนกลางของเวิร์กเกอร์ โชคดีที่มี self global ที่ให้ข้อมูลอ้างอิงถึงขอบเขตส่วนกลางเสมอ โดยจะใช้ได้ในผู้ปฏิบัติงานทุกประเภท รวมถึง Service Worker และใน DOM

โหลดเวิร์กเกอร์ล่วงหน้าด้วย modulepreload

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

<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">

<script>
  addEventListener('load', () => {
    // our worker code is likely already parsed and ready to execute!
    const worker = new Worker('worker.js', { type: 'module' });
  });
</script>

นอกจากนี้ โมดูลที่โหลดไว้ล่วงหน้ายังใช้ได้ทั้งเทรดหลักและเวิร์กเกอร์โมดูล ซึ่งจะเป็นประโยชน์สำหรับโมดูลที่นำเข้าในทั้ง 2 บริบท หรือในกรณีที่ไม่สามารถทราบล่วงหน้าได้ว่าโมดูลจะใช้ในเทรดหลักหรือในเวิร์กเกอร์

ก่อนหน้านี้ ตัวเลือกที่ใช้ได้สำหรับการโหลดสคริปต์ Web Worker ล่วงหน้ามีจำกัดและอาจไม่น่าเชื่อถือ Classic Worker มีประเภททรัพยากร "Worker" ของตนเองสำหรับการโหลดล่วงหน้า แต่ไม่มีเบราว์เซอร์ใดที่ใช้ <link rel="preload" as="worker"> ด้วยเหตุนี้ เทคนิคหลักที่ใช้ในการโหลด Web Worker ล่วงหน้าคือการใช้ <link rel="prefetch"> ซึ่งอาศัยแคช HTTP ทั้งหมด เมื่อใช้ร่วมกับส่วนหัวการแคชที่ถูกต้อง วิธีนี้จะช่วยหลีกเลี่ยงไม่ให้การสร้างอินสแตนซ์ของ Worker ต้องรอการดาวน์โหลดสคริปต์ Worker อย่างไรก็ตาม เทคนิคนี้ไม่รองรับทรัพยากร Dependency ที่โหลดไว้ล่วงหน้าหรือการแยกวิเคราะห์ล่วงหน้า ซึ่งต่างจาก modulepreload

แล้ว Shared Worker ล่ะ

Shared Worker ได้รับการอัปเดตให้รองรับโมดูล JavaScript ตั้งแต่ Chrome 83 เป็นต้นไป เช่นเดียวกับ Dedicated Worker การสร้าง Shared Worker ด้วยตัวเลือก {type:"module"} จะโหลดสคริปต์ Worker เป็นโมดูลแทนสคริปต์แบบคลาสสิก

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

ก่อนที่จะรองรับโมดูล JavaScript ตัวสร้าง SharedWorker() ต้องการเพียง URL และอาร์กิวเมนต์ name ที่ไม่บังคับ ซึ่งจะยังคงใช้ได้กับการใช้งาน Shared Worker แบบคลาสสิก แต่การสร้าง Shared Worker ของโมดูลจะต้องใช้อาร์กิวเมนต์ options ใหม่ ตัวเลือกที่ใช้ได้จะเหมือนกับตัวเลือกสำหรับ Dedicated Worker รวมถึงตัวเลือก name ที่มีผลแทนอาร์กิวเมนต์ name ก่อนหน้า

แล้ว Service Worker ล่ะ

ข้อกำหนดของ Service Worker ได้รับการอัปเดตแล้วเพื่อรองรับการยอมรับโมดูล JavaScript เป็นจุดแรกเข้า โดยใช้ตัวเลือก {type:"module"} เดียวกันกับโมดูลเวิร์กเกอร์ แต่การเปลี่ยนแปลงนี้ยังไม่ได้นำไปใช้ในเบราว์เซอร์ เมื่อเป็นเช่นนั้น คุณจะสร้างอินสแตนซ์ของ Service Worker โดยใช้โมดูล JavaScript ได้โดยใช้โค้ดต่อไปนี้

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

เมื่อมีการอัปเดตข้อกำหนดแล้ว เบราว์เซอร์ต่างๆ จะเริ่มใช้ลักษณะการทำงานใหม่ ซึ่งอาจใช้เวลาสักพักเนื่องจากมีความซับซ้อนเพิ่มเติมบางอย่างที่เกี่ยวข้องกับการนำโมดูล JavaScript ไปใช้กับ Service Worker การลงทะเบียน Service Worker ต้องเปรียบเทียบสคริปต์ที่นำเข้ากับเวอร์ชันที่แคชไว้ก่อนหน้าเมื่อพิจารณาว่าจะทริกเกอร์การอัปเดตหรือไม่ และต้องใช้กับโมดูล JavaScript เมื่อใช้สำหรับ Service Worker นอกจากนี้ Service Worker ต้องสามารถข้ามแคชสำหรับสคริปต์ในบางกรณีเมื่อตรวจหาการอัปเดต

แหล่งข้อมูลเพิ่มเติมและข้อมูลที่น่าสนใจ