เมื่อโหลดสคริปต์ เบราว์เซอร์ต้องใช้เวลาในการประเมินสคริปต์ก่อนดำเนินการ ซึ่งอาจทําให้งานใช้เวลานาน ดูวิธีการทำงานของการประเมินสคริปต์ และสิ่งที่คุณทําได้เพื่อไม่ให้การประเมินทําให้งานใช้เวลานานระหว่างการโหลดหน้าเว็บ
เมื่อพูดถึงการเพิ่มประสิทธิภาพ Interaction to Next Paint (INP) คำแนะนำส่วนใหญ่ที่คุณจะเห็นคือการเพิ่มประสิทธิภาพการโต้ตอบ เช่น ในคู่มือเพิ่มประสิทธิภาพงานระยะยาว จะมีเทคนิคต่างๆ เช่น การแสดงผลด้วย setTimeout
และอื่นๆ เทคนิคเหล่านี้มีประโยชน์เนื่องจากช่วยให้เธรดหลักมีเวลาหายใจโดยหลีกเลี่ยงงานที่ใช้เวลานาน ซึ่งจะเพิ่มโอกาสในการโต้ตอบและกิจกรรมอื่นๆ ให้ทำงานได้เร็วขึ้น แทนที่จะต้องรองานเดียวที่ใช้เวลานาน
แต่จะเกิดอะไรขึ้นกับงานที่ใช้เวลานานซึ่งมาจากการโหลดสคริปต์เอง งานที่กล่าวมาข้างต้นอาจรบกวนการโต้ตอบของผู้ใช้และส่งผลต่อ INP ของหน้าเว็บระหว่างการโหลด คู่มือนี้จะอธิบายวิธีที่เบราว์เซอร์จัดการงานที่เริ่มต้นด้วยการประเมินสคริปต์ และดูสิ่งที่คุณอาจทำได้เพื่อแบ่งงานการประเมินสคริปต์เพื่อให้เธรดหลักตอบสนองต่ออินพุตของผู้ใช้ได้มากขึ้นขณะที่หน้าเว็บกำลังโหลด
การประเมินสคริปต์คืออะไร
หากคุณโปรไฟล์แอปพลิเคชันที่มาพร้อมกับ JavaScript จำนวนมาก คุณอาจเห็นงานที่ใช้เวลานานซึ่งมีการติดป้ายกำกับว่าประเมินสคริปต์
การประเมินสคริปต์เป็นส่วนสําคัญของการดำเนินการ JavaScript ในเบราว์เซอร์ เนื่องจาก JavaScript ได้รับการคอมไพล์ทันเวลาก่อนการดําเนินการ เมื่อประเมินสคริปต์ ระบบจะแยกวิเคราะห์เพื่อหาข้อผิดพลาดก่อน หากโปรแกรมแยกวิเคราะห์ไม่พบข้อผิดพลาด ระบบจะคอมไพล์สคริปต์เป็น ไบต์โค้ด แล้วดําเนินการต่อ
แม้ว่าการประเมินสคริปต์จะเป็นสิ่งจําเป็น แต่อาจก่อให้เกิดปัญหาได้ เนื่องจากผู้ใช้อาจพยายามโต้ตอบกับหน้าเว็บไม่นานหลังจากที่หน้าเว็บแสดงผลครั้งแรก อย่างไรก็ตาม การที่หน้าเว็บแสดงผลไม่ได้หมายความว่าหน้าเว็บโหลดเสร็จแล้ว การโต้ตอบที่เกิดขึ้นระหว่างการโหลดอาจล่าช้าเนื่องจากหน้าเว็บกําลังประเมินสคริปต์ แม้ว่าจะไม่มีการรับประกันว่าจะมีการโต้ตอบเกิดขึ้น ณ เวลานี้ เนื่องจากสคริปต์ที่รับผิดชอบอาจยังไม่ได้โหลด แต่อาจมีการโต้ตอบที่ขึ้นอยู่กับ JavaScript ซึ่งพร้อมแล้ว หรืออาจไม่มีการโต้ตอบที่ขึ้นอยู่กับ JavaScript เลย
ความสัมพันธ์ระหว่างสคริปต์กับงานที่ประเมิน
วิธีที่ระบบเริ่มงานที่มีส่วนรับผิดชอบในการประเมินสคริปต์จะขึ้นอยู่กับว่าสคริปต์ที่คุณโหลดนั้นโหลดมาพร้อมกับองค์ประกอบ <script>
ทั่วไป หรือสคริปต์เป็นโมดูลที่โหลดมาพร้อมกับ type=module
เนื่องจากเบราว์เซอร์มีแนวโน้มที่จะจัดการสิ่งต่างๆ แตกต่างกัน เราจะพูดถึงวิธีที่เครื่องมือเบราว์เซอร์หลักจัดการการประเมินสคริปต์ซึ่งลักษณะการทํางานของการประเมินสคริปต์จะแตกต่างกันไป
สคริปต์ที่โหลดด้วยองค์ประกอบ <script>
โดยทั่วไปแล้ว จำนวนงานที่ส่งไปประเมินสคริปต์จะมีความสัมพันธ์โดยตรงกับจํานวนองค์ประกอบ <script>
ในหน้าเว็บ องค์ประกอบ <script>
แต่ละรายการจะเริ่มต้นงานเพื่อประเมินสคริปต์ที่ขอเพื่อให้สามารถแยกวิเคราะห์ คอมไพล์ และดำเนินการได้ กรณีนี้เกิดขึ้นกับเบราว์เซอร์ที่ใช้ Chromium, Safari และ Firefox
ทำไมสิ่งนี้จึงสำคัญ สมมติว่าคุณใช้เครื่องมือรวมเพื่อจัดการสคริปต์เวอร์ชันที่ใช้งานจริง และคุณกําหนดค่าให้รวมทุกอย่างที่หน้าเว็บจําเป็นต้องใช้ไว้ในสคริปต์เดียว หากเป็นเช่นนั้นกับเว็บไซต์ของคุณ คุณก็คาดได้ว่าจะมีการส่งงานเดียวเพื่อประเมินสคริปต์นั้น การทำเช่นนี้ไม่ดีใช่ไหม ไม่จำเป็น เว้นแต่ว่าสคริปต์นั้นจะมีขนาดใหญ่มาก
คุณสามารถแบ่งงานการประเมินสคริปต์ได้โดยหลีกเลี่ยงการโหลด JavaScript จำนวนมาก และโหลดสคริปต์ขนาดเล็กๆ แต่ละรายการเพิ่มเติมโดยใช้องค์ประกอบ <script>
เพิ่มเติม
แม้ว่าคุณควรพยายามโหลด JavaScript ให้น้อยที่สุดเสมอในระหว่างการโหลดหน้าเว็บ แต่การแยกสคริปต์จะช่วยให้คุณมีงานขนาดเล็กจำนวนมากขึ้นที่จะไม่บล็อกเธรดหลักเลย หรืออย่างน้อยก็น้อยกว่าจำนวนงานที่เริ่มต้น
คุณสามารถมองว่าการแบ่งงานสําหรับการประเมินสคริปต์นั้นคล้ายกับการหยุดแสดงผลระหว่างการเรียกเหตุการณ์ซ้ำที่ทํางานระหว่างการโต้ตอบ อย่างไรก็ตาม เมื่อใช้การประเมินสคริปต์ กลไกการสร้างรายได้จะแบ่ง JavaScript ที่คุณโหลดออกเป็นสคริปต์ขนาดเล็กหลายรายการ แทนที่จะเป็นสคริปต์ขนาดใหญ่จำนวนน้อยที่มีแนวโน้มจะบล็อกเธรดหลัก
สคริปต์ที่โหลดด้วยองค์ประกอบ <script>
และแอตทริบิวต์ type=module
ตอนนี้คุณโหลดโมดูล ES ในเบราว์เซอร์ได้โดยตรงด้วยแอตทริบิวต์ type=module
ในองค์ประกอบ <script>
วิธีการโหลดสคริปต์นี้มีข้อดีบางอย่างสำหรับนักพัฒนาซอฟต์แวร์ เช่น ไม่ต้องเปลี่ยนรูปแบบโค้ดเพื่อใช้งานในเวอร์ชันที่ใช้งานจริง โดยเฉพาะเมื่อใช้ร่วมกับแผนที่การนําเข้า อย่างไรก็ตาม การโหลดสคริปต์ด้วยวิธีนี้จะกำหนดเวลางานที่แตกต่างกันไปในแต่ละเบราว์เซอร์
เบราว์เซอร์ที่ใช้ Chromium
ในเบราว์เซอร์อย่าง Chrome หรือเบราว์เซอร์ที่มาจาก Chrome การโหลดโมดูล ES โดยใช้แอตทริบิวต์ type=module
จะสร้างงานประเภทต่างๆ แตกต่างจากที่คุณเห็นตามปกติเมื่อไม่ได้ใช้ type=module
เช่น งานสําหรับสคริปต์แต่ละข้อบังคับจะทํางานที่เกี่ยวข้องกับกิจกรรมที่ติดป้ายกํากับว่าคอมไพล์ข้อบังคับ
เมื่อคอมไพล์โมดูลแล้ว โค้ดที่ทำงานในโมดูลนั้นๆ จะเริ่มต้นกิจกรรมที่ติดป้ายกํากับว่าประเมินโมดูล
ผลที่เกิดขึ้นใน Chrome และเบราว์เซอร์ที่เกี่ยวข้องอย่างน้อยคือขั้นตอนคอมไพล์จะแบ่งออกเป็นหลายขั้นตอนเมื่อใช้โมดูล ES การดำเนินการนี้ถือเป็นข้อได้เปรียบที่ชัดเจนในด้านการจัดการงานที่ใช้เวลานาน อย่างไรก็ตาม การประเมินข้อบังคับที่ตามมาจะยังคงทำให้คุณมีค่าใช้จ่ายบางอย่างที่หลีกเลี่ยงไม่ได้ แม้ว่าคุณควรพยายามรวม JavaScript ให้ได้น้อยที่สุด แต่การใช้โมดูล ES ไม่ว่าจะในเบราว์เซอร์ใดก็ตามจะให้ประโยชน์ต่อไปนี้
- โค้ดโมดูลทั้งหมดจะทำงานในโหมดที่เข้มงวดโดยอัตโนมัติ ซึ่งช่วยให้เครื่องมือ JavaScript สามารถเพิ่มประสิทธิภาพได้ ซึ่งไม่สามารถทำได้ในบริบทที่ไม่เข้มงวด
- ระบบจะถือว่าสคริปต์ที่โหลดโดยใช้
type=module
เป็นสคริปต์ที่เลื่อนไว้โดยค่าเริ่มต้น คุณสามารถใช้แอตทริบิวต์async
ในสคริปต์ที่โหลดด้วยtype=module
เพื่อเปลี่ยนลักษณะการทำงานนี้ได้
Safari และ Firefox
เมื่อโหลดโมดูลใน Safari และ Firefox ระบบจะประเมินแต่ละโมดูลในภารกิจแยกกัน ซึ่งหมายความว่าในทางทฤษฎี คุณสามารถโหลดโมดูลระดับบนสุดโมดูลเดียวที่มีเฉพาะคำสั่ง static import
ไปยังโมดูลอื่นๆ และโมดูลที่โหลดทุกโมดูลจะทำให้เกิดคำขอเครือข่ายและงานแยกต่างหากเพื่อประเมิน
สคริปต์ที่โหลดด้วย import()
แบบไดนามิก
import()
แบบไดนามิกเป็นวิธีอื่นในการโหลดสคริปต์ การเรียก import()
แบบไดนามิกสามารถปรากฏที่ใดก็ได้ในสคริปต์เพื่อโหลด JavaScript บางส่วนตามคําขอ ซึ่งต่างจากคำสั่ง import
แบบคงที่ที่ต้องอยู่ด้านบนของโมดูล ES เทคนิคนี้เรียกว่าการแยกโค้ด
import()
แบบไดนามิกมีข้อดี 2 ข้อในการปรับปรุง INP ดังนี้
- โมดูลที่เลื่อนเวลาการโหลดไปไว้ภายหลังจะลดการแย่งชิงของเธรดหลักระหว่างการเริ่มต้นระบบโดยการลดจํานวน JavaScript ที่โหลดในเวลานั้น ซึ่งจะช่วยเพิ่มพื้นที่ว่างของเธรดหลักเพื่อให้ตอบสนองต่อการโต้ตอบของผู้ใช้ได้ดียิ่งขึ้น
- เมื่อมีการเรียกใช้
import()
แบบไดนามิก การเรียกแต่ละครั้งจะแยกการคอมไพล์และการประเมินแต่ละโมดูลออกเป็นงานของตัวเองอย่างมีประสิทธิภาพ แน่นอนว่าimport()
แบบไดนามิกที่โหลดโมดูลขนาดใหญ่มากจะเริ่มต้นการทํางานในการประเมินสคริปต์ที่ค่อนข้างใหญ่ และอาจรบกวนความสามารถของเธรดหลักในการตอบสนองต่ออินพุตของผู้ใช้หากการโต้ตอบเกิดขึ้นพร้อมกันกับการเรียกใช้import()
แบบไดนามิก ดังนั้น คุณจึงควรโหลด JavaScript ให้น้อยที่สุด
การเรียกใช้ import()
แบบไดนามิกจะทํางานคล้ายกันในเครื่องมือเบราว์เซอร์หลักทั้งหมด โดยงานการประเมินสคริปต์ที่แสดงผลจะเหมือนกับจํานวนโมดูลที่นําเข้าแบบไดนามิก
สคริปต์ที่โหลดในเว็บเวิร์กเกอร์
เวิร์กเกอร์เว็บเป็นกรณีการใช้งาน JavaScript พิเศษ ระบบจะลงทะเบียนเวิร์กเกอร์เว็บในเธรดหลัก จากนั้นโค้ดภายในเวิร์กเกอร์จะทำงานในเธรดของตัวเอง ซึ่งมีประโยชน์อย่างมากในแง่ที่ว่าในขณะที่โค้ดที่ลงทะเบียน Web Worker ทำงานบนเธรดหลัก โค้ดภายใน Web Worker จะไม่ทำงาน วิธีนี้ช่วยลดความแออัดของเธรดหลัก และช่วยให้เธรดหลักตอบสนองต่อการโต้ตอบของผู้ใช้ได้ดียิ่งขึ้น
นอกจากการลดการทำงานของชุดข้อความหลักแล้ว เว็บเวิร์กเกอร์เองยังโหลดสคริปต์ภายนอกเพื่อใช้ในบริบทเวิร์กเกอร์ได้ ไม่ว่าจะผ่านคำสั่ง importScripts
หรือ import
แบบคงที่ในเบราว์เซอร์ที่รองรับเวิร์กเกอร์โมดูล ผลที่ได้คือสคริปต์ที่เว็บเวิร์กเกอร์ขอจะได้รับการประเมินนอกเธรดหลัก
ข้อดีและข้อเสียที่ต้องพิจารณา
แม้ว่าการแยกสคริปต์ออกเป็นไฟล์ขนาดเล็กๆ แยกกันจะช่วยจำกัดงานที่ใช้เวลานานได้ แทนที่จะโหลดไฟล์ขนาดใหญ่จำนวนน้อยลง แต่คุณก็ควรพิจารณาบางสิ่งเมื่อตัดสินใจว่าจะแยกสคริปต์อย่างไร
ประสิทธิภาพการบีบอัด
การบีบอัดเป็นปัจจัยในการแบ่งสคริปต์ เมื่อสคริปต์มีขนาดเล็กลง การบีบอัดจะมีประสิทธิภาพน้อยลง สคริปต์ขนาดใหญ่จะได้ประโยชน์จากการบีบอัดมากกว่า แม้ว่าการเพิ่มประสิทธิภาพการบีบอัดจะช่วยทำให้เวลาในการโหลดสคริปต์ต่ำที่สุดเท่าที่จะเป็นไปได้ แต่คุณก็ควรแบ่งสคริปต์ออกเป็นชิ้นเล็กๆ มากพอที่จะทําให้การโต้ตอบระหว่างผู้ใช้กับเว็บไซต์ดีขึ้นในระหว่างการเริ่มต้น
เครื่องมือรวมเป็นเครื่องมือที่เหมาะสําหรับจัดการขนาดเอาต์พุตสําหรับสคริปต์ที่เว็บไซต์ของคุณใช้อยู่
- สําหรับ webpack นั้น ปลั๊กอิน
SplitChunksPlugin
จะช่วยได้ โปรดดูตัวเลือกที่คุณตั้งค่าได้เพื่อช่วยจัดการขนาดชิ้นงานในเอกสารประกอบของSplitChunksPlugin
- สำหรับเครื่องมือรวมอื่นๆ เช่น Rollup และ esbuild คุณสามารถจัดการขนาดไฟล์สคริปต์ได้โดยใช้การเรียก
import()
แบบไดนามิกในโค้ด เครื่องมือจัดกลุ่มเหล่านี้และ 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
วิธีหนึ่งในการหลีกเลี่ยงปัญหานี้คือการใช้เครื่องมือรวม แต่อย่าลืมกําหนดค่าเครื่องมือรวมให้แบ่งสคริปต์เพื่อกระจายงานการประเมินสคริปต์
หากไม่ต้องการใช้เครื่องมือรวมไฟล์ อีกวิธีในการหลีกเลี่ยงการเรียกใช้โมดูลที่ฝังอยู่คือการใช้คำแนะนำทรัพยากร modulepreload
ซึ่งจะโหลดโมดูล ES ล่วงหน้าเพื่อหลีกเลี่ยงการเรียกใช้เครือข่ายแบบเป็นทอดๆ
บทสรุป
การเพิ่มประสิทธิภาพการประเมินสคริปต์ในเบราว์เซอร์นั้นไม่ใช่เรื่องง่าย แนวทางนี้ขึ้นอยู่กับข้อกำหนดและข้อจำกัดของเว็บไซต์ อย่างไรก็ตาม การแยกสคริปต์เป็นการกระจายงานการประเมินสคริปต์ไปยังงานเล็กๆ จำนวนมาก ซึ่งช่วยให้ชุดข้อความหลักจัดการกับการโต้ตอบของผู้ใช้ได้อย่างมีประสิทธิภาพมากขึ้น แทนที่จะบล็อกชุดข้อความหลัก
โดยสรุปแล้ว คุณสามารถทําสิ่งต่อไปนี้เพื่อแบ่งงานการประเมินสคริปต์ขนาดใหญ่
- เมื่อโหลดสคริปต์โดยใช้องค์ประกอบ
<script>
ที่ไม่มีแอตทริบิวต์type=module
ให้หลีกเลี่ยงการโหลดสคริปต์ที่มีขนาดใหญ่มาก เนื่องจากจะเริ่มต้นงานการประเมินสคริปต์ที่ต้องใช้ทรัพยากรมากซึ่งบล็อกเธรดหลัก กระจายสคริปต์ไปยังองค์ประกอบ<script>
เพิ่มเติมเพื่อแบ่งงานนี้ - การใช้แอตทริบิวต์
type=module
เพื่อโหลดโมดูล ES ในเบราว์เซอร์โดยตรงจะเริ่มต้นแต่ละงานเพื่อประเมินสคริปต์โมดูลแยกต่างหากแต่ละรายการ - ลดขนาดของ App Bundle เริ่มต้นโดยใช้การเรียก
import()
แบบไดนามิก การดำเนินการนี้ยังใช้ได้กับเครื่องมือรวมไฟล์ด้วย เนื่องจากเครื่องมือรวมไฟล์จะถือว่าแต่ละโมดูลที่นําเข้าแบบไดนามิกเป็น "จุดแยก" ซึ่งจะส่งผลให้มีการสร้างสคริปต์แยกต่างหากสําหรับแต่ละโมดูลที่นําเข้าแบบไดนามิก - อย่าลืมพิจารณาข้อดีข้อเสีย เช่น ประสิทธิภาพการบีบอัดและการลบล้างแคช สคริปต์ขนาดใหญ่จะบีบอัดได้ดีกว่า แต่มีแนวโน้มที่จะเกี่ยวข้องกับงานการประเมินสคริปต์ที่มีราคาแพงกว่าในจำนวนงานที่น้อยลง และส่งผลให้แคชของเบราว์เซอร์ใช้งานไม่ได้ ซึ่งทำให้ประสิทธิภาพการแคชโดยรวมลดลง
- หากใช้โมดูล ES แบบดั้งเดิมโดยไม่รวมกลุ่ม ให้ใช้
modulepreload
คำแนะนำทรัพยากรเพื่อเพิ่มประสิทธิภาพการโหลดโมดูลดังกล่าวระหว่างการเริ่มต้น - โปรดใช้ JavaScript น้อยที่สุดเท่าที่จะเป็นไปได้
แน่นอนว่าคุณต้องหาจุดสมดุล แต่การแยกสคริปต์และลดเพย์โหลดเริ่มต้นด้วย import()
แบบไดนามิกจะช่วยให้คุณได้รับประสิทธิภาพในการเริ่มต้นที่ดีขึ้นและรองรับการโต้ตอบของผู้ใช้ได้ดียิ่งขึ้นในช่วงเริ่มต้นที่สำคัญ ซึ่งจะช่วยให้คุณได้รับคะแนนที่ดีขึ้นสำหรับเมตริก INP จึงมอบประสบการณ์การใช้งานที่ดีขึ้นแก่ผู้ใช้