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

ตอนนี้คุณสามารถย้ายการประมวลผลหนักๆ ไปยังเธรดเบื้องหลังได้ง่ายขึ้นด้วยโมดูล JavaScript ใน Web Worker

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

ในแพลตฟอร์มเว็บ องค์ประกอบหลักสำหรับการสร้างเธรดและความขนานคือ Web Workers API Worker เป็นการแยกส่วนแบบเบาที่อยู่เหนือเธรดของระบบปฏิบัติการซึ่งแสดง 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 ปีแล้ว แม้ว่าสิ่งนี้จะหมายความว่า Worker มีการรองรับเบราว์เซอร์ที่ยอดเยี่ยมและได้รับการเพิ่มประสิทธิภาพอย่างดี แต่ก็หมายความว่า Worker มีมานานก่อนโมดูล JavaScript เนื่องจากไม่มีระบบโมดูลเมื่อออกแบบ Worker API สำหรับการโหลดโค้ดลงใน Worker และการเขียนสคริปต์จึงยังคงคล้ายกับวิธีการโหลดสคริปต์แบบซิงโครนัส ที่ใช้กันทั่วไปในปี 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() สำหรับการโหลดโค้ด แต่จะห่อหุ้มโมดูลในฟังก์ชันเพื่อหลีกเลี่ยงการชนกันของตัวแปรและจำลอง การนำเข้าและส่งออกการอ้างอิง

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

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

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

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

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

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() เมธอดเก่าจะใช้ไม่ได้ในโมดูล Worker เพื่อให้มั่นใจว่าคุณจะได้ประสิทธิภาพที่ยอดเยี่ยม การเปลี่ยนให้ Worker ใช้โมดูล JavaScript หมายความว่าระบบจะโหลดโค้ดทั้งหมดในโหมดเข้มงวด การเปลี่ยนแปลงที่สำคัญอีกอย่างคือค่าของ this ในขอบเขตระดับบนสุดของโมดูล JavaScript คือ undefined ในขณะที่ใน Worker แบบคลาสสิก ค่าดังกล่าวคือขอบเขตส่วนกลางของ Worker โชคดีที่self global มีมาโดยตลอดเพื่อใช้อ้างอิงขอบเขตส่วนกลาง โดยใช้ได้ใน Worker ทุกประเภท รวมถึง Service Worker และใน DOM

โหลดล่วงหน้าสำหรับผู้ปฏิบัติงานด้วย modulepreload

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

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

แล้วพนักงานที่แชร์ล่ะ

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

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

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

แล้ว Service Worker ล่ะ

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

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

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

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