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