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

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

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

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

ประวัติ: ผู้ใช้แบบคลาสสิก

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

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

ฟังก์ชัน importScripts() พร้อมใช้งานภายในเวิร์กเกอร์เว็บสำหรับการโหลดโค้ดเพิ่มเติม แต่จะทำให้การดําเนินการของเวิร์กเกอร์หยุดชั่วคราวเพื่อดึงข้อมูลและประเมินสคริปต์แต่ละรายการ นอกจากนี้ ยังเรียกใช้สคริปต์ในขอบเขตส่วนกลางเช่นเดียวกับแท็ก <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';
}

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

ป้อนผู้ปฏิบัติงานของข้อบังคับ

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

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

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

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

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

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

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

เวิร์กเกอร์ที่แชร์ได้รับการอัปเดตให้รองรับโมดูล 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 การลงทะเบียน Service Worker ต้องเปรียบเทียบสคริปต์ที่นําเข้ากับเวอร์ชันที่แคชไว้ก่อนหน้านี้เมื่อพิจารณาว่าจะทริกเกอร์การอัปเดตหรือไม่ และจะต้องติดตั้งใช้งานสําหรับโมดูล JavaScript เมื่อใช้กับ Service Worker นอกจากนี้ ผู้ให้บริการต้องสามารถข้ามแคชสคริปต์ได้ในบางกรณีเมื่อตรวจสอบการอัปเดต

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