ปลดล็อกเว็บแอปพลิเคชันที่เร็วขึ้นด้วยการทำความเข้าใจ Browser Rendering Pipeline และวิธีที่ JavaScript อาจเป็นคอขวดด้านประสิทธิภาพ เรียนรู้วิธีการเพิ่มประสิทธิภาพเพื่อประสบการณ์ผู้ใช้ที่ราบรื่น
เชี่ยวชาญ Browser Rendering Pipeline: เจาะลึกผลกระทบของ JavaScript ต่อประสิทธิภาพการทำงาน
ในโลกดิจิทัล ความเร็วไม่ใช่แค่ฟีเจอร์ แต่เป็นรากฐานของประสบการณ์ผู้ใช้ที่ยอดเยี่ยม เว็บไซต์ที่ช้าและไม่ตอบสนองอาจนำไปสู่ความหงุดหงิดของผู้ใช้ เพิ่มอัตราการตีกลับ (bounce rates) และท้ายที่สุดส่งผลเสียต่อเป้าหมายทางธุรกิจ ในฐานะนักพัฒนาเว็บ เราคือสถาปนิกของประสบการณ์นี้ และการทำความเข้าใจกลไกหลักว่าเบราว์เซอร์เปลี่ยนโค้ดของเราให้กลายเป็นหน้าเว็บที่มองเห็นและโต้ตอบได้นั้นเป็นสิ่งสำคัญยิ่ง กระบวนการนี้ซึ่งมักจะซับซ้อนเรียกว่า Browser Rendering Pipeline
หัวใจสำคัญของเว็บแบบอินเทอร์แอคทีฟสมัยใหม่คือ JavaScript มันเป็นภาษาที่ทำให้หน้าเว็บที่หยุดนิ่งของเรามีชีวิตชีวาขึ้นมา ตั้งแต่การอัปเดตเนื้อหาแบบไดนามิกไปจนถึงแอปพลิเคชันหน้าเดียวที่ซับซ้อน อย่างไรก็ตาม พลังที่ยิ่งใหญ่มาพร้อมกับความรับผิดชอบที่ใหญ่ยิ่ง JavaScript ที่ไม่ได้รับการปรับแต่งเป็นหนึ่งในสาเหตุที่พบบ่อยที่สุดที่อยู่เบื้องหลังประสิทธิภาพเว็บที่ย่ำแย่ มันสามารถขัดจังหวะ, ทำให้ล่าช้า หรือบังคับให้ rendering pipeline ของเบราว์เซอร์ทำงานที่สิ้นเปลืองและซ้ำซ้อน ซึ่งนำไปสู่สิ่งที่น่ากลัวอย่าง 'jank'—แอนิเมชันที่กระตุก การตอบสนองต่อผู้ใช้ที่ช้า และความรู้สึกโดยรวมที่อืดอาด
คู่มือฉบับสมบูรณ์นี้ออกแบบมาสำหรับนักพัฒนา front-end, วิศวกรด้านประสิทธิภาพ และทุกคนที่หลงใหลในการสร้างเว็บที่เร็วขึ้น เราจะไขความลึกลับของ browser rendering pipeline โดยแบ่งออกเป็นขั้นตอนที่เข้าใจง่าย ที่สำคัญกว่านั้น เราจะเน้นย้ำถึงบทบาทของ JavaScript ภายในกระบวนการนี้ โดยสำรวจอย่างละเอียดว่ามันสามารถกลายเป็นคอขวดด้านประสิทธิภาพได้อย่างไร และที่สำคัญคือ เราจะทำอะไรได้บ้างเพื่อลดผลกระทบนั้น เมื่ออ่านจบ คุณจะมีความรู้และกลยุทธ์ที่นำไปใช้ได้จริงในการเขียน JavaScript ที่มีประสิทธิภาพมากขึ้น และมอบประสบการณ์ที่ราบรื่นและน่าพึงพอใจให้กับผู้ใช้ของคุณทั่วโลก
พิมพ์เขียวของเว็บ: การแยกส่วนประกอบของ Browser Rendering Pipeline
ก่อนที่เราจะสามารถเพิ่มประสิทธิภาพได้ เราต้องเข้าใจก่อน browser rendering pipeline (หรือที่เรียกว่า Critical Rendering Path) คือลำดับขั้นตอนที่เบราว์เซอร์ใช้ในการแปลง HTML, CSS และ JavaScript ที่คุณเขียนให้กลายเป็นพิกเซลบนหน้าจอ ลองนึกภาพว่าเป็นสายการผลิตในโรงงานที่มีประสิทธิภาพสูง แต่ละสถานีมีหน้าที่เฉพาะ และประสิทธิภาพของสายการผลิตทั้งหมดขึ้นอยู่กับว่าผลิตภัณฑ์เคลื่อนจากสถานีหนึ่งไปยังอีกสถานีหนึ่งได้อย่างราบรื่นเพียงใด
แม้ว่ารายละเอียดอาจแตกต่างกันเล็กน้อยระหว่าง browser engine (เช่น Blink สำหรับ Chrome/Edge, Gecko สำหรับ Firefox และ WebKit สำหรับ Safari) แต่ขั้นตอนพื้นฐานในเชิงแนวคิดนั้นเหมือนกัน เรามาดูกันทีละขั้นตอนของสายการผลิตนี้
ขั้นตอนที่ 1: Parsing - จากโค้ดสู่ความเข้าใจ
กระบวนการเริ่มต้นด้วยทรัพยากรที่เป็นข้อความดิบ: ไฟล์ HTML และ CSS ของคุณ เบราว์เซอร์ไม่สามารถทำงานกับสิ่งเหล่านี้ได้โดยตรง มันจำเป็นต้องแยกวิเคราะห์ (parse) ให้เป็นโครงสร้างที่มันสามารถเข้าใจได้
- การ Parsing HTML ไปเป็น DOM: HTML parser ของเบราว์เซอร์จะประมวลผลมาร์กอัป HTML โดยทำการ tokenizing และสร้างเป็นโครงสร้างข้อมูลแบบต้นไม้ที่เรียกว่า Document Object Model (DOM) DOM จะเป็นตัวแทนของเนื้อหาและโครงสร้างของหน้าเว็บ แต่ละแท็ก HTML จะกลายเป็น 'โหนด' (node) ในต้นไม้นี้ สร้างความสัมพันธ์แบบแม่-ลูกที่สะท้อนลำดับชั้นของเอกสารของคุณ
- การ Parsing CSS ไปเป็น CSSOM: ในขณะเดียวกัน เมื่อเบราว์เซอร์พบ CSS (ไม่ว่าจะในแท็ก
<style>
หรือสไตล์ชีตภายนอก<link>
) มันจะทำการ parse เพื่อสร้าง CSS Object Model (CSSOM) เช่นเดียวกับ DOM, CSSOM เป็นโครงสร้างแบบต้นไม้ที่เก็บสไตล์ทั้งหมดที่เกี่ยวข้องกับโหนด DOM รวมถึงสไตล์โดยปริยายจาก user-agent และกฎที่คุณกำหนดเอง
จุดสำคัญ: CSS ถือเป็นทรัพยากรที่ ขัดขวางการเรนเดอร์ (render-blocking) เบราว์เซอร์จะไม่เรนเดอร์ส่วนใดๆ ของหน้าเว็บจนกว่าจะดาวน์โหลดและแยกวิเคราะห์ CSS ทั้งหมดเสร็จสิ้น ทำไม? เพราะมันจำเป็นต้องรู้สไตล์สุดท้ายสำหรับทุกองค์ประกอบก่อนที่จะสามารถกำหนดวิธีการจัดวางหน้าเว็บได้ หน้าเว็บที่ไม่มีสไตล์แล้วจู่ๆ ก็เปลี่ยนสไตล์ไปมาจะเป็นประสบการณ์ที่ไม่ดีต่อผู้ใช้
ขั้นตอนที่ 2: Render Tree - พิมพ์เขียวสำหรับการแสดงผล
เมื่อเบราว์เซอร์มีทั้ง DOM (เนื้อหา) และ CSSOM (สไตล์) แล้ว มันจะรวมทั้งสองอย่างเข้าด้วยกันเพื่อสร้าง Render Tree ต้นไม้นี้เป็นตัวแทนของสิ่งที่จะถูกแสดงผลบนหน้าจอจริงๆ
Render Tree ไม่ใช่การคัดลอก DOM แบบหนึ่งต่อหนึ่ง มันจะรวมเฉพาะโหนดที่เกี่ยวข้องกับการแสดงผลเท่านั้น ตัวอย่างเช่น:
- โหนดอย่าง
<head>
,<script>
, หรือ<meta>
ซึ่งไม่มีผลลัพธ์ทางการมองเห็น จะถูกละไว้ - โหนดที่ถูกซ่อนโดยชัดเจนผ่าน CSS (เช่น ด้วย
display: none;
) ก็จะถูกตัดออกจาก Render Tree เช่นกัน (หมายเหตุ: องค์ประกอบที่มีvisibility: hidden;
จะยังคงถูกรวมอยู่ เพราะมันยังคงใช้พื้นที่ในการจัดวาง)
แต่ละโหนดใน Render Tree จะมีทั้งเนื้อหาจาก DOM และสไตล์ที่คำนวณแล้วจาก CSSOM
ขั้นตอนที่ 3: Layout (หรือ Reflow) - การคำนวณรูปทรงเรขาคณิต
เมื่อสร้าง Render Tree เสร็จแล้ว ตอนนี้เบราว์เซอร์รู้แล้วว่าต้องเรนเดอร์อะไร แต่ยังไม่รู้ว่าต้องวางที่ไหนหรือขนาดเท่าไหร่ นี่คืองานของขั้นตอน Layout เบราว์เซอร์จะไล่ตาม Render Tree ตั้งแต่ราก และคำนวณข้อมูลทางเรขาคณิตที่แม่นยำสำหรับแต่ละโหนด: ขนาด (ความกว้าง, ความสูง) และตำแหน่งบนหน้าเว็บเทียบกับ viewport
กระบวนการนี้ยังเป็นที่รู้จักในชื่อ Reflow คำว่า 'reflow' เหมาะสมอย่างยิ่งเพราะการเปลี่ยนแปลงเพียงองค์ประกอบเดียวอาจส่งผลกระทบต่อเนื่อง ทำให้ต้องคำนวณรูปทรงเรขาคณิตของลูก, บรรพบุรุษ และพี่น้องของมันใหม่ทั้งหมด ตัวอย่างเช่น การเปลี่ยนความกว้างขององค์ประกอบแม่มักจะทำให้เกิด reflow สำหรับองค์ประกอบลูกหลานทั้งหมดของมัน สิ่งนี้ทำให้ Layout เป็นการดำเนินการที่อาจใช้พลังการประมวลผลสูงมาก
ขั้นตอนที่ 4: Paint - การลงสีพิกเซล
ตอนนี้เบราว์เซอร์รู้โครงสร้าง, สไตล์, ขนาด และตำแหน่งของทุกองค์ประกอบแล้ว ก็ถึงเวลาแปลงข้อมูลนั้นให้เป็นพิกเซลจริงๆ บนหน้าจอ ขั้นตอน Paint (หรือ Repaint) เกี่ยวข้องกับการเติมพิกเซลสำหรับส่วนที่มองเห็นได้ทั้งหมดของแต่ละโหนด: สี, ข้อความ, รูปภาพ, เส้นขอบ, เงา ฯลฯ
เพื่อให้กระบวนการนี้มีประสิทธิภาพมากขึ้น เบราว์เซอร์สมัยใหม่ไม่ได้แค่ paint ลงบน canvas เดียว พวกเขามักจะแบ่งหน้าเว็บออกเป็นหลายเลเยอร์ (layers) ตัวอย่างเช่น องค์ประกอบที่ซับซ้อนที่มี CSS transform
หรือองค์ประกอบ <video>
อาจถูกเลื่อนขึ้นไปอยู่ในเลเยอร์ของตัวเอง การ Paint สามารถเกิดขึ้นได้ทีละเลเยอร์ ซึ่งเป็นการเพิ่มประสิทธิภาพที่สำคัญสำหรับขั้นตอนสุดท้าย
ขั้นตอนที่ 5: Compositing - การประกอบภาพสุดท้าย
ขั้นตอนสุดท้ายคือ Compositing เบราว์เซอร์จะนำเลเยอร์ที่ถูก paint แยกกันทั้งหมดมาประกอบกันตามลำดับที่ถูกต้องเพื่อสร้างภาพสุดท้ายที่แสดงบนหน้าจอ นี่คือจุดที่พลังของเลเยอร์ปรากฏชัด
หากคุณทำแอนิเมชันองค์ประกอบที่อยู่บนเลเยอร์ของตัวเอง (ตัวอย่างเช่น ใช้ transform: translateX(10px);
) เบราว์เซอร์ไม่จำเป็นต้องทำขั้นตอน Layout หรือ Paint ใหม่สำหรับทั้งหน้า มันสามารถย้ายเลเยอร์ที่ถูก paint ไว้แล้วได้เลย งานนี้มักจะถูกส่งต่อไปยัง Graphics Processing Unit (GPU) ทำให้มันรวดเร็วและมีประสิทธิภาพอย่างไม่น่าเชื่อ นี่คือความลับเบื้องหลังแอนิเมชันที่ลื่นไหลระดับ 60 เฟรมต่อวินาที (fps)
การปรากฏตัวของ JavaScript: เครื่องยนต์แห่งการโต้ตอบ
แล้ว JavaScript เข้ามาอยู่ตรงไหนใน pipeline ที่จัดลำดับไว้อย่างดีนี้? ทุกที่เลย JavaScript เป็นพลังแบบไดนามิกที่สามารถแก้ไข DOM และ CSSOM ได้ทุกเมื่อหลังจากที่พวกมันถูกสร้างขึ้น นี่คือหน้าที่หลักและความเสี่ยงด้านประสิทธิภาพที่ยิ่งใหญ่ที่สุดของมัน
โดยปกติแล้ว JavaScript เป็นแบบ parser-blocking เมื่อ HTML parser พบแท็ก <script>
(ที่ไม่ได้ระบุ async
หรือ defer
) มันจะต้องหยุดกระบวนการสร้าง DOM ชั่วคราว จากนั้นมันจะไปดึงสคริปต์ (ถ้าเป็นสคริปต์ภายนอก), ประมวลผลมัน และหลังจากนั้นจึงจะกลับมา parse HTML ต่อ หากสคริปต์นี้อยู่ในส่วน <head>
ของเอกสาร มันสามารถทำให้การเรนเดอร์ครั้งแรกของหน้าเว็บล่าช้าลงอย่างมากเพราะการสร้าง DOM ถูกหยุดไว้
จะ Block หรือไม่ Block: `async` และ `defer`
เพื่อลดพฤติกรรมการ blocking นี้ เรามี attribute ที่ทรงพลังสองตัวสำหรับแท็ก <script>
:
defer
: attribute นี้บอกเบราว์เซอร์ให้ดาวน์โหลดสคริปต์ในพื้นหลังขณะที่การ parse HTML ยังคงดำเนินต่อไป สคริปต์จะถูกรับประกันว่าจะทำงานหลังจากที่ HTML parser ทำงานเสร็จสิ้นแล้วเท่านั้น แต่ก่อนที่เหตุการณ์DOMContentLoaded
จะเกิดขึ้น หากคุณมีสคริปต์ที่ defer ไว้หลายตัว พวกมันจะทำงานตามลำดับที่ปรากฏในเอกสาร นี่เป็นตัวเลือกที่ยอดเยี่ยมสำหรับสคริปต์ที่ต้องการให้ DOM ทั้งหมดพร้อมใช้งานและลำดับการทำงานมีความสำคัญasync
: attribute นี้ก็บอกให้เบราว์เซอร์ดาวน์โหลดสคริปต์ในพื้นหลังโดยไม่ขัดขวางการ parse HTML เช่นกัน อย่างไรก็ตาม ทันทีที่สคริปต์ดาวน์โหลดเสร็จ HTML parser จะหยุดชั่วคราวและสคริปต์จะถูกประมวลผล สคริปต์แบบ Async ไม่มีการรับประกันลำดับการทำงาน เหมาะสำหรับสคริปต์อิสระของบุคคลที่สาม เช่น analytics หรือโฆษณา ซึ่งลำดับการทำงานไม่สำคัญและคุณต้องการให้มันทำงานโดยเร็วที่สุด
พลังในการเปลี่ยนแปลงทุกสิ่ง: การจัดการ DOM และ CSSOM
เมื่อทำงานแล้ว JavaScript จะสามารถเข้าถึง API ทั้งของ DOM และ CSSOM ได้อย่างเต็มที่ มันสามารถเพิ่มองค์ประกอบ, ลบออก, เปลี่ยนแปลงเนื้อหา และปรับเปลี่ยนสไตล์ได้ ตัวอย่างเช่น:
document.getElementById('welcome-banner').style.display = 'none';
JavaScript บรรทัดเดียวนี้แก้ไข CSSOM สำหรับองค์ประกอบ 'welcome-banner' การเปลี่ยนแปลงนี้จะทำให้ Render Tree ที่มีอยู่ไม่ถูกต้องอีกต่อไป บังคับให้เบราว์เซอร์ต้องทำบางส่วนของ rendering pipeline ซ้ำเพื่อสะท้อนการอัปเดตบนหน้าจอ
ตัวการด้านประสิทธิภาพ: JavaScript ทำท่อส่งข้อมูลอุดตันได้อย่างไร
ทุกครั้งที่ JavaScript แก้ไข DOM หรือ CSSOM มันมีความเสี่ยงที่จะกระตุ้นให้เกิด reflow และ repaint แม้ว่าสิ่งนี้จะจำเป็นสำหรับเว็บแบบไดนามิก แต่การดำเนินการเหล่านี้อย่างไม่มีประสิทธิภาพอาจทำให้แอปพลิเคชันของคุณหยุดชะงักได้ เรามาสำรวจกับดักด้านประสิทธิภาพที่พบบ่อยที่สุดกัน
วงจรอุบาทว์: การบังคับให้เกิด Synchronous Layouts และ Layout Thrashing
นี่อาจเป็นหนึ่งในปัญหาด้านประสิทธิภาพที่รุนแรงและซับซ้อนที่สุดในการพัฒนา front-end ดังที่เราได้กล่าวไปแล้ว Layout เป็นการดำเนินการที่สิ้นเปลือง เพื่อให้มีประสิทธิภาพ เบราว์เซอร์จึงฉลาดและพยายามรวบรวมการเปลี่ยนแปลง DOM เข้าด้วยกัน พวกมันจะจัดคิวการเปลี่ยนแปลงสไตล์จาก JavaScript ของคุณ แล้วในภายหลัง (โดยปกติคือตอนท้ายของเฟรมปัจจุบัน) พวกมันจะทำการคำนวณ Layout เพียงครั้งเดียวเพื่อใช้การเปลี่ยนแปลงทั้งหมดในคราวเดียว
อย่างไรก็ตาม คุณสามารถทำลายการเพิ่มประสิทธิภาพนี้ได้ หาก JavaScript ของคุณแก้ไขสไตล์แล้วทันทีทันใดก็ร้องขอค่าทางเรขาคณิต (เช่น offsetHeight
, offsetWidth
หรือ getBoundingClientRect()
ขององค์ประกอบ) คุณกำลังบังคับให้เบราว์เซอร์ทำขั้นตอน Layout แบบ synchronously เบราว์เซอร์ต้องหยุด, ใช้การเปลี่ยนแปลงสไตล์ที่ค้างอยู่ทั้งหมด, ทำการคำนวณ Layout ทั้งหมด แล้วจึงส่งคืนค่าที่ร้องขอกลับไปยังสคริปต์ของคุณ สิ่งนี้เรียกว่า Forced Synchronous Layout
เมื่อสิ่งนี้เกิดขึ้นภายในลูป มันจะนำไปสู่ปัญหาประสิทธิภาพที่เลวร้ายที่เรียกว่า Layout Thrashing คุณกำลังอ่านและเขียนซ้ำๆ บังคับให้เบราว์เซอร์ต้อง reflow ทั้งหน้าซ้ำแล้วซ้ำอีกภายในเฟรมเดียว
ตัวอย่างของ Layout Thrashing (สิ่งที่ไม่ควรทำ):
function resizeAllParagraphs() {
const paragraphs = document.querySelectorAll('p');
for (let i = 0; i < paragraphs.length; i++) {
// READ: gets the width of the container (forces layout)
const containerWidth = document.body.offsetWidth;
// WRITE: sets the paragraph's width (invalidates layout)
paragraphs[i].style.width = (containerWidth / 2) + 'px';
}
}
ในโค้ดนี้ ภายในทุกๆ รอบของลูป เราอ่าน offsetWidth
(การอ่านที่กระตุ้น layout) แล้วเขียนลงใน style.width
ทันที (การเขียนที่ทำให้ layout ไม่ถูกต้อง) สิ่งนี้บังคับให้เกิด reflow ในทุกๆ พารากราฟ
เวอร์ชันที่ปรับปรุงแล้ว (การรวบรวมการอ่านและการเขียน):
function resizeAllParagraphsOptimized() {
const paragraphs = document.querySelectorAll('p');
// First, READ all the values you need
const containerWidth = document.body.offsetWidth;
// Then, WRITE all the changes
for (let i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = (containerWidth / 2) + 'px';
}
}
เพียงแค่ปรับโครงสร้างโค้ดเพื่อทำการอ่านทั้งหมดก่อน ตามด้วยการเขียนทั้งหมด เราทำให้เบราว์เซอร์สามารถรวบรวมการดำเนินการได้ มันจะทำการคำนวณ Layout หนึ่งครั้งเพื่อรับความกว้างเริ่มต้น แล้วประมวลผลการอัปเดตสไตล์ทั้งหมด ซึ่งนำไปสู่การ reflow เพียงครั้งเดียวในตอนท้ายของเฟรม ความแตกต่างด้านประสิทธิภาพอาจมหาศาล
การปิดกั้น Main Thread: งาน JavaScript ที่ทำงานยาวนาน
main thread ของเบราว์เซอร์เป็นที่ที่วุ่นวาย มันรับผิดชอบการประมวลผล JavaScript, การตอบสนองต่อการป้อนข้อมูลของผู้ใช้ (คลิก, เลื่อน) และการทำงานของ rendering pipeline เนื่องจาก JavaScript เป็น single-threaded หากคุณรันสคริปต์ที่ซับซ้อนและทำงานยาวนาน คุณกำลังปิดกั้น main thread อย่างมีประสิทธิภาพ ขณะที่สคริปต์ของคุณกำลังทำงาน เบราว์เซอร์ไม่สามารถทำอย่างอื่นได้เลย มันไม่สามารถตอบสนองต่อการคลิก, ไม่สามารถประมวลผลการเลื่อน และไม่สามารถรันแอนิเมชันใดๆ ได้ หน้าเว็บจะค้างและไม่ตอบสนองโดยสิ้นเชิง
งานใดๆ ที่ใช้เวลานานกว่า 50ms ถือเป็น 'Long Task' และอาจส่งผลเสียต่อประสบการณ์ของผู้ใช้ โดยเฉพาะอย่างยิ่งต่อ Core Web Vital ที่ชื่อว่า Interaction to Next Paint (INP) สาเหตุทั่วไป ได้แก่ การประมวลผลข้อมูลที่ซับซ้อน, การจัดการการตอบสนอง API ขนาดใหญ่ หรือการคำนวณที่หนักหน่วง
วิธีแก้คือการแบ่งงานยาวๆ ออกเป็นส่วนเล็กๆ และ 'ยอม' ให้ main thread ทำงานอื่นบ้างในระหว่างนั้น ซึ่งจะทำให้เบราว์เซอร์มีโอกาสจัดการกับงานอื่นๆ ที่ค้างอยู่ วิธีง่ายๆ ในการทำเช่นนี้คือใช้ setTimeout(callback, 0)
ซึ่งจะจัดตารางเวลาให้ callback ทำงานใน task ถัดไป หลังจากที่เบราว์เซอร์ได้มีโอกาสพักหายใจ
ความตายจากบาดแผลนับพัน: การจัดการ DOM มากเกินไป
แม้ว่าการจัดการ DOM เพียงครั้งเดียวจะรวดเร็ว แต่การทำเป็นพันๆ ครั้งอาจช้ามาก ทุกครั้งที่คุณเพิ่ม, ลบ หรือแก้ไของค์ประกอบใน DOM ที่ใช้งานอยู่ คุณมีความเสี่ยงที่จะกระตุ้นให้เกิด reflow และ repaint หากคุณต้องการสร้างรายการจำนวนมากและต่อท้ายเข้ากับหน้าทีละรายการ คุณกำลังสร้างงานที่ไม่จำเป็นจำนวนมากให้กับเบราว์เซอร์
แนวทางที่มีประสิทธิภาพมากกว่าคือการสร้างโครงสร้าง DOM ของคุณ 'ออฟไลน์' แล้วค่อยต่อท้ายเข้ากับ DOM ที่ใช้งานอยู่ในการดำเนินการเพียงครั้งเดียว DocumentFragment
เป็นอ็อบเจกต์ DOM ที่มีน้ำหนักเบาและมีขนาดเล็กที่สุดโดยไม่มี parent คุณสามารถคิดว่ามันเป็นคอนเทนเนอร์ชั่วคราว คุณสามารถต่อท้ายองค์ประกอบใหม่ทั้งหมดของคุณเข้ากับ fragment แล้วต่อท้าย fragment ทั้งหมดเข้ากับ DOM ในครั้งเดียว ซึ่งส่งผลให้เกิด reflow/repaint เพียงครั้งเดียว ไม่ว่าคุณจะเพิ่มองค์ประกอบเข้าไปกี่ชิ้นก็ตาม
ตัวอย่างการใช้ DocumentFragment:
const list = document.getElementById('my-list');
const data = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];
// Create a DocumentFragment
const fragment = document.createDocumentFragment();
data.forEach(itemText => {
const li = document.createElement('li');
li.textContent = itemText;
// Append to the fragment, not the live DOM
fragment.appendChild(li);
});
// Append the entire fragment in one operation
list.appendChild(fragment);
การเคลื่อนไหวที่กระตุก: แอนิเมชัน JavaScript ที่ไม่มีประสิทธิภาพ
การสร้างแอนิเมชันด้วย JavaScript เป็นเรื่องปกติ แต่การทำอย่างไม่มีประสิทธิภาพจะนำไปสู่การกระตุกและ 'jank' รูปแบบที่ไม่ดีที่พบบ่อยคือการใช้ setTimeout
หรือ setInterval
เพื่ออัปเดตสไตล์ขององค์ประกอบในลูป
ปัญหาคือ timer เหล่านี้ไม่ได้ซิงโครไนซ์กับวงจรการเรนเดอร์ของเบราว์เซอร์ สคริปต์ของคุณอาจทำงานและอัปเดตสไตล์หลังจากที่เบราว์เซอร์เพิ่ง paint เฟรมเสร็จไป ทำให้ต้องทำงานพิเศษและอาจพลาดกำหนดเวลาของเฟรมถัดไป ส่งผลให้เกิด frame drop
วิธีที่ทันสมัยและถูกต้องในการทำแอนิเมชันด้วย JavaScript คือการใช้ requestAnimationFrame(callback)
API นี้บอกเบราว์เซอร์ว่าคุณต้องการทำแอนิเมชันและขอให้เบราว์เซอร์จัดตารางการ repaint หน้าต่างสำหรับเฟรมแอนิเมชันถัดไป ฟังก์ชัน callback ของคุณจะถูกเรียกใช้งานก่อนที่เบราว์เซอร์จะทำการ paint ครั้งถัดไป ทำให้มั่นใจได้ว่าการอัปเดตของคุณจะตรงเวลาและมีประสิทธิภาพ เบราว์เซอร์ยังสามารถเพิ่มประสิทธิภาพโดยการไม่เรียกใช้ callback หากหน้าเว็บอยู่ในแท็บพื้นหลัง
นอกจากนี้ สิ่งที่คุณทำแอนิเมชันก็สำคัญพอๆ กับวิธีที่คุณทำแอนิเมชัน การเปลี่ยนแปลงคุณสมบัติเช่น width
, height
, top
หรือ left
จะกระตุ้นขั้นตอน Layout ซึ่งช้า สำหรับแอนิเมชันที่ลื่นไหลที่สุด คุณควรใช้คุณสมบัติที่สามารถจัดการโดย Compositor เพียงอย่างเดียว ซึ่งโดยทั่วไปจะทำงานบน GPU คุณสมบัติเหล่านี้ส่วนใหญ่คือ:
transform
(สำหรับการย้าย, ปรับขนาด, หมุน)opacity
(สำหรับการทำให้จางเข้า/ออก)
การทำแอนิเมชันคุณสมบัติเหล่านี้ช่วยให้เบราว์เซอร์สามารถย้ายหรือทำให้เลเยอร์ที่ถูก paint ไว้แล้วขององค์ประกอบจางลงได้โดยไม่จำเป็นต้องทำขั้นตอน Layout หรือ Paint ใหม่ นี่คือกุญแจสำคัญในการบรรลุแอนิเมชัน 60fps ที่สม่ำเสมอ
จากทฤษฎีสู่การปฏิบัติ: ชุดเครื่องมือสำหรับการเพิ่มประสิทธิภาพ
การเข้าใจทฤษฎีเป็นขั้นตอนแรก ตอนนี้เรามาดูที่กลยุทธ์และเครื่องมือที่สามารถนำไปใช้ได้จริงเพื่อนำความรู้นี้ไปปฏิบัติ
การโหลดสคริปต์อย่างชาญฉลาด
วิธีที่คุณโหลด JavaScript คือแนวป้องกันด่านแรก ถามตัวเองเสมอว่าสคริปต์นั้นสำคัญต่อการเรนเดอร์ครั้งแรกจริงๆ หรือไม่ ถ้าไม่ ให้ใช้ defer
สำหรับสคริปต์ที่ต้องการ DOM หรือ async
สำหรับสคริปต์ที่เป็นอิสระ สำหรับแอปพลิเคชันสมัยใหม่ ให้ใช้เทคนิคอย่าง code-splitting โดยใช้ import()
แบบไดนามิกเพื่อโหลดเฉพาะ JavaScript ที่จำเป็นสำหรับมุมมองปัจจุบันหรือการโต้ตอบของผู้ใช้เท่านั้น เครื่องมืออย่าง Webpack หรือ Rollup ยังมี tree-shaking เพื่อกำจัดโค้ดที่ไม่ได้ใช้ออกจาก bundle สุดท้ายของคุณ ซึ่งช่วยลดขนาดไฟล์
การควบคุมเหตุการณ์ความถี่สูง: Debouncing และ Throttling
เหตุการณ์ของเบราว์เซอร์บางอย่าง เช่น scroll
, resize
และ mousemove
สามารถเกิดขึ้นได้หลายร้อยครั้งต่อวินาที หากคุณมี event handler ที่สิ้นเปลืองทรัพยากรผูกอยู่กับมัน (เช่น ตัวที่ทำการจัดการ DOM) คุณอาจทำให้ main thread อุดตันได้อย่างง่ายดาย มีสองรูปแบบที่จำเป็นที่นี่:
- Throttling: ทำให้แน่ใจว่าฟังก์ชันของคุณจะถูกเรียกใช้งานไม่เกินหนึ่งครั้งต่อช่วงเวลาที่กำหนด ตัวอย่างเช่น 'เรียกใช้ฟังก์ชันนี้ไม่เกินหนึ่งครั้งทุกๆ 200ms' สิ่งนี้มีประโยชน์สำหรับสิ่งต่างๆ เช่น infinite scroll handlers
- Debouncing: ทำให้แน่ใจว่าฟังก์ชันของคุณจะถูกเรียกใช้งานหลังจากไม่มีการใช้งานเป็นระยะเวลาหนึ่งเท่านั้น ตัวอย่างเช่น 'เรียกใช้ฟังก์ชันค้นหานี้เฉพาะหลังจากผู้ใช้หยุดพิมพ์เป็นเวลา 300ms' สิ่งนี้เหมาะสำหรับแถบค้นหาแบบเติมข้อความอัตโนมัติ
การแบ่งเบาภาระ: ความรู้เบื้องต้นเกี่ยวกับ Web Workers
สำหรับการคำนวณ JavaScript ที่หนักและทำงานยาวนานจริงๆ ซึ่งไม่ต้องการการเข้าถึง DOM โดยตรง Web Workers คือตัวเปลี่ยนเกม Web Worker ช่วยให้คุณสามารถรันสคริปต์บน thread พื้นหลังที่แยกจากกัน ซึ่งจะปลดปล่อย main thread ให้ว่างเพื่อตอบสนองต่อผู้ใช้ได้อย่างเต็มที่ คุณสามารถส่งข้อความระหว่าง main thread และ worker thread เพื่อส่งข้อมูลและรับผลลัพธ์ กรณีการใช้งานรวมถึงการประมวลผลภาพ, การวิเคราะห์ข้อมูลที่ซับซ้อน หรือการดึงข้อมูลและการแคชในพื้นหลัง
การเป็นนักสืบด้านประสิทธิภาพ: การใช้ Browser DevTools
คุณไม่สามารถเพิ่มประสิทธิภาพในสิ่งที่คุณวัดผลไม่ได้ แผง Performance ในเบราว์เซอร์สมัยใหม่อย่าง Chrome, Edge และ Firefox เป็นเครื่องมือที่ทรงพลังที่สุดของคุณ นี่คือคำแนะนำสั้นๆ:
- เปิด DevTools และไปที่แท็บ 'Performance'
- คลิกปุ่มบันทึกและทำการกระทำบนไซต์ของคุณที่คุณสงสัยว่าช้า (เช่น การเลื่อน, การคลิกปุ่ม)
- หยุดการบันทึก
คุณจะเห็น flame chart ที่มีรายละเอียด ให้มองหา:
- Long Tasks: สิ่งเหล่านี้จะถูกทำเครื่องหมายด้วยสามเหลี่ยมสีแดง นี่คือตัวบล็อก main thread ของคุณ คลิกที่มันเพื่อดูว่าฟังก์ชันใดเป็นสาเหตุของความล่าช้า
- บล็อก 'Layout' สีม่วง: บล็อกสีม่วงขนาดใหญ่บ่งชี้ว่ามีการใช้เวลาในขั้นตอน Layout จำนวนมาก
- คำเตือน Forced Synchronous Layout: เครื่องมือมักจะเตือนคุณอย่างชัดเจนเกี่ยวกับ forced reflows โดยแสดงให้เห็นบรรทัดโค้ดที่รับผิดชอบ
- บล็อก 'Paint' สีเขียวขนาดใหญ่: สิ่งเหล่านี้อาจบ่งชี้ถึงการดำเนินการ paint ที่ซับซ้อนซึ่งอาจสามารถปรับปรุงได้
นอกจากนี้ แท็บ 'Rendering' (มักจะซ่อนอยู่ในลิ้นชักของ DevTools) มีตัวเลือกเช่น 'Paint Flashing' ซึ่งจะไฮไลต์พื้นที่ของหน้าจอเป็นสีเขียวเมื่อใดก็ตามที่มีการ repaint นี่เป็นวิธีที่ยอดเยี่ยมในการดีบัก repaints ที่ไม่จำเป็นด้วยสายตา
สรุป: สร้างเว็บที่เร็วขึ้น ทีละเฟรม
browser rendering pipeline เป็นกระบวนการที่ซับซ้อนแต่มีเหตุผล ในฐานะนักพัฒนา โค้ด JavaScript ของเราเป็นแขกประจำใน pipeline นี้ และพฤติกรรมของมันเป็นตัวกำหนดว่าจะช่วยสร้างประสบการณ์ที่ราบรื่นหรือก่อให้เกิดคอขวดที่รบกวน โดยการทำความเข้าใจแต่ละขั้นตอน—ตั้งแต่ Parsing ถึง Compositing—เราจะได้รับข้อมูลเชิงลึกที่จำเป็นในการเขียนโค้ดที่ทำงานร่วมกับเบราว์เซอร์ ไม่ใช่ต่อต้านมัน
ประเด็นสำคัญคือการผสมผสานระหว่างการตระหนักรู้และการลงมือทำ:
- เคารพ main thread: ทำให้มันว่างอยู่เสมอโดยการ defer สคริปต์ที่ไม่สำคัญ, แบ่งงานยาวๆ และส่งต่องานหนักไปยัง Web Workers
- หลีกเลี่ยง Layout Thrashing: จัดโครงสร้างโค้ดของคุณเพื่อรวบรวมการอ่านและเขียน DOM การเปลี่ยนแปลงง่ายๆ นี้สามารถให้ผลตอบแทนด้านประสิทธิภาพอย่างมหาศาล
- ฉลาดกับการใช้ DOM: ใช้เทคนิคเช่น DocumentFragments เพื่อลดจำนวนครั้งที่คุณแตะต้อง DOM ที่ใช้งานอยู่
- ทำแอนิเมชันอย่างมีประสิทธิภาพ: เลือกใช้
requestAnimationFrame
แทนเมธอด timer แบบเก่า และใช้คุณสมบัติที่เป็นมิตรกับ compositor เช่นtransform
และopacity
- วัดผลเสมอ: ใช้เครื่องมือสำหรับนักพัฒนาในเบราว์เซอร์เพื่อโปรไฟล์แอปพลิเคชันของคุณ, ระบุคอขวดในโลกแห่งความเป็นจริง และตรวจสอบการเพิ่มประสิทธิภาพของคุณ
การสร้างเว็บแอปพลิเคชันที่มีประสิทธิภาพสูงไม่ใช่เรื่องของการเพิ่มประสิทธิภาพก่อนเวลาอันควรหรือการท่องจำเทคนิคที่คลุมเครือ มันเกี่ยวกับการทำความเข้าใจแพลตฟอร์มที่คุณกำลังสร้างขึ้นมาอย่างถ่องแท้ โดยการเชี่ยวชาญปฏิสัมพันธ์ระหว่าง JavaScript และ rendering pipeline คุณจะเพิ่มขีดความสามารถให้ตัวเองในการสร้างประสบการณ์เว็บที่เร็วขึ้น, ยืดหยุ่นมากขึ้น และท้ายที่สุดคือสนุกสนานยิ่งขึ้นสำหรับทุกคน ทุกที่