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

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

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

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

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

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

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

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

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

ฟังก์ชัน importScripts() พร้อมใช้งานภายใน Web 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';
}

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

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

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

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

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

การย้ายไปยังโมดูล JavaScript ยังทำให้สามารถใช้การนำเข้าแบบไดนามิกสำหรับโค้ดการโหลดแบบ Lazy Loading ได้โดยไม่ต้องบล็อกการดำเนินการของผู้ปฏิบัติงาน การนำเข้าแบบไดนามิกนั้นชัดเจนกว่าการใช้ 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 ทั่วโลกที่อ้างอิงถึงขอบเขตระดับโลกมาโดยตลอด ฟีเจอร์นี้พร้อมใช้งานสำหรับผู้ปฏิบัติงานทุกประเภท รวมถึง 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 ล่วงหน้านั้นมีจำกัดและไม่จำเป็นต้องเชื่อถือได้ ผู้ปฏิบัติงานแบบคลาสสิกมีประเภททรัพยากร "ผู้ปฏิบัติงาน" สำหรับการโหลดล่วงหน้า แต่ไม่มีเบราว์เซอร์ที่ใช้ <link rel="preload" as="worker"> ดังนั้น เทคนิคหลักที่ใช้ได้สำหรับการโหลดผู้ปฏิบัติงานบนเว็บล่วงหน้าก็คือการใช้ <link rel="prefetch"> ซึ่งอาศัยแคช HTTP ทั้งหมด เมื่อใช้ร่วมกับส่วนหัวการแคชที่ถูกต้อง วิธีนี้จะช่วยให้หลีกเลี่ยงการสร้างอินสแตนซ์สำหรับผู้ปฏิบัติงานที่ต้องรอดาวน์โหลดสคริปต์ของผู้ปฏิบัติงานได้ แต่สิ่งที่ต่างจาก modulepreload คือเทคนิคนี้ไม่รองรับการโหลดทรัพยากร Dependency หรือการแยกวิเคราะห์ล่วงหน้า

แล้วผู้ปฏิบัติงานที่แชร์เป็นอย่างไร

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

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

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

แล้ว Service Worker ล่ะ

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

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

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

แหล่งข้อมูลเพิ่มเติมและอ่านเพิ่มเติม