ไม่รู้แล้วจะเสียดาย เคล็ดลับ JavaScript จัดการการเรนเดอร์คอมโพเนนต์ให้ไวขึ้น ลดภาระ เพิ่มประสิทธิภาพแอปพลิเคชันของคุณอย่างก้าวกระโดด

webmaster

A professional software developer, fully clothed in a modest business casual shirt and dark trousers, sitting thoughtfully at a modern office desk. Their computer screen shows a web application with a visibly slow loading spinner and slightly jumbled UI elements, conveying frustration and inefficiency. The lighting is soft, creating a serious yet professional atmosphere. Safe for work, appropriate content, perfect anatomy, correct proportions, natural pose, professional photography, high quality, well-formed hands, proper finger count, natural body proportions, family-friendly.

เคยไหมครับที่รู้สึกว่าเว็บแอปพลิเคชันที่เราสร้างขึ้นมามันเริ่มทำงานช้าลงเรื่อยๆ ทั้งๆ ที่โค้ดก็ดูไม่ได้ซับซ้อนอะไร? ในฐานะนักพัฒนาซอฟต์แวร์ที่คลุกคลีกับการสร้างสรรค์เว็บไซต์และแอปมาหลายปี ผมเองก็เคยเจอสถานการณ์แบบนี้บ่อยครั้งจนเกือบจะถอดใจ และจากประสบการณ์ตรง ผมเข้าใจดีว่าความล่าช้าเพียงนิดเดียวก็สร้างความหงุดหงิดให้ผู้ใช้งานได้มากแค่ไหน โดยเฉพาะอย่างยิ่งในยุคปัจจุบันที่ผู้ใช้งานคาดหวังประสบการณ์ที่ลื่นไหลรวดเร็ว ไม่ต่างอะไรกับการขับรถบนถนนโล่งๆ ถ้าเว็บไซต์เราโหลดช้า หรือมีอาการกระตุกแม้แต่นิดเดียว ผู้ใช้งานก็พร้อมที่จะปิดทิ้งไปหาคู่แข่งทันที การที่คอมโพเนนต์ใน JavaScript เกิดการ “Re-render” บ่อยครั้งโดยไม่จำเป็นนี่แหละครับ คือตัวการสำคัญที่บั่นทอนประสิทธิภาพและสร้างความหงุดหงิดให้ทั้งผู้ใช้และตัวนักพัฒนาเองยิ่งแอปพลิเคชันสมัยใหม่ซับซ้อนขึ้นเรื่อยๆ มีข้อมูลที่ต้องจัดการมากมาย การมองข้ามการจัดการ Re-render จึงไม่ใช่ทางเลือกอีกต่อไป เพราะมันส่งผลโดยตรงต่อ User Experience (UX), SEO และแม้แต่ต้นทุน Server จากการศึกษาและลองผิดลองถูกมานับครั้งไม่ถ้วน ผมค้นพบว่าการทำความเข้าใจและจัดการกับวงจรชีวิตของ Component และเทคนิคการ Optimization ต่างๆ อย่าง React.memo, useCallback หรือ useMemo (สำหรับ React) เป็นสิ่งสำคัญอย่างยิ่ง ที่ช่วยให้แอปพลิเคชันของเรากลับมาลื่นไหลอีกครั้ง ไม่ใช่แค่การแก้ปัญหาเฉพาะหน้า แต่เป็นการวางรากฐานที่ดีเพื่อการ Scale ในอนาคตอีกด้วย เราจะมาเรียนรู้เรื่องนี้กันให้ชัดเจนครับ!

ทำไม Component ถึง Re-render บ่อยครั้งและเราจะรู้ได้อย่างไร?

วจะเส - 이미지 1
ปัญหา Re-render ที่ไม่ได้ตั้งใจนี่แหละครับ คือตัวการหลักที่ทำให้แอปพลิเคชันของเราหน่วงจนผู้ใช้งานรู้สึกหงุดหงิด เหมือนกับว่าเรากำลังขับรถอยู่ดีๆ แล้วมีคนคอยเบรกและเหยียบคันเร่งซ้ำๆ ทั้งที่ไม่มีความจำเป็นเลย จากประสบการณ์ตรงของผมที่ทำโปรเจกต์มามากมาย ไม่ว่าจะเป็นเว็บ E-commerce ขนาดใหญ่ หรือแพลตฟอร์มโซเชียลมีเดียที่มีการโต้ตอบแบบเรียลไทม์ ผมสังเกตเห็นว่าต้นตอของ Re-render มักจะมาจากการเปลี่ยนแปลงของ Props, State หรือ Context ที่คอมโพเนนต์นั้นๆ หรือคอมโพเนนต์แม่ของมันมีการอัปเดต ถ้าคอมโพเนนต์ใดคอมโพเนนต์หนึ่งใน Tree ถูกอัปเดต ไม่ว่าจะเล็กน้อยแค่ไหน คอมโพเนนต์ลูกที่อยู่ภายใต้คอมโพเนนต์นั้นก็มีแนวโน้มที่จะถูก Re-render ใหม่ทั้งหมด ถึงแม้ว่าข้อมูลที่มันแสดงผลจะไม่ได้มีการเปลี่ยนแปลงเลยก็ตาม

1.1. การเปลี่ยนแปลงของ Props และ State

ทุกครั้งที่ Props หรือ State ของคอมโพเนนต์เกิดการเปลี่ยนแปลง React จะทำการเปรียบเทียบข้อมูลชุดใหม่กับชุดเดิม (Virtual DOM Diffing) และถ้าพบว่ามีความแตกต่างก็จะทำการ Re-render คอมโพเนนต์นั้นและคอมโพเนนต์ลูกทั้งหมดที่อยู่ภายใต้มัน นี่คือพฤติกรรมพื้นฐานของ React ที่ช่วยให้ UI ของเราอัปเดตตามข้อมูลได้อย่างรวดเร็ว แต่ปัญหาคือบางครั้งข้อมูลที่เปลี่ยนไปอาจจะไม่ได้ส่งผลให้ UI ต้องเปลี่ยนตาม หรือข้อมูลที่ถูกส่งเป็น Props อาจจะเป็น Object หรือ Array ที่ถูกสร้างขึ้นมาใหม่ในแต่ละครั้งที่ Parent Component Re-render ทำให้ Child Component มองว่า Props เปลี่ยน ทั้งที่ข้างในข้อมูลยังเหมือนเดิมเป๊ะๆ ตรงนี้แหละครับที่ทำให้เกิด Re-render เกินความจำเป็น ผมเคยมีเคสที่เจอว่า UI ของหน้า Product List โหลดช้าลงอย่างเห็นได้ชัด ทั้งที่ข้อมูลสินค้าไม่ได้มีการเปลี่ยนแปลง แค่ Parent Component มี State บางอย่างเปลี่ยนที่ไม่ได้เกี่ยวอะไรกับ Child Component เลย นี่แหละที่น่าปวดหัว!

1.2. Context API และการ Re-render แบบเหมาเข่ง

Context API เป็นเครื่องมือที่ทรงพลังในการส่งข้อมูลข้ามคอมโพเนนต์โดยไม่ต้อง Props Drilling แต่มันก็มีดาบสองคมที่ต้องระวังให้ดีครับ เมื่อไหร่ก็ตามที่ค่าใน Context มีการเปลี่ยนแปลง คอมโพเนนต์ทุกตัวที่ใช้ Context นั้น (Consumer) จะถูก Re-render ทั้งหมด ไม่ว่าจะใช้ข้อมูลส่วนไหนของ Context หรือไม่ก็ตาม ถ้า Context ของเรามีข้อมูลจำนวนมากและมีการอัปเดตบ่อยๆ ก็จะส่งผลกระทบต่อประสิทธิภาพอย่างรุนแรง ผมเคยลองใช้ Context เก็บข้อมูล User ทั่วทั้งแอปพลิเคชัน พอ User Profile มีการอัปเดตแม้แต่นิดเดียว ทุกคอมโพเนนต์ที่เรียกใช้ Context นั้นก็ Re-render พร้อมกันหมด ทำให้แอปพลิเคชันกระตุกอย่างเห็นได้ชัดเลยครับ

ผลกระทบของการ Re-render ที่ไม่จำเป็นต่อประสิทธิภาพของแอปพลิเคชัน

การที่คอมโพเนนต์ Re-render ซ้ำซ้อนโดยไม่มีเหตุผลอันควรนั้นส่งผลกระทบมากกว่าที่เราคิดเยอะเลยครับ มันไม่ได้แค่ทำให้แอปพลิเคชันช้าลงจนผู้ใช้เซ็งเท่านั้น แต่ยังกระทบถึงทรัพยากรเครื่องของผู้ใช้งานและแม้แต่ต้นทุนการพัฒนาในระยะยาวอีกด้วย เหมือนกับเรากำลังขับรถไปไหนมาไหนด้วยความเร็วต่ำๆ ทั้งที่มีถนนโล่งๆ รอให้เราเหยียบไปได้เต็มที่ ซึ่งมันทั้งเสียเวลาและเปลืองน้ำมันโดยใช่เหตุ

2.1. ประสบการณ์ผู้ใช้ (User Experience) ที่ย่ำแย่

แน่นอนว่านี่คือผลกระทบที่เห็นได้ชัดเจนที่สุด ผู้ใช้งานในปัจจุบันคาดหวังความรวดเร็วและลื่นไหลจากการใช้งานแอปพลิเคชัน ลองจินตนาการว่าคุณกำลังเลื่อนดู Feed บนแพลตฟอร์มโซเชียลมีเดีย แล้วจู่ๆ ภาพหรือข้อความก็กระตุก หรือต้องรอโหลดข้อมูลซ้ำๆ ทั้งที่กดเพียงปุ่มเดียว ความรู้สึกหงุดหงิดจะเกิดขึ้นทันที และผู้ใช้งานส่วนใหญ่ก็พร้อมที่จะปิดหน้าต่างนั้นไปหาคู่แข่งทันทีที่รู้สึกว่าไม่โอเค การที่แอปของเราโหลดช้า หรือมีอาการ “สะอึก” บ่อยๆ จะทำให้ User Dwell Time ลดลง (ผู้ใช้อยู่ในเว็บเราสั้นลง) ซึ่งส่งผลเสียต่อ AdSense CTR และ RPM อย่างหลีกเลี่ยงไม่ได้ เพราะยิ่งผู้ใช้อยู่ในเว็บนาน โอกาสที่ Ads จะแสดงผลและถูกคลิกก็ยิ่งมากขึ้น

2.2. สิ้นเปลืองทรัพยากรและพลังงาน

ทุกครั้งที่คอมโพเนนต์ Re-render มันจะทำการประมวลผล JavaScript ใหม่, สร้าง Virtual DOM ใหม่, เปรียบเทียบ Diff, และอาจจะมีการ Manipulate DOM จริงๆ ด้วย ซึ่งกระบวนการเหล่านี้ล้วนต้องใช้ทรัพยากร CPU และ Memory ของเครื่องผู้ใช้งาน ยิ่งมี Re-render บ่อยๆ ทรัพยากรเหล่านี้ก็จะถูกใช้มากเกินความจำเป็น ทำให้แบตเตอรี่มือถือหมดเร็วขึ้น หรือทำให้เครื่องคอมพิวเตอร์ทำงานหนักจนเกิดความร้อนสะสม ผมเคยลองใช้ Chrome DevTools Performance Tab วิเคราะห์ดู แล้วพบว่ากว่า 30% ของ CPU Time ถูกใช้ไปกับการ Re-render ที่ไม่จำเป็น นี่มันเหมือนกับการที่คุณเปิดเครื่องปรับอากาศทิ้งไว้ทั้งที่ไม่มีคนอยู่ในห้องเลยนั่นแหละครับ

กลยุทธ์หลักในการลดการ Re-render: หัวใจของการปรับปรุงประสิทธิภาพ

เมื่อเราเข้าใจถึงสาเหตุและผลกระทบของการ Re-render ที่ไม่จำเป็นแล้ว ขั้นตอนต่อไปคือการหาวิธีป้องกันและลดมันลงครับ ใน React นั้นมีเครื่องมือและเทคนิคหลายอย่างที่ช่วยให้เราควบคุมพฤติกรรม Re-render ของคอมโพเนนต์ได้ ซึ่งผมบอกเลยว่าการเรียนรู้และนำสิ่งเหล่านี้ไปใช้อย่างถูกจังหวะเวลาจะพลิกโฉมแอปพลิเคชันของคุณให้เร็วขึ้นอย่างก้าวกระโดดเลยทีเดียว จากประสบการณ์ของผม การ Optimization ไม่ใช่เรื่องของการทำทุกอย่างให้เร็วที่สุดเท่าที่จะทำได้ แต่เป็นการทำในจุดที่สำคัญที่สุด และเลือกใช้เทคนิคที่เหมาะสมกับสถานการณ์ เหมือนกับการเลือกใช้อุปกรณ์ให้ถูกประเภทกับงานที่ทำ

3.1. ใช้ React.memo เพื่อจดจำ Component

React.memo เป็น HOC (Higher-Order Component) ที่ใช้สำหรับ “จดจำ” คอมโพเนนต์แบบ Functional Component ถ้า Props ที่ส่งเข้ามาไม่เปลี่ยนแปลง React.memo จะไม่ทำให้คอมโพเนนต์นั้น Re-render ซ้ำ มันจะคืนค่าที่เคย Render ไว้ก่อนหน้า ทำให้ประหยัดทรัพยากรในการประมวลผลได้อย่างมหาศาล ผมใช้ React.memo บ่อยมากกับคอมโพเนนต์ที่มีการแสดงผลซับซ้อน หรือคอมโพเนนต์ที่เป็น UI หลักๆ ที่มีการ Re-render บ่อยๆ เช่น Item ใน List หรือ Card ที่แสดงข้อมูลสินค้า แต่มีข้อควรระวังคือ ถ้า Props เป็น Object หรือ Array ที่สร้างขึ้นใหม่ทุกครั้ง มันก็จะยัง Re-render อยู่ดี เพราะ Object หรือ Array นั้นจะมี Reference ที่ต่างกัน ทำให้ React.memo มองว่า Props เปลี่ยนแปลง

3.2. useCallback สำหรับการจดจำ Function

useCallback เป็น Hook ที่ช่วยให้เรา “จดจำ” ฟังก์ชันที่สร้างขึ้นภายในคอมโพเนนต์ ถ้า Dependency Array ของ useCallback ไม่เปลี่ยนแปลง ฟังก์ชันนั้นก็จะไม่ถูกสร้างขึ้นมาใหม่ในแต่ละรอบของการ Re-render นี่เป็นสิ่งสำคัญมากเมื่อคุณส่งฟังก์ชันเป็น Props ลงไปให้ Child Component ที่ใช้ React.memo ถ้าฟังก์ชัน Parent Component ถูกสร้างใหม่ทุกครั้ง Child Component ที่ใช้ React.memo ก็จะ Re-render เพราะมันมองว่า Prop ที่เป็นฟังก์ชันนั้นเปลี่ยนไป ผมใช้ useCallback บ่อยมากในการจัดการ Event Handlers เช่น onClick, onChange หรือ onSubmit เพื่อให้แน่ใจว่า Child Component จะไม่ Re-render โดยไม่จำเป็น

3.3. useMemo สำหรับการจดจำค่า

useMemo เป็น Hook ที่คล้ายกับ useCallback แต่ใช้สำหรับ “จดจำ” ค่าที่ได้จากการคำนวณที่ซับซ้อน ถ้า Dependency Array ของ useMemo ไม่เปลี่ยนแปลง ค่าที่ได้จากการคำนวณก็จะถูกนำมาใช้ซ้ำโดยไม่ต้องคำนวณใหม่ นี่มีประโยชน์มากเมื่อคุณต้องคำนวณข้อมูลจำนวนมาก หรือประมวลผลข้อมูลที่ใช้เวลานาน เช่น การ Filter หรือ Sort ข้อมูลจำนวนมากๆ ก่อนที่จะนำไปแสดงผล ผมเคยใช้ useMemo กับการคำนวณยอดรวมสินค้าในตะกร้า ซึ่งมีหลายชิ้นและมีส่วนลดที่ซับซ้อน การใช้ useMemo ช่วยให้การอัปเดต UI เร็วขึ้นอย่างเห็นได้ชัด เพราะไม่ต้องคำนวณใหม่ทุกครั้งที่คอมโพเนนต์ Re-render

การใช้ React.memo, useCallback, useMemo ให้ถูกจังหวะ: เครื่องมือที่ต้องเข้าใจอย่างลึกซึ้ง

การรู้ว่ามีเครื่องมือเหล่านี้อยู่แล้วนำไปใช้ทันทีแบบไม่คิดหน้าคิดหลังก็อาจจะไม่ได้ช่วยให้แอปพลิเคชันเร็วขึ้นเสมอไปครับ บางครั้งอาจจะทำให้โค้ดซับซ้อนขึ้นโดยไม่จำเป็นด้วยซ้ำ เพราะการ Optimize ก็มีต้นทุนเช่นกัน ต้นทุนในที่นี้คือเรื่องของ Memory ที่ต้องใช้เก็บค่าที่ Memoize ไว้ และต้นทุนในการเปรียบเทียบ Dependency Array ดังนั้นเราต้องเลือกใช้ให้ถูกจังหวะ เหมือนกับการเลือกใช้เครื่องมือช่างที่เหมาะสมกับงาน ไม่ใช่เอาค้อนไปตอกตะปูทั้งที่งานนั้นต้องการไขควง

4.1. เมื่อไหร่ที่ควรใช้ React.memo?

เมื่อคุณมี Functional Component ที่:
1. มีการ Re-render บ่อยครั้ง แต่ Props ที่ส่งให้คอมโพเนนต์นั้นไม่ค่อยเปลี่ยนแปลง หรือเปลี่ยนแปลงน้อยมาก
2. มีการคำนวณหรือการ Render ที่ซับซ้อน ใช้ทรัพยากรเยอะ เช่น เป็นคอมโพเนนต์ที่แสดงผลข้อมูลเป็นตารางใหญ่ๆ มีการคำนวณภายในเยอะ หรือเป็นส่วนประกอบของ UI ที่มีการอัปเดตบ่อยๆ เช่น ใน List Item ที่มีเป็นร้อยเป็นพันชิ้น
3.

ไม่มี State ของตัวเอง หรือ State นั้นมีการเปลี่ยนแปลงน้อยมาก
* ข้อควรระวัง: อย่าใช้ React.memo กับทุกคอมโพเนนต์ เพราะการเปรียบเทียบ Props ก็มีต้นทุนเช่นกัน ถ้าคอมโพเนนต์นั้น Re-render ไม่บ่อย หรือ Render ได้เร็วอยู่แล้ว การใช้ React.memo อาจจะทำให้โค้ดซับซ้อนขึ้นและไม่ได้ประโยชน์ด้านประสิทธิภาพเท่าที่ควร

4.2. เมื่อไหร่ที่ควรใช้ useCallback และ useMemo?

useCallback และ useMemo มักจะใช้ร่วมกับ React.memo เพื่อให้การ Memoization สมบูรณ์:
1. ส่ง Function หรือ Object/Array ที่ซับซ้อนเป็น Props: ถ้าคุณส่งฟังก์ชันหรือ Object/Array ที่สร้างขึ้นใหม่ในแต่ละรอบการ Re-render ของ Parent Component ไปให้ Child Component ที่ใช้ React.memo คุณจะต้องใช้ useCallback หรือ useMemo เพื่อให้ค่าเหล่านั้นมี Reference เดิมตราบใดที่ Dependencies ไม่เปลี่ยน มิฉะนั้น Child Component ที่ใช้ React.memo ก็จะ Re-render อยู่ดี
2.

มีการคำนวณที่ซับซ้อนและใช้ทรัพยากรมาก: ถ้ามีโค้ดส่วนไหนที่คุณต้องคำนวณค่าที่ใช้เวลานาน หรือสร้างข้อมูลชุดใหญ่ขึ้นมาใหม่ทุกครั้งที่คอมโพเนนต์ Re-render การใช้ useMemo จะช่วยให้คุณสามารถจดจำผลลัพธ์ของการคำนวณนั้นได้
* ข้อควรระวัง: เช่นเดียวกับ React.memo การใช้ useCallback/useMemo ก็มีต้นทุนของตัวเอง หากฟังก์ชันหรือการคำนวณนั้นไม่ได้ซับซ้อนมาก การสร้างใหม่ทุกครั้งอาจจะเร็วกว่าการ Memoize และเปรียบเทียบ Dependencies ครับ

คุณสมบัติ React.memo useCallback useMemo
วัตถุประสงค์หลัก ป้องกัน Functional Component ไม่ให้ Re-render หาก Props ไม่เปลี่ยน จดจำฟังก์ชัน เพื่อให้มี Reference เดิมหาก Dependencies ไม่เปลี่ยน จดจำค่าที่ได้จากการคำนวณ เพื่อไม่ต้องคำนวณซ้ำหาก Dependencies ไม่เปลี่ยน
ใช้กับ Functional Component ฟังก์ชัน ค่า (เช่น object, array, ผลลัพธ์จากการคำนวณ)
Dependency Array ไม่มี (แต่สามารถใช้ เป็น Custom Comparison ได้) มี (ควบคุมการสร้างฟังก์ชันใหม่) มี (ควบคุมการคำนวณค่าใหม่)
ผลที่ได้ Component ไม่ Re-render Reference ของฟังก์ชันคงเดิม Reference ของค่าคงเดิม / ไม่ต้องคำนวณซ้ำ
เหมาะสำหรับ Component ที่รับ Props เข้ามาเยอะๆ, Component ที่ Render ซับซ้อนใน List ฟังก์ชันที่ส่งเป็น Prop ให้ Child Component ที่ Memoized การคำนวณที่ใช้เวลานาน, สร้าง Object/Array ขนาดใหญ่

ข้อควรระวังและการจัดการกับ Dependency Array

การใช้ Hook ที่มีการระบุ Dependency Array อย่าง useCallback และ useMemo นั้นเป็นสิ่งสำคัญมากครับ เพราะมันคือตัวควบคุมว่าเมื่อไหร่ที่ฟังก์ชันหรือค่านั้นจะถูกสร้างขึ้นมาใหม่ ถ้าเราใส่ Dependency ไม่ครบ หรือใส่ผิดไปแม้แต่นิดเดียว ก็อาจจะทำให้เกิดบั๊กที่หายาก หรือทำให้การ Optimization ของเราไม่มีผลเลยก็ได้ ซึ่งผมเองก็เคยพลาดมาหลายครั้งแล้วในจุดนี้ ต้องมานั่งดีบั๊กเป็นวันๆ กว่าจะเจอว่าแค่ลืมใส่ตัวแปรบางตัวเข้าไปใน Array

5.1. สิ่งที่ต้องระวังในการใส่ Dependency Array

1. ต้องใส่ Dependencies ให้ครบ: ถ้าฟังก์ชันหรือการคำนวณของคุณใช้ค่าตัวแปร, state, props, หรือฟังก์ชันอื่นๆ ที่มาจากนอกขอบเขตของ useCallback/useMemo คุณต้องใส่สิ่งเหล่านั้นลงไปใน Dependency Array ให้ครบถ้วนเสมอ มิฉะนั้นฟังก์ชันหรือค่าที่ Memoize ไว้จะใช้ข้อมูลเก่า ทำให้เกิดบั๊กได้ง่ายๆ ลองนึกภาพว่าคุณมีฟังก์ชัน ที่ใช้ ถ้าคุณไม่ใส่ ใน Dependency Array ของ ฟังก์ชัน ก็จะใช้ อันเก่าเสมอ ทำให้เพิ่มสินค้าลงตะกร้าได้ไม่ถูกต้อง
2.

ระวังการวนลูปของ Dependencies: บางครั้งคุณอาจจะเจอสถานการณ์ที่ฟังก์ชัน A ต้องใช้ฟังก์ชัน B และฟังก์ชัน B ก็ต้องใช้ฟังก์ชัน A ทำให้เกิดการวนลูปของ Dependencies ในกรณีแบบนี้คุณอาจจะต้องพิจารณา refactor โค้ด หรือใช้ เข้ามาช่วยเก็บ Reference ของค่าที่ไม่เปลี่ยนบ่อย
3.

อย่าใส่ Object หรือ Array ที่สร้างใหม่เป็น Dependency โดยตรง: หากคุณใส่ Object หรือ Array เข้าไปใน Dependency Array โดยตรง และ Object/Array นั้นถูกสร้างขึ้นมาใหม่ทุกครั้งที่ Parent Component Re-render Hook ก็จะมองว่า Dependency เปลี่ยน และทำการสร้างฟังก์ชันหรือค่าใหม่ทุกครั้ง ทำให้การ Memoize ไม่มีผล คุณควรใช้ค่า Primitive Types (string, number, boolean) หรือถ้าจำเป็นต้องใช้ Object/Array ก็ควร Memoize Object/Array นั้นด้วย useMemo ก่อนแล้วค่อยนำมาใส่ใน Dependency Array

การวัดผลและการติดตาม: รู้ได้อย่างไรว่าดีขึ้นจริง?

การ Optimize เป็นเรื่องของการปรับปรุงประสิทธิภาพ แต่เราจะรู้ได้อย่างไรว่าสิ่งที่เราทำไปนั้นมัน “ดีขึ้นจริง” หรือเปล่า? การวัดผลนี่แหละครับคือคำตอบ ไม่ใช่แค่ความรู้สึกว่า “น่าจะเร็วขึ้นนะ” แต่เป็นการมีข้อมูลตัวเลขมาสนับสนุน เหมือนกับการลงทุน เราต้องดู Return on Investment (ROI) ด้วย ไม่ใช่แค่ลงทุนไปเรื่อยๆ โดยไม่ดูผลตอบแทน

6.1. ใช้ Chrome DevTools Performance Tab

เครื่องมือที่ผมใช้บ่อยที่สุดและขาดไม่ได้เลยคือ Performance Tab ใน Chrome DevTools ครับ มันช่วยให้เราเห็นภาพรวมของสิ่งต่างๆ ที่เกิดขึ้นในแอปพลิเคชันของเราได้อย่างละเอียด ไม่ว่าจะเป็น
* CPU Usage: ดูว่าส่วนไหนของโค้ดที่ใช้ CPU มากที่สุด
* Rendering: ดูว่ามี Layout Shift หรือ Paint ที่ไม่จำเป็นหรือไม่
* Scripting: ดูว่า JavaScript ทำงานอะไรบ้าง และใช้เวลานานแค่ไหน
* Flame Chart: แสดง Call Stack ของฟังก์ชันต่างๆ ที่ถูกเรียก ทำให้เราสามารถระบุจุดคอขวดที่เกิดการ Re-render หรือการคำนวณที่กินเวลาได้ง่ายๆ ครับ ผมมักจะใช้ตัวนี้เพื่อหาว่าคอมโพเนนต์ไหนที่ Re-render บ่อยเกินไป และมันกินทรัพยากรแค่ไหน

6.2. React Developer Tools Profiler

ถ้าคุณพัฒนาด้วย React เครื่องมือนี้คือของขวัญครับ! React Developer Tools มีแท็บ Profiler ที่ช่วยให้เราสามารถบันทึกการ Re-render ของคอมโพเนนต์ต่างๆ ใน Tree ได้อย่างชัดเจน คุณจะเห็นว่าคอมโพเนนต์ไหนที่ Re-render, ทำไมถึง Re-render (เช่น “Props changed,” “State changed”) และใช้เวลานานเท่าไหร่ในการ Re-render แต่ละครั้ง มันช่วยให้ผม pinpoint ปัญหาได้แม่นยำมาก ไม่ต้องมานั่งเดาเลยว่าต้นตอของความช้าอยู่ตรงไหน

6.3. การเปรียบเทียบก่อนและหลังการ Optimize

หลังจากที่คุณทำการ Optimize ด้วย React.memo, useCallback, หรือ useMemo แล้ว สิ่งสำคัญคือการเปรียบเทียบประสิทธิภาพก่อนและหลังการเปลี่ยนแปลง เพื่อให้แน่ใจว่าสิ่งที่คุณทำไปนั้นได้ผลจริง ผมมักจะทำ A/B Testing ในการวัดผล เช่น การใช้ Lighthouse Score เพื่อดูคะแนน Core Web Vitals (Largest Contentful Paint, Cumulative Layout Shift, First Input Delay) หรือการวัด FPS (Frames Per Second) ในการ Interaction ที่ซับซ้อน ถ้าตัวเลขดีขึ้นอย่างเห็นได้ชัด นั่นหมายความว่าคุณมาถูกทางแล้วครับ!

การ Optimize ไม่ใช่การ “ทำไปงั้นๆ” แต่เป็นการ “ทำอย่างมีกลยุทธ์และวัดผลได้” เพื่อให้แอปพลิเคชันของเราไม่ใช่แค่ทำงานได้ แต่ต้องทำงานได้อย่างมีประสิทธิภาพสูงสุดด้วยครับ.

หวังว่าบทความนี้จะช่วยให้คุณเข้าใจถึงปัญหา Re-render ที่ไม่ได้ตั้งใจและผลกระทบของมันได้ลึกซึ้งยิ่งขึ้นนะครับ การลดการ Re-render ที่ไม่จำเป็นไม่ใช่แค่ทำให้แอปพลิเคชันของคุณเร็วขึ้นเท่านั้น แต่ยังช่วยประหยัดทรัพยากรของผู้ใช้งานและมอบประสบการณ์ที่ดีขึ้นอย่างมหาศาลให้กับพวกเขาอีกด้วย

จำไว้เสมอว่าการ Optimization ไม่ใช่การทำทุกอย่างให้เร็วที่สุดเท่าที่จะทำได้ แต่เป็นการเลือกใช้เครื่องมืออย่าง React.memo, useCallback, และ useMemo อย่างถูกจังหวะและเหมาะสมกับสถานการณ์ พร้อมทั้งวัดผลลัพธ์ด้วยเครื่องมือต่างๆ อย่าง Chrome DevTools และ React Developer Tools Profiler เพื่อให้แน่ใจว่าสิ่งที่คุณทำไปนั้นได้ผลจริงครับ

เริ่มต้นจากการระบุจุดคอขวดที่แท้จริง ก่อนที่จะลงมือปรับปรุง เพียงเท่านี้คุณก็จะสามารถสร้างแอปพลิเคชัน React ที่ทั้งเร็วและลื่นไหลได้อย่างแน่นอน ขอให้สนุกกับการเขียนโค้ดนะครับ!

ข้อมูลน่ารู้

1. หลีกเลี่ยงการสร้าง Object/Array ใหม่ใน Render Function: การสร้าง Object หรือ Array ใหม่ในทุกๆ รอบของการ Render จะทำให้ Prop ที่ส่งไปหา Child Component มี Reference ใหม่เสมอ แม้ข้อมูลข้างในจะเหมือนเดิมก็ตาม ซึ่งจะทำให้ Child Component Re-render โดยไม่จำเป็น ควรสร้างนอก Component หรือใช้ .

2. ใช้ สำหรับค่าที่ไม่ต้องการให้ Re-render: หากคุณมีค่าบางอย่างที่ต้องอัปเดตแต่ไม่ต้องการให้มันไปกระตุ้นให้ Component Re-render เช่น ตัวแปรสำหรับเก็บ Timeout ID หรือ Scroll Position, เป็นทางเลือกที่ดีครับ เพราะมันจะไม่ทำให้ Component Re-render เมื่อค่าใน ของมันเปลี่ยนแปลง.

3. Lazy Loading Components ด้วย และ : สำหรับแอปพลิเคชันขนาดใหญ่ การแบ่งโค้ด (Code Splitting) และโหลด Component เฉพาะตอนที่จำเป็นเท่านั้น จะช่วยลดขนาด Bundle Size และทำให้ Initial Load Time เร็วขึ้นอย่างมากครับ.

4. การใช้ List Virtualization สำหรับรายการที่ยาวมากๆ: หากคุณมี List ที่มี Item เป็นร้อยเป็นพัน ควรพิจารณาใช้ไลบรารีอย่าง หรือ เพื่อ Render เฉพาะ Item ที่มองเห็นบนหน้าจอเท่านั้น ช่วยประหยัดทรัพยากรได้อย่างมหาศาล.

5. Debounce หรือ Throttle Event Handlers: สำหรับ Event ที่เกิดขึ้นบ่อยๆ เช่น , , หรือ ในช่อง Input ที่มีการค้นหา ควรใช้เทคนิค Debouncing หรือ Throttling เพื่อจำกัดจำนวนครั้งที่ฟังก์ชันเหล่านั้นถูกเรียก ช่วยลดการ Re-render และการคำนวณที่ไม่จำเป็น.

สรุปประเด็นสำคัญ

ปัญหา Component Re-render บ่อยครั้งมักเกิดจากการเปลี่ยนแปลงของ Props, State, หรือ Context ซึ่งส่งผลกระทบโดยตรงต่อประสิทธิภาพและประสบการณ์ผู้ใช้ การแก้ปัญหานี้ทำได้โดยใช้ React.memo เพื่อป้องกัน Component Re-render, useCallback เพื่อจดจำฟังก์ชัน, และ useMemo เพื่อจดจำค่าที่ซับซ้อน การจัดการ Dependency Array อย่างถูกต้องเป็นสิ่งสำคัญ และอย่าลืมวัดผลด้วยเครื่องมืออย่าง Chrome DevTools และ React Developer Tools Profiler เพื่อยืนยันประสิทธิภาพที่แท้จริง

คำถามที่พบบ่อย (FAQ) 📖

ถาม: จากประสบการณ์ที่คุณเล่ามา การ “Re-render” ที่เกิดขึ้นบ่อยครั้งเกินจำเป็นใน JavaScript Component คืออะไรกันแน่ครับ และทำไมมันถึงเป็นปัญหาใหญ่ที่กระทบประสิทธิภาพเว็บไซต์ของเราได้ถึงขนาดนั้น?

ตอบ: อื้อหือ คำถามนี้โดนใจนักพัฒนาหลายๆ คนเลยครับ เพราะผมเองก็เคยปวดหัวกับเรื่องนี้มาเยอะจนเข้าใจซึ้งเลยว่ามันน่าหงุดหงิดแค่ไหน ลองนึกภาพแบบนี้นะครับ เวลาที่เราสร้างแอปพลิเคชันขึ้นมา คอมโพเนนต์ต่างๆ ที่เราเขียน (เช่น ปุ่ม, กล่องข้อความ, รูปภาพ) มันก็เหมือนชิ้นส่วนที่ประกอบกันเป็นหน้าจอ ทีนี้ “Re-render” ก็คือกระบวนการที่ React หรือไลบรารีอื่นๆ มันสร้างหรืออัปเดตหน้าตาของคอมโพเนนต์เหล่านั้นขึ้นมาใหม่ เพื่อให้มันแสดงผลข้อมูลล่าสุด หรือตอบสนองต่อการเปลี่ยนแปลงต่างๆปัญหาคือ บางครั้งมันก็ “Re-render” ทั้งๆ ที่ข้อมูลที่เกี่ยวข้องกับคอมโพเนนต์นั้นๆ ไม่ได้เปลี่ยนไปเลยสักนิดเดียวครับ เหมือนเรากำลังจะทำกับข้าวแล้วเดินไปดูตู้เย็นบ่อยๆ ทั้งๆ ที่รู้ว่าไม่มีอะไรเปลี่ยนนั่นแหละครับ พอคอมโพเนนต์ต้อง Re-render บ่อยๆ โดยไม่จำเป็น มันก็ต้องใช้ทรัพยากรของเครื่องมากขึ้น ไม่ว่าจะเป็น CPU หรือ RAM เหมือนมีคนมาวิ่งวนๆ ในบ้านเราเยอะๆ ทั้งๆ ที่ไม่ได้มีอะไรต้องทำจริงๆ ผลก็คือ เว็บไซต์ของเราก็เลยช้าลง ประมวลผลได้ไม่ทันใจผู้ใช้งาน หน้าจออาจจะกระตุก กดแล้วหน่วง แถมยังเปลืองแบตเตอรี่ของอุปกรณ์ผู้ใช้อีกด้วยครับ ยิ่งแอปพลิเคชันเราซับซ้อนมากเท่าไหร่ มีคอมโพเนนต์เยอะเท่าไหร่ การ Re-render ที่ไม่จำเป็นก็จะยิ่งเป็นตัวการบั่นทอนประสิทธิภาพอย่างมหาศาลเลยครับ บอกเลยว่าความรู้สึกที่ผู้ใช้กดปิดเว็บเราไปเพราะโหลดช้านี่มันเจ็บปวดสุดๆ ครับ

ถาม: คุณพูดถึง React.memo, useCallback, useMemo ในฐานะเครื่องมือสำคัญที่ช่วยแก้ปัญหา Re-render ได้ อยากทราบว่าแต่ละตัวมันทำงานยังไง และเราควรเลือกใช้ตัวไหนในสถานการณ์แบบไหนครับ?

ตอบ: ใช่ครับ! สามตัวนี้แหละคือเหมือนไม้เท้ากายสิทธิ์ของนักพัฒนา React เลยก็ว่าได้ครับ จากที่ผมลองผิดลองถูกมาเยอะจนเจอจุดที่พอดี ผมมองว่ามันทำงานคล้ายๆ กับการที่เราบอกคอมโพเนนต์ว่า “เฮ้ย ถ้าข้อมูลข้างในไม่เปลี่ยน ไม่ต้องวาดใหม่นะ!”: ตัวนี้จะใช้หุ้มคอมโพเนนต์ที่เราต้องการให้ React จำไว้ว่า “ถ้า props (ข้อมูลที่ส่งเข้ามาให้คอมโพเนนต์) มันยังเหมือนเดิมเป๊ะๆ ก็ไม่ต้อง Re-render นะ ใช้ของเก่าที่เคยสร้างไว้ได้เลย” มันเหมาะมากกับคอมโพเนนต์ที่แสดงผลข้อมูลคงที่ หรือเปลี่ยนไม่บ่อย เช่น คอมโพเนนต์ Header หรือ Footer หรือแม้แต่ Item ลิสต์ต่างๆ ถ้าข้อมูลของ Item นั้นๆ ไม่เปลี่ยน ก็ไม่จำเป็นต้อง Re-render ทั้งหมดครับ การใช้ เหมือนเป็นการติดป้ายหน้าคอมโพเนนต์ว่า “เช็คก่อน ถ้าไม่มีอะไรเปลี่ยน ไม่ต้องเปิดประตูเข้ามานะ”: ทีนี้ปัญหาที่ซับซ้อนขึ้นมาหน่อยคือ ถ้าเราส่งฟังก์ชันเป็น props ให้กับคอมโพเนนต์ลูก เช่น ส่งฟังก์ชัน ให้ปุ่ม คอมโพเนนต์ลูกที่ ไว้ก็อาจจะยัง Re-render อยู่ดี เพราะ JavaScript มองว่าฟังก์ชันที่ถูกสร้างขึ้นมาใหม่ (แม้จะโค้ดเหมือนเดิม) ก็คือคนละตัวกันครับ ก็เลยมาช่วยแก้ตรงนี้ โดยมันจะ “จดจำ” ฟังก์ชันที่เราสร้างขึ้นมา ไม่ให้สร้างใหม่ทุกครั้งที่คอมโพเนนต์แม่ Re-render ตราบใดที่ dependencies (ตัวแปรที่ฟังก์ชันนั้นๆ อ้างอิง) ไม่เปลี่ยน มันเหมาะมากกับการส่งฟังก์ชันให้คอมโพเนนต์ลูกที่ถูก หุ้มไว้ เพื่อให้การป้องกัน Re-render ได้ผลเต็มที่ครับ: คล้ายๆ ครับ แต่ ไม่ได้ใช้จดจำฟังก์ชัน แต่ใช้จดจำ “ค่า” ที่ได้จากการคำนวณที่ซับซ้อน เช่น การคำนวณข้อมูลปริมาณมากๆ หรือการกรองข้อมูลที่ใช้เวลานานๆ แทนที่จะคำนวณใหม่ทุกครั้งที่คอมโพเนนต์ Re-render เราก็ใช้ หุ้มการคำนวณนั้นไว้ มันจะคำนวณแค่ครั้งแรก หรือเมื่อค่า dependencies ที่เกี่ยวข้องเปลี่ยนไปเท่านั้นเองครับ คิดซะว่า คือการทำ “แคช” ผลลัพธ์ของการคำนวณเพื่อไม่ต้องคำนวณซ้ำนั่นเองการใช้ทั้งสามตัวนี้อย่างถูกจังหวะเวลา จะช่วยลดภาระการทำงานของแอปพลิเคชันได้เยอะมากครับ เหมือนเราวางแผนการทำงานในบ้านให้มีประสิทธิภาพ ไม่ต้องเดินไปมาซ้ำซ้อน ทำให้ทุกอย่างลื่นไหลและประหยัดพลังงานขึ้นเยอะเลยครับ

ถาม: ในฐานะนักพัฒนา เราจะรู้ได้อย่างไรครับว่าคอมโพเนนต์ตัวไหนในแอปพลิเคชันของเรากำลัง “Re-render” บ่อยเกินจำเป็น และมีเครื่องมือหรือวิธีการไหนที่ช่วยให้เราตรวจสอบและระบุปัญหาเหล่านี้ได้อย่างแม่นยำบ้างไหมครับ?

ตอบ: คำถามนี้เป็นหัวใจสำคัญของการแก้ไขปัญหาเลยครับ เพราะถ้าเราไม่รู้ว่าอะไรคือตัวปัญหา เราก็แก้ไม่ถูกจุด เหมือนหาเข็มในมหาสมุทรเลยครับ จากประสบการณ์ตรงของผม การจับสัญญาณ Re-render ที่ไม่จำเป็นนี่แหละคือจุดเริ่มต้นสู่ประสิทธิภาพที่แท้จริงวิธีที่ผมใช้บ่อยและแนะนำให้นักพัฒนาทุกคนติดตั้งไว้เลยคือ ที่เป็น Extension ของ Browser ครับ ตัวนี้เทพมาก!
มันจะมีฟีเจอร์ที่เรียกว่า “Highlight Updates” ครับ พอกดเปิดใช้งานปุ๊บ เวลาคอมโพเนนต์ไหนมัน Re-render บนหน้าจอ มันจะขึ้นกรอบสีเขียวๆ รอบๆ คอมโพเนนต์นั้นๆ แวบนึงครับ ถ้าเห็นคอมโพเนนต์บางตัวที่ไม่ได้มีการเปลี่ยนแปลงข้อมูลเลย แต่กลับมีกรอบสีเขียวๆ ขึ้นมาบ่อยๆ นั่นแหละครับคือสัญญาณเตือนว่า “เฮ้ย!
มีอะไรผิดปกติที่นี่นะ!” บางทีมันก็ Re-render ถี่มากจนเราเห็นเป็นสีเหลืองหรือแดงเลยก็มี นั่นยิ่งอาการหนักเลยครับนอกจากนี้ ใน React Dev Tools ยังมี Tab “Profiler” ที่ช่วยให้เราบันทึก Performance ของแอปพลิเคชันได้อีกด้วยครับ เราสามารถกด Record แล้วลองใช้งานเว็บไซต์ของเรา แล้วหลังจากนั้นมันจะแสดงผลออกมาเป็นกราฟว่าคอมโพเนนต์แต่ละตัวใช้เวลาในการ render ไปเท่าไหร่ และ Re-render กี่ครั้ง มันช่วยให้เราเห็นภาพรวมและเจาะจงไปที่คอมโพเนนต์ที่ใช้ทรัพยากรสูงๆ ได้เลยครับอีกวิธีง่ายๆ แต่ได้ผลดีคือการ ในคอมโพเนนต์ที่เราสงสัยครับ เช่น ใส่ ไว้ในฟังก์ชันคอมโพเนนต์ หรือใน ที่ไม่มี dependencies แล้วเปิด Developer Console ดูครับ ถ้าเห็นข้อความนี้ขึ้นมาบ่อยๆ ทั้งๆ ที่ไม่น่าจะขึ้น นั่นก็เป็นอีกสัญญาณที่บอกว่าคอมโพเนนต์นั้นกำลัง Re-render เกินจำเป็นครับการใช้เครื่องมือเหล่านี้จะช่วยให้เราเห็นภาพปัญหาได้ชัดเจนขึ้นเยอะครับ พอเรารู้ว่าตัวไหนเป็นปัญหา เราก็จะสามารถนำ , , เข้าไปประยุกต์ใช้ได้อย่างถูกจุด ทำให้แอปพลิเคชันกลับมาลื่นไหลเหมือนเดิม ความรู้สึกตอนที่เห็นกรอบเขียวๆ หายไปหรือขึ้นน้อยลงนี่มันโล่งใจอย่างบอกไม่ถูกเลยครับ เหมือนแก้ปัญหาที่ค้างคาใจได้สำเร็จ!

📚 อ้างอิง