เมื่อโหลดสคริปต์ เบราว์เซอร์ต้องใช้เวลาในการประเมินสคริปต์ก่อนการดำเนินการ ซึ่งอาจทําให้งานใช้เวลานาน ดูวิธีการทํางานของการประเมินสคริปต์ และสิ่งที่ทําได้เพื่อป้องกันไม่ให้งานใช้เวลานานในระหว่างการโหลดหน้าเว็บ
เมื่อพูดถึงการเพิ่มประสิทธิภาพ Interaction to Next Paint (INP) คำแนะนำส่วนใหญ่ที่คุณจะได้พบคือการเพิ่มประสิทธิภาพการโต้ตอบด้วยตนเอง ตัวอย่างเช่น ในคู่มือเพิ่มประสิทธิภาพการทำงานที่ใช้เวลานาน จะมีการพูดคุยเกี่ยวกับเทคนิคต่างๆ เช่น ผลตอบแทนด้วย setTimeout
และอื่นๆ เทคนิคเหล่านี้มีประโยชน์เนื่องจากช่วยให้เทรดหลักมีพื้นที่หายใจ โดยการหลีกเลี่ยงงานที่ใช้เวลานาน ซึ่งอาจช่วยเพิ่มโอกาสให้การโต้ตอบและกิจกรรมอื่นๆ ทำงานเร็วขึ้น แทนที่จะต้องรองานเดียวที่ใช้เวลานาน
แต่หากเป็นงานที่ใช้เวลานานซึ่งมาจากการโหลดสคริปต์เอง งานเหล่านี้อาจรบกวนการโต้ตอบของผู้ใช้และส่งผลต่อ INP ของหน้าเว็บระหว่างการโหลด คู่มือนี้จะอธิบายวิธีที่เบราว์เซอร์จัดการงานที่เกิดจากการประเมินสคริปต์ และดูว่าคุณอาจทำอะไรได้บ้างเพื่อแบ่งงานการประเมินสคริปต์ เพื่อให้เทรดหลักตอบสนองต่อข้อมูลจากผู้ใช้ได้มากขึ้นในขณะที่ระบบกำลังโหลดหน้าเว็บ
การประเมินสคริปต์คืออะไร
หากคุณได้ทำโปรไฟล์แอปพลิเคชันที่ส่ง JavaScript จำนวนมาก คุณอาจเห็นงานที่ใช้เวลานานซึ่งมีป้ายกำกับประเมินสคริปต์
การประเมินสคริปต์เป็นส่วนที่จำเป็นสำหรับการเรียกใช้ JavaScript ในเบราว์เซอร์ เนื่องจาก JavaScript ได้รับการคอมไพล์ก่อนเวลาดำเนินการ เมื่อมีการประเมินสคริปต์ ระบบจะแยกวิเคราะห์ข้อผิดพลาดก่อน หากโปรแกรมแยกวิเคราะห์ไม่พบข้อผิดพลาด ระบบจะรวบรวมสคริปต์เป็นไบต์โค้ดเพื่อให้ดำเนินการต่อไปได้
การประเมินสคริปต์อาจเป็นปัญหาได้แม้จะจำเป็น เนื่องจากผู้ใช้อาจพยายามที่จะโต้ตอบกับหน้าเว็บหลังจากที่หน้าเว็บแสดงผลครั้งแรกไม่นาน อย่างไรก็ตาม การแสดงผลหน้าเว็บไม่ได้หมายความว่าหน้าเว็บโหลดเสร็จแล้ว การโต้ตอบที่เกิดขึ้นระหว่างการโหลดอาจล่าช้าเนื่องจากหน้าเว็บกำลังยุ่งอยู่กับการประเมินสคริปต์ แม้ว่าจะยังรับประกันไม่ได้ว่าการโต้ตอบจะเกิดขึ้น ณ เวลานี้ แต่เนื่องจากสคริปต์ที่มีการโต้ตอบอาจยังไม่ได้โหลด แต่ก็อาจมีการโต้ตอบที่ต้องอาศัย JavaScript ที่พร้อมใช้งาน หรือการโต้ตอบไม่ขึ้นอยู่กับ JavaScript เลย
ความสัมพันธ์ระหว่างสคริปต์กับงานที่ประเมินสคริปต์
วิธีเริ่มต้นการประเมินสคริปต์จะขึ้นอยู่กับว่าสคริปต์ที่คุณกำลังโหลดนั้นโหลดด้วยองค์ประกอบ <script>
ทั่วไป หรือสคริปต์เป็นโมดูลที่โหลดด้วย type=module
เนื่องจากเบราว์เซอร์มีแนวโน้มที่จะจัดการกับสิ่งต่างๆ ที่แตกต่างกันออกไป ดังนั้นเครื่องมือหลักของเบราว์เซอร์ในการจัดการการประเมินสคริปต์จึงขึ้นอยู่กับลักษณะการทำงานของการประเมินสคริปต์ในแต่ละเบราว์เซอร์ที่ต่างกัน
สคริปต์ที่โหลดด้วยองค์ประกอบ <script>
โดยทั่วไปจำนวนงานที่ส่งไปประเมินสคริปต์จะมีความสัมพันธ์โดยตรงกับจำนวนองค์ประกอบ <script>
ในหน้าเว็บ องค์ประกอบ <script>
แต่ละรายการจะเริ่มต้นงานเพื่อประเมินสคริปต์ที่ขอเพื่อแยกวิเคราะห์ คอมไพล์ และดำเนินการได้ ในกรณีเช่นนี้สำหรับเบราว์เซอร์ใน Chromium, Safari และ Firefox
ทำไมสิ่งนี้จึงสำคัญ สมมติว่าคุณใช้ Bundler จัดการสคริปต์การผลิต และกำหนดค่าเพื่อรวมทุกอย่างที่หน้าเว็บของคุณจำเป็นต้องใช้ไว้ในสคริปต์เดียว หากเว็บไซต์ของคุณเป็นกรณีนี้ คาดว่าจะมีงานหนึ่งถูกส่งออกไปเพื่อประเมินสคริปต์นั้น ไม่ทราบว่าคุณสะดวกไหม ไม่จำเป็น เว้นแต่สคริปต์นั้นจะมีขนาดใหญ่
คุณสามารถแบ่งงานการประเมินสคริปต์โดยหลีกเลี่ยงการโหลด JavaScript กลุ่มใหญ่ และโหลดสคริปต์ขนาดเล็กลงให้เหมาะกับแต่ละบุคคลมากขึ้นโดยใช้องค์ประกอบ <script>
เพิ่มเติม
แม้ว่าคุณควรพยายามโหลด JavaScript ให้น้อยที่สุดเท่าที่จะเป็นไปได้ในระหว่างการโหลดหน้าเว็บเสมอ แต่การแยกสคริปต์จะช่วยให้มั่นใจได้ว่า แทนที่จะทำงานใหญ่ๆ เดียวที่อาจบล็อกเทรดหลัก คุณจะมีงานเล็กๆ จำนวนมากขึ้นที่จะไม่บล็อกเทรดหลักเลย หรืออย่างน้อยน้อยกว่าที่คุณเริ่มไว้
คุณอาจมองว่าการแบ่งงานสำหรับการประเมินสคริปต์นั้นค่อนข้างคล้ายกับการตอบกลับระหว่างการเรียกกลับของเหตุการณ์ซึ่งเกิดขึ้นระหว่างการโต้ตอบ อย่างไรก็ตาม การประเมินสคริปต์จะแบ่ง JavaScript ที่คุณโหลดออกเป็นสคริปต์ขนาดเล็กหลายๆ สคริปต์ แทนที่จะเป็นสคริปต์ขนาดใหญ่กว่าที่มีแนวโน้มที่จะบล็อกเทรดหลักมากกว่า
สคริปต์ที่โหลดด้วยองค์ประกอบ <script>
และแอตทริบิวต์ type=module
ตอนนี้คุณสามารถโหลดโมดูล ES แบบเนทีฟในเบราว์เซอร์ได้แล้วด้วยแอตทริบิวต์ type=module
ในองค์ประกอบ <script>
การโหลดสคริปต์วิธีนี้ให้ประโยชน์กับประสบการณ์การใช้งานของนักพัฒนาซอฟต์แวร์บางประการ เช่น ไม่ต้องแปลงโค้ดเพื่อการใช้งานจริง โดยเฉพาะเมื่อใช้ร่วมกับแผนที่นำเข้า อย่างไรก็ตาม การโหลดสคริปต์ในลักษณะนี้จะกำหนดเวลางานของแต่ละเบราว์เซอร์ที่แตกต่างกัน
เบราว์เซอร์ที่ใช้ Chromium
ในเบราว์เซอร์อย่างเช่น Chrome หรือเบราว์เซอร์ที่มาจากเบราว์เซอร์ การโหลดโมดูล ES โดยใช้แอตทริบิวต์ type=module
จะสร้างงานที่แตกต่างจากที่คุณเห็นตามปกติเมื่อไม่ได้ใช้ type=module
เช่น งานสำหรับสคริปต์ของโมดูลแต่ละรายการจะทำงานซึ่งเกี่ยวข้องกับกิจกรรมที่มีป้ายกำกับว่าคอมไพล์โมดูล
เมื่อคอมไพล์โมดูลแล้ว โค้ดใดๆ ที่ทำงานในโมดูลในภายหลังจะเริ่มต้นกิจกรรมที่มีป้ายกำกับประเมินโมดูล
ผลกระทบในที่นี้ (เป็นอย่างน้อย) ใน Chrome และเบราว์เซอร์ที่เกี่ยวข้อง คือขั้นตอนการคอมไพล์จะถูกตัดรายละเอียดเมื่อใช้โมดูล ES นี่เป็นชัยชนะที่ชัดเจนในแง่ของการจัดการงานที่ใช้เวลานาน แต่ผลการประเมินโมดูลที่ได้ผลลัพธ์ยังหมายความว่าคุณจะมีค่าใช้จ่ายที่หลีกเลี่ยงไม่ได้ แม้ว่าคุณควรพยายามจัดส่ง JavaScript ให้น้อยที่สุดเท่าที่จะทำได้ แต่การใช้โมดูล ES ไม่ว่าจะใช้เบราว์เซอร์ใดก็ตาม ก็ให้ประโยชน์ดังต่อไปนี้
- โค้ดโมดูลทั้งหมดจะทำงานในโหมดเข้มงวดโดยอัตโนมัติ ซึ่งทำให้เครื่องมือ JavaScript มีโอกาสเพิ่มประสิทธิภาพ แต่ไม่สามารถทำได้ในบริบทที่ไม่เข้มงวด
- สคริปต์ที่โหลดโดยใช้
type=module
จะได้รับการปฏิบัติเสมือนว่าเป็นการเลื่อนโดยค่าเริ่มต้น คุณอาจใช้แอตทริบิวต์async
ในสคริปต์ที่โหลดด้วยtype=module
เพื่อเปลี่ยนลักษณะการทำงานนี้
Safari และ Firefox
เมื่อโหลดโมดูลใน Safari และ Firefox แต่ละโมดูลจะได้รับการประเมินในงานแยกกัน ซึ่งหมายความว่าโดยหลักแล้ว คุณอาจโหลดโมดูลระดับบนสุด 1 โมดูลที่มีเพียงคำสั่ง import
แบบคงที่ไปยังโมดูลอื่นๆ ได้ และทุกโมดูลที่โหลดจะมีคำขอเครือข่ายและงานแยกต่างหากเพื่อทำการประเมิน
สคริปต์โหลดด้วย import()
แบบไดนามิกแล้ว
ไดนามิก import()
เป็นอีกวิธีการหนึ่งสำหรับการโหลดสคริปต์ การเรียก import()
แบบไดนามิกจะปรากฏที่ใดก็ได้ในสคริปต์เพื่อโหลดกลุ่ม JavaScript ออนดีมานด์ ซึ่งต่างจากคำสั่ง import
แบบคงที่ซึ่งจำเป็นต้องอยู่ที่ด้านบนของโมดูล ES เทคนิคนี้เรียกว่าการแยกโค้ด
import()
แบบไดนามิกมีข้อดี 2 ข้อในการปรับปรุง INP ดังนี้
- โมดูลที่ถูกเลื่อนให้โหลดในภายหลังจะลดการช่วงชิงเทรดหลักระหว่างการเริ่มต้นใช้งานด้วยการลดจำนวน JavaScript ที่โหลดในเวลานั้น วิธีนี้จะทำให้เทรดหลักว่างมากขึ้นเพื่อให้ตอบสนองต่อการโต้ตอบของผู้ใช้ได้ดีขึ้น
- เมื่อมีการเรียกใช้
import()
แบบไดนามิก การเรียกใช้แต่ละครั้งจะแยกการรวบรวมและการประเมินของแต่ละโมดูลออกเป็นงานของตนเองอย่างมีประสิทธิภาพ แน่นอนว่าimport()
แบบไดนามิกที่โหลดโมดูลขนาดใหญ่มากจะเริ่มต้นงานการประเมินสคริปต์ที่ค่อนข้างใหญ่ และอาจรบกวนความสามารถของเทรดหลักในการตอบกลับอินพุตของผู้ใช้ หากการโต้ตอบเกิดขึ้นพร้อมกับการเรียกใช้import()
แบบไดนามิก ดังนั้น สิ่งสำคัญก็คือคุณต้องโหลด JavaScript ให้น้อยที่สุดเท่าที่จะทำได้
การเรียกใช้ import()
แบบไดนามิกจะทำงานคล้ายกันในเครื่องมือเบราว์เซอร์หลักทั้งหมด กล่าวคือ งานการประเมินสคริปต์ที่ได้ผลลัพธ์จะเท่ากับจำนวนโมดูลที่นำเข้าแบบไดนามิก
สคริปต์ที่โหลดใน Web Worker
ผู้ปฏิบัติงานบนเว็บเป็นกรณีการใช้งาน JavaScript พิเศษ ผู้ปฏิบัติงานบนเว็บจะได้รับการลงทะเบียนในเทรดหลัก จากนั้นโค้ดภายในผู้ปฏิบัติงานจะทำงานบนเทรดของตัวเอง ซึ่งมีประโยชน์อย่างมากในแง่ที่ว่า โค้ดที่ใช้ลงทะเบียน Web Worker จะทำงานบนเทรดหลัก แต่โค้ดภายใน Web Work ไม่สามารถใช้งานได้ ซึ่งจะช่วยลดความคับคั่งของชุดข้อความหลัก และช่วยให้ชุดข้อความหลักตอบสนองต่อการโต้ตอบของผู้ใช้ได้ดียิ่งขึ้น
นอกจากการลดการทำงานของเทรดหลักแล้ว ผู้ปฏิบัติงานบนเว็บยังโหลดสคริปต์ภายนอกเพื่อโหลดสคริปต์ภายนอกที่จะใช้ในบริบทของผู้ปฏิบัติงานได้ ผ่านคำสั่ง importScripts
หรือแบบคงที่ import
ในเบราว์เซอร์ที่รองรับผู้ปฏิบัติงานโมดูล ผลลัพธ์คือสคริปต์ที่ผู้ปฏิบัติงานเว็บขอจะได้รับการประเมินออกจากเทรดหลัก
ข้อดีและข้อควรพิจารณา
แม้ว่าการแบ่งสคริปต์ของคุณออกเป็นไฟล์ย่อยๆ ไฟล์ขนาดเล็กจะช่วยจำกัดงานที่ใช้เวลานาน แทนที่จะต้องโหลดไฟล์จำนวนน้อยลงแต่มีขนาดใหญ่ขึ้นมาก สิ่งสำคัญคือต้องคำนึงถึงสิ่งต่อไปนี้ด้วยเมื่อตัดสินใจว่าจะแยกสคริปต์อย่างไร
ประสิทธิภาพการบีบอัด
การบีบอัดเป็นปัจจัยในการแยกสคริปต์ เมื่อสคริปต์มีขนาดเล็กลง การบีบอัดจะมีประสิทธิภาพน้อยลง สคริปต์ขนาดใหญ่กว่าจะได้รับประโยชน์จากการบีบอัดมากกว่า แม้ว่าการเพิ่มประสิทธิภาพในการบีบอัดจะช่วยให้โหลดสคริปต์น้อยที่สุดเท่าที่จะเป็นไปได้ แต่ก็ยังมีการสร้างความสมดุลเล็กน้อยเพื่อแบ่งสคริปต์ออกเป็นส่วนย่อยๆ ที่มากพอเพื่อให้การโต้ตอบดีขึ้นในช่วงเริ่มต้น
Bundler เป็นเครื่องมือที่เหมาะสำหรับการจัดการขนาดเอาต์พุตสำหรับสคริปต์ที่เว็บไซต์ใช้ ดังนี้
- หากกังวลเรื่อง Webpack ปลั๊กอิน
SplitChunksPlugin
ของ Webpack สามารถช่วยคุณได้ ดูเอกสารประกอบเกี่ยวกับSplitChunksPlugin
เพื่อดูตัวเลือกที่คุณตั้งค่าเพื่อช่วยจัดการขนาดชิ้นงานได้ - สำหรับ Bundler อื่นๆ เช่น Rollup และ esbuild คุณจัดการขนาดไฟล์สคริปต์ได้โดยใช้การเรียก
import()
แบบไดนามิกในโค้ดของคุณ Bundler เหล่านี้รวมถึง Webpack จะแยกเนื้อหาที่นำเข้าแบบไดนามิกเป็นไฟล์ของตัวเองโดยอัตโนมัติ ซึ่งจะหลีกเลี่ยงขนาด Bundle เริ่มต้นที่ใหญ่กว่า
การทำให้แคชใช้งานไม่ได้
การเอาข้อมูลเก่าในแคชออกเป็นส่วนสำคัญในการเพิ่มความเร็วในการโหลดหน้าเว็บเมื่อเข้าชมซ้ำ ในการจัดส่งแพ็กเกจสคริปต์แบบโมโนลิธจำนวนมาก คุณจะเสียเปรียบในการแคชเบราว์เซอร์ ทั้งนี้เนื่องจากเมื่ออัปเดตโค้ดของบุคคลที่หนึ่ง ไม่ว่าจะผ่านการอัปเดตแพ็กเกจหรือการแก้ไขข้อบกพร่องในการจัดส่ง แพ็กเกจทั้งหมดจะใช้งานไม่ได้และต้องดาวน์โหลดอีกครั้ง
การแบ่งสคริปต์ไม่เพียงแต่แบ่งงานการประเมินสคริปต์ออกเป็นงานย่อยๆ เท่านั้น แต่ยังเพิ่มแนวโน้มที่ผู้เข้าชมที่กลับมาจะดึงสคริปต์จากแคชของเบราว์เซอร์มากกว่าการทำงานจากเครือข่าย ซึ่งจะทำให้หน้าเว็บโหลดได้เร็วขึ้นโดยรวม
โมดูลที่ฝังและประสิทธิภาพการโหลด
หากจัดส่งโมดูล ES เป็นเวอร์ชันที่ใช้งานจริงและโหลดโมดูลดังกล่าวด้วยแอตทริบิวต์ type=module
คุณต้องทราบว่าการฝังโมดูลจะส่งผลต่อเวลาเริ่มต้นอย่างไร การซ้อนโมดูลหมายถึงเมื่อโมดูล ES นำเข้าโมดูล ES อื่นแบบคงที่ที่นำเข้าโมดูล ES อื่นแบบคงที่ ดังนี้
// a.js
import {b} from './b.js';
// b.js
import {c} from './c.js';
หากโมดูล ES ไม่ได้รวมกลุ่มไว้ด้วยกัน รหัสก่อนหน้าจะส่งผลให้เกิดห่วงโซ่คำขอเครือข่าย: เมื่อมีการขอ a.js
จากองค์ประกอบ <script>
จะมีการส่งคำขอเครือข่ายอื่นสำหรับ b.js
ซึ่งจะรวมถึงคำขออื่นสำหรับ c.js
วิธีหนึ่งในการหลีกเลี่ยงปัญหานี้คือการใช้ Bundler แต่คุณต้องกำหนดค่า Bundler เพื่อแยกสคริปต์เพื่อกระจายงานการประเมินสคริปต์
หากไม่ต้องการใช้ Bundler อีกวิธีหนึ่งในการหลีกเลี่ยงการเรียกใช้โมดูลที่ซ้อนกันคือการใช้คำแนะนำทรัพยากร modulepreload
ซึ่งจะโหลดโมดูล ES ล่วงหน้าล่วงหน้าเพื่อหลีกเลี่ยงเชนคำขอเครือข่าย
บทสรุป
การเพิ่มประสิทธิภาพการประเมินสคริปต์ในเบราว์เซอร์เป็นงานที่ยุ่งยาก วิธีการจะขึ้นอยู่กับข้อกำหนดและข้อจำกัดของเว็บไซต์ อย่างไรก็ตาม การแยกสคริปต์ออกเป็นการกระจายงานในการประเมินสคริปต์ไปยังงานเล็กๆ จำนวนมาก และทำให้เทรดหลักสามารถจัดการการโต้ตอบของผู้ใช้ได้อย่างมีประสิทธิภาพมากขึ้น แทนที่จะบล็อกเทรดหลัก
เพื่อเป็นการสรุป ต่อไปนี้คือสิ่งที่คุณสามารถทำได้เพื่อแบ่งงานการประเมินสคริปต์ขนาดใหญ่
- เมื่อโหลดสคริปต์โดยใช้องค์ประกอบ
<script>
ที่ไม่มีแอตทริบิวต์type=module
ให้หลีกเลี่ยงการโหลดสคริปต์ที่มีขนาดใหญ่มาก เนื่องจากสคริปต์เหล่านี้จะเริ่มต้นงานการประเมินสคริปต์ที่ใช้ทรัพยากรจำนวนมากซึ่งบล็อกเทรดหลัก กระจายสคริปต์ผ่านองค์ประกอบ<script>
อื่นๆ เพื่อแบ่งการดำเนินการนี้ - การใช้แอตทริบิวต์
type=module
เพื่อโหลดโมดูล ES แบบดั้งเดิมในเบราว์เซอร์จะเริ่มต้นงานเดี่ยวสำหรับการประเมินสคริปต์โมดูลที่แยกกัน - ลดขนาดของแพ็กเกจเริ่มต้นโดยใช้การเรียก
import()
แบบไดนามิก วิธีนี้ยังใช้ได้กับ Bundler อีกด้วย เนื่องจาก Bundler จะถือว่าโมดูลที่นำเข้าแบบไดนามิกแต่ละรายการเป็น "จุดแยก" ส่งผลให้เกิดการสร้างสคริปต์แยกต่างหากสำหรับโมดูลที่นำเข้าแบบไดนามิกแต่ละโมดูล - อย่าลืมพิจารณาข้อดีข้อเสีย เช่น ประสิทธิภาพการบีบอัดและการเลิกใช้แคช สคริปต์ขนาดใหญ่กว่าจะบีบอัดได้ดีกว่า แต่มีแนวโน้มที่จะทำให้การประเมินสคริปต์มีราคาแพงกว่าในงานจำนวนน้อยลง และส่งผลให้แคชของเบราว์เซอร์ใช้งานไม่ได้ ซึ่งจะทำให้ประสิทธิภาพการแคชโดยรวมลดลง
- หากใช้โมดูล ES แบบเนทีฟโดยไม่มีการรวมกลุ่ม ให้ใช้คำแนะนำเกี่ยวกับทรัพยากร
modulepreload
เพื่อเพิ่มประสิทธิภาพการโหลดโมดูลดังกล่าวในช่วงเริ่มต้นใช้งาน - และเช่นเคย โปรดจัดส่ง JavaScript ให้น้อยที่สุดเท่าที่จะทำได้
แน่นอนว่านี่เป็นการทำงานที่สมดุล แต่เมื่อแยกสคริปต์และลดเปย์โหลดเริ่มต้นด้วย import()
แบบไดนามิก คุณจะสามารถเพิ่มประสิทธิภาพในการเริ่มต้นทำงานได้ดียิ่งขึ้นและรองรับการโต้ตอบของผู้ใช้ในช่วงเริ่มต้นที่สำคัญนี้มากขึ้น ซึ่งจะช่วยให้คุณได้คะแนน INP ได้ดีขึ้น และส่งผลให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดีขึ้น
รูปภาพหลักจาก Unsplash โดย Markus Spiske