JavaScript แบบแยกโค้ด

การโหลดทรัพยากร JavaScript ขนาดใหญ่ส่งผลต่อความเร็วของหน้าอย่างมาก การแบ่ง JavaScript ออกเป็นส่วนย่อยๆ และดาวน์โหลดเฉพาะสิ่งที่จำเป็นต้องให้หน้าเว็บทำงานระหว่างการเริ่มต้นทำงานจะช่วยปรับปรุงการตอบสนองของการโหลดของหน้าเว็บได้ ซึ่งส่งผลให้การโต้ตอบกับ NextPaint (INP) ของหน้าเว็บดีขึ้นได้

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

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

ลดการแยกวิเคราะห์และเรียกใช้ JavaScript ระหว่างเริ่มต้นผ่านการแยกโค้ด

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

ยิ่งไปกว่านั้น การดำเนินการและการแยกวิเคราะห์ของ JavaScript ที่มากเกินไปจะก่อให้เกิดปัญหาอย่างมากในระหว่างการโหลดหน้าเว็บครั้งแรก เนื่องจากนี่เป็นจุดในวงจรของหน้าเว็บที่ผู้ใช้มักจะโต้ตอบกับหน้าเว็บ อันที่จริงแล้ว Total block Time (TBT) ซึ่งเป็นเมตริกการตอบสนองของการโหลดมีความเชื่อมโยงสูงกับ INP ซึ่งชี้ให้เห็นว่าผู้ใช้มีแนวโน้มสูงที่จะพยายามโต้ตอบระหว่างการโหลดหน้าเว็บครั้งแรก

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

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

  • เนื่องจากจำเป็นต้องใช้ JavaScript เมื่อโหลดหน้าเว็บ จึงไม่สามารถโหลดที่เวลาอื่นได้
  • JavaScript ที่ยังเหลืออยู่ซึ่งโหลดได้ภายหลัง ซึ่งมักจะเป็นช่วงเวลาที่ผู้ใช้โต้ตอบกับองค์ประกอบการโต้ตอบหนึ่งๆ ในหน้านั้น

การแยกโค้ดทำได้โดยใช้ไวยากรณ์import() แบบไดนามิก ไวยากรณ์นี้แตกต่างจากองค์ประกอบ <script> ที่ขอทรัพยากร JavaScript ที่ระบุในช่วงเริ่มต้นใช้งาน โดยจะส่งคำขอสำหรับทรัพยากร JavaScript ภายหลังในระหว่างวงจรของหน้าเว็บ

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

ในข้อมูลโค้ด JavaScript ก่อนหน้า ระบบจะดาวน์โหลด แยกวิเคราะห์ และเรียกใช้โมดูล validate-form.mjs เฉพาะเมื่อผู้ใช้เบลอช่อง <input> ของแบบฟอร์ม ในสถานการณ์นี้ ทรัพยากร JavaScript ที่มีหน้าที่ขับเคลื่อนตรรกะการตรวจสอบความถูกต้องของแบบฟอร์มจะเกี่ยวข้องกับหน้าเว็บเมื่อมีแนวโน้มที่จะมีการใช้จริงมากที่สุดเท่านั้น

คุณสามารถกำหนดค่า Bundler ของ JavaScript เช่น Webpack, Parcel, Rollup และที่สร้างเพื่อแบ่งกลุ่ม JavaScript ออกเป็นกลุ่มเล็กๆ ทุกครั้งที่พบการเรียกใช้ import() แบบไดนามิกในซอร์สโค้ด เครื่องมือเหล่านี้ส่วนใหญ่ทำงานโดยอัตโนมัติ แต่โดยเฉพาะอย่างยิ่งในการสร้าง คุณจะต้องเลือกใช้การเพิ่มประสิทธิภาพนี้

หมายเหตุที่เป็นประโยชน์เกี่ยวกับการแยกโค้ด

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

ใช้ Bundler หากทำได้

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

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

นอกจากนี้ Bundler ยังหลีกเลี่ยงปัญหาเกี่ยวกับการจัดส่งโมดูลที่เลิกรวมกลุ่มจำนวนมาก ผ่านเครือข่ายด้วย สถาปัตยกรรมที่ใช้โมดูล JavaScript มักจะมีแผนผังโมดูลขนาดใหญ่และซับซ้อน เมื่อเลิกรวมกลุ่มโมดูล แต่ละโมดูลจะเป็นตัวแทนของคำขอ HTTP ที่แยกกัน และการโต้ตอบในเว็บแอปอาจล่าช้าหากคุณไม่รวมโมดูล แม้ว่าคุณจะใช้คำแนะนำทรัพยากร <link rel="modulepreload"> เพื่อโหลดแผนผังโมดูลขนาดใหญ่ให้เร็วที่สุดเท่าที่จะทำได้ แต่ยังคงเป็นแพ็กเกจ JavaScript ที่ดีกว่าในแง่ประสิทธิภาพการโหลด

อย่าปิดใช้งานการรวบรวมสตรีมมิงโดยไม่ตั้งใจ

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

คุณมี 2 วิธีในการตรวจสอบว่าการรวบรวมสตรีมเกิดขึ้นสำหรับเว็บแอปพลิเคชันใน Chromium ดังนี้

  • เปลี่ยนรูปแบบโค้ดที่ใช้งานจริงเพื่อหลีกเลี่ยงการใช้โมดูล JavaScript Bundler เปลี่ยนรูปแบบซอร์สโค้ด JavaScript โดยอิงตามเป้าหมายการคอมไพล์ และเป้าหมายมักเฉพาะเจาะจงกับสภาพแวดล้อมหนึ่งๆ โดย V8 จะใช้การคอมไพล์สตรีมมิงกับโค้ด JavaScript ที่ไม่ได้ใช้โมดูล โดยคุณจะกำหนดค่า Bundler ให้เปลี่ยนรูปแบบโค้ดโมดูล JavaScript ให้เป็นไวยากรณ์ที่ไม่ได้ใช้โมดูล JavaScript และฟีเจอร์ต่างๆ ได้
  • หากต้องการส่งโมดูล JavaScript ไปยังเวอร์ชันที่ใช้งานจริง ให้ใช้ส่วนขยาย .mjs ไม่ว่า JavaScript เวอร์ชันที่ใช้งานจริงจะใช้โมดูลหรือไม่ จะไม่มีประเภทเนื้อหาพิเศษสำหรับ JavaScript ที่ใช้โมดูล เมื่อเทียบกับ JavaScript ที่ไม่ได้ใช้ หากเกี่ยวข้องกับ V8 คุณสามารถเลือกไม่ใช้การคอมไพล์สตรีมมิงได้อย่างมีประสิทธิภาพเมื่อจัดส่งโมดูล JavaScript ในเวอร์ชันที่ใช้งานจริงโดยใช้ส่วนขยาย .js หากคุณใช้ส่วนขยาย .mjs สำหรับโมดูล JavaScript V8 ก็มั่นใจได้ว่าการรวบรวมสตรีมมิงสำหรับโค้ด JavaScript แบบโมดูลจะไม่เสียหาย

อย่าปล่อยให้ข้อควรพิจารณาเหล่านี้ทำให้คุณไม่สามารถใช้การแยกโค้ด การแยกโค้ดเป็นวิธีที่มีประสิทธิภาพในการลดเพย์โหลด JavaScript เริ่มต้นให้แก่ผู้ใช้ แต่การใช้ Bundler และการทราบวิธีรักษาลักษณะการคอมไพล์สตรีมมิงของ V8 คุณจะมั่นใจได้ว่าโค้ด JavaScript เวอร์ชันที่ใช้งานจริงจะให้ผู้ใช้ทำงานได้เร็วที่สุดเท่าที่จะเป็นไปได้

การสาธิตการนำเข้าแบบไดนามิก

Webpack

webpack มาพร้อมกับปลั๊กอินชื่อ SplitChunksPlugin ซึ่งช่วยให้คุณกำหนดค่าวิธีที่ Bundler แยกไฟล์ JavaScript โดย Webpack จะจดจำทั้งคำสั่ง import() แบบไดนามิกและ import แบบคงที่ คุณแก้ไขลักษณะการทำงานของ SplitChunksPlugin ได้โดยระบุตัวเลือก chunks ในการกำหนดค่า ดังนี้

  • chunks: async เป็นค่าเริ่มต้นและอ้างอิงถึงการเรียก import() แบบไดนามิก
  • chunks: initial หมายถึงการโทร import แบบคงที่
  • chunks: all ครอบคลุมทั้งการนำเข้า import() แบบไดนามิกและการนำเข้าแบบคงที่ ซึ่งช่วยให้คุณแชร์กลุ่มระหว่างการนำเข้า async ถึง initial ได้

โดยค่าเริ่มต้น เมื่อใดก็ตามที่ Webpack พบคำสั่ง import() แบบไดนามิก ระบบจะสร้างกลุ่มแยกต่างหากสำหรับโมดูลนั้น ดังนี้

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

การกำหนดค่า Webpack เริ่มต้นสำหรับข้อมูลโค้ดก่อนหน้าจะแบ่งเป็น 2 ส่วน ดังนี้

  • กลุ่ม main.js ซึ่ง Webpack จัดอยู่ในกลุ่ม initial ที่มีโมดูล main.js และ ./my-function.js
  • กลุ่ม async ซึ่งประกอบด้วย form-validation.js เท่านั้น (มีแฮชของไฟล์ในชื่อทรัพยากรหากกำหนดค่าไว้) ระบบจะดาวน์โหลดส่วนนี้ก็ต่อเมื่อ condition เชื่อถือได้เท่านั้น

การกำหนดค่านี้ช่วยให้คุณเลื่อนการโหลดกลุ่ม form-validation.js ได้จนกว่าจะจำเป็น ซึ่งจะช่วยปรับปรุงการตอบสนองของการโหลดได้โดยลดเวลาการประเมินสคริปต์ในระหว่างการโหลดหน้าเว็บครั้งแรก การดาวน์โหลดสคริปต์และการประเมินกลุ่ม form-validation.js จะเกิดขึ้นเมื่อตรงตามเงื่อนไขที่ระบุ ซึ่งในกรณีนี้ระบบจะดาวน์โหลดโมดูลที่นำเข้าแบบไดนามิก ตัวอย่างหนึ่งอาจเป็นเงื่อนไขที่มีการดาวน์โหลด Polyfill สำหรับเบราว์เซอร์บางอย่างเท่านั้น หรืออย่างในตัวอย่างก่อนหน้านี้ โมดูลที่นำเข้าจำเป็นสำหรับการโต้ตอบของผู้ใช้

ในทางกลับกัน การเปลี่ยนการกำหนดค่า SplitChunksPlugin เพื่อระบุ chunks: initial จะทำให้โค้ดแยกออกเป็นส่วนแรกเท่านั้น รายการเหล่านี้คือส่วนต่างๆ เช่น รายการที่นําเข้าแบบคงที่ หรือแสดงอยู่ในพร็อพเพอร์ตี้ entry ของ Webpack จากตัวอย่างก่อนหน้านี้ กลุ่มที่ได้จะเป็นชุดค่าผสม form-validation.js และ main.js ในไฟล์สคริปต์ไฟล์เดียว ซึ่งอาจส่งผลให้ประสิทธิภาพการโหลดหน้าเว็บเริ่มต้นแย่ลง

นอกจากนี้ ตัวเลือกสำหรับ SplitChunksPlugin ยังกำหนดค่าให้แยกสคริปต์ขนาดใหญ่ออกเป็นสคริปต์ขนาดเล็กหลายๆ สคริปต์ได้ด้วย เช่น โดยการใช้ตัวเลือก maxSize เพื่อสั่งให้ Webpack แยกส่วนออกเป็นไฟล์ย่อยๆ แยกกัน หากสคริปต์เหล่านั้นมีขนาดเกินที่ maxSize กำหนด การแบ่งไฟล์สคริปต์ขนาดใหญ่ออกเป็นไฟล์เล็กๆ ช่วยปรับปรุงการตอบสนองของการโหลดได้ เนื่องจากในบางกรณี งานการประเมินสคริปต์ที่ใช้ CPU มากจะแบ่งออกเป็นงานย่อยๆ ซึ่งมีโอกาสน้อยที่การบล็อกเทรดหลักจะทำงานเป็นเวลานานขึ้น

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

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

การสาธิต Webpack

การสาธิต Webpack SplitChunksPlugin

ทดสอบความรู้ของคุณ

คำสั่ง import ประเภทใดที่ใช้เมื่อทำการแยกโค้ด

import() แบบไดนามิก
ถูกต้องแล้ว!
importแบบคงที่
โปรดลองอีกครั้ง

คำสั่ง import ประเภทใดต้องอยู่ที่ด้านบนของโมดูล JavaScript และอยู่ในตำแหน่งอื่น

import() แบบไดนามิก
โปรดลองอีกครั้ง
importแบบคงที่
ถูกต้องแล้ว!

เมื่อใช้ SplitChunksPlugin ใน Webpack กลุ่ม async และกลุ่ม initial แตกต่างกันอย่างไร

ส่วน async โหลดโดยใช้ import() แบบไดนามิก และส่วน initial โหลดโดยใช้ import แบบคงที่
ถูกต้องแล้ว!
กลุ่ม async โหลดโดยใช้ import แบบคงที่ และ initial โหลดโดยใช้ import() แบบไดนามิก
โปรดลองอีกครั้ง

ถัดไป: รูปภาพการโหลดแบบ Lazy Loading และองค์ประกอบ <iframe>

แม้จะเป็นทรัพยากรประเภทที่มีราคาค่อนข้างแพง แต่ JavaScript ไม่ได้เป็นเพียงประเภททรัพยากรเดียวที่คุณสามารถเลื่อนการโหลดออกไปได้ องค์ประกอบรูปภาพและ <iframe> อาจเป็นทรัพยากรราคาแพงโดยตัวมันเอง เช่นเดียวกับ JavaScript คุณเลื่อนการโหลดรูปภาพและองค์ประกอบ <iframe> ได้โดยการโหลดแบบ Lazy Loading ซึ่งอธิบายไว้ในโมดูลถัดไปของหลักสูตรนี้