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