คุณอาจพบข้อผิดพลาดที่พบบ่อยใน Compose ข้อผิดพลาดเหล่านี้อาจทำให้คุณได้โค้ด ที่ดูเหมือนจะทำงานได้ดีพอ แต่ก็อาจส่งผลเสียต่อประสิทธิภาพของ UI ทําตามแนวทางปฏิบัติแนะนําเพื่อเพิ่มประสิทธิภาพแอปใน Compose
ใช้ remember เพื่อลดการคำนวณที่มีค่าใช้จ่ายสูง
ฟังก์ชันที่ประกอบกันได้อาจทำงานบ่อยมาก ซึ่งอาจทำงานทุกเฟรม ของภาพเคลื่อนไหว ด้วยเหตุนี้ คุณจึงควรทำการคำนวณใน ส่วนเนื้อหาของฟังก์ชันที่ประกอบกันได้ให้น้อยที่สุด
เทคนิคสำคัญคือจัดเก็บผลลัพธ์ของการคำนวณด้วย
remember วิธีนี้จะช่วยให้การคำนวณทำงานเพียงครั้งเดียว และคุณสามารถดึงข้อมูล
ผลลัพธ์ได้ทุกเมื่อที่ต้องการ
ตัวอย่างเช่น โค้ดต่อไปนี้แสดงรายการชื่อที่จัดเรียงแล้ว แต่จะจัดเรียงด้วยวิธีที่มีค่าใช้จ่ายสูงมาก
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { LazyColumn(modifier) { // DON’T DO THIS items(contacts.sortedWith(comparator)) { contact -> // ... } } }
ทุกครั้งที่ ContactsList มีการจัดเรียงใหม่ ระบบจะจัดเรียงรายชื่อติดต่อทั้งหมดอีกครั้ง แม้ว่ารายชื่อจะไม่มีการเปลี่ยนแปลงก็ตาม หากผู้ใช้เลื่อนรายการ
ระบบจะทำการรวม Composable อีกครั้งทุกครั้งที่แถวใหม่ปรากฏขึ้น
หากต้องการแก้ปัญหานี้ ให้จัดเรียงรายการภายนอก LazyColumn แล้วจัดเก็บ
รายการที่จัดเรียงแล้วด้วย remember ดังนี้
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { val sortedContacts = remember(contacts, comparator) { contacts.sortedWith(comparator) } LazyColumn(modifier) { items(sortedContacts) { // ... } } }
ตอนนี้ระบบจะจัดเรียงรายการเพียงครั้งเดียวเมื่อเขียน ContactList เป็นครั้งแรก หากมีการเปลี่ยนแปลง
รายชื่อติดต่อหรือตัวเปรียบเทียบ ระบบจะสร้างรายการที่จัดเรียงใหม่ ไม่เช่นนั้น
Composable จะใช้รายการที่จัดเรียงที่แคชไว้ต่อไปได้
ใช้แป้นเลย์เอาต์แบบขี้เกียจ
เลย์เอาต์แบบ Lazy จะนำรายการกลับมาใช้ซ้ำอย่างมีประสิทธิภาพ โดยจะสร้างใหม่หรือจัดองค์ประกอบใหม่ เมื่อจำเป็นเท่านั้น อย่างไรก็ตาม คุณช่วยเพิ่มประสิทธิภาพเลย์เอาต์แบบ Lazy สำหรับการจัดองค์ประกอบใหม่ได้
สมมติว่าการดำเนินการของผู้ใช้ทําให้รายการย้ายไปอยู่ในรายการอื่น เช่น สมมติว่าคุณแสดงรายการโน้ตที่จัดเรียงตามเวลาที่แก้ไข โดยมีโน้ตที่แก้ไข ล่าสุดอยู่ด้านบน
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes ) { note -> NoteRow(note) } } }
แต่โค้ดนี้มีปัญหา สมมติว่ามีการเปลี่ยนแปลงหมายเหตุท้าย ตอนนี้โน้ตดังกล่าวเป็นโน้ตที่แก้ไขล่าสุด จึงจะไปอยู่ด้านบนสุดของรายการ และ โน้ตอื่นๆ ทั้งหมดจะเลื่อนลงไป 1 ตำแหน่ง
หากไม่มีความช่วยเหลือจากคุณ Compose จะไม่ทราบว่ารายการที่ไม่มีการเปลี่ยนแปลงเป็นเพียงการย้ายในลิสต์ แต่ Compose จะคิดว่า "รายการ 2" เก่าถูกลบไปแล้วและมีการสร้างรายการใหม่สำหรับรายการ 3, รายการ 4 และรายการอื่นๆ ทั้งหมด ผลลัพธ์คือ Compose จะทำการ ประกอบใหม่ทุกรายการในลิสต์ แม้ว่าจะมีเพียงรายการเดียวที่ มีการเปลี่ยนแปลงจริงก็ตาม
วิธีแก้ปัญหานี้คือระบุคีย์ของรายการ การระบุคีย์ที่เสถียรสำหรับ แต่ละรายการจะช่วยให้ Compose ไม่ต้องจัดองค์ประกอบใหม่โดยไม่จำเป็น ในกรณีนี้ Compose สามารถระบุได้ว่ารายการที่อยู่ตำแหน่งที่ 3 ในตอนนี้คือรายการเดียวกันกับที่เคยอยู่ตำแหน่งที่ 2 เนื่องจากไม่มีการเปลี่ยนแปลงข้อมูลของรายการนั้น Compose จึงไม่ต้อง จัดองค์ประกอบใหม่
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes, key = { note -> // Return a stable, unique key for the note note.id } ) { note -> NoteRow(note) } } }
ใช้ derivedStateOf เพื่อจำกัดการจัดองค์ประกอบใหม่
ความเสี่ยงอย่างหนึ่งของการใช้สถานะใน Composable คือหากสถานะเปลี่ยนแปลงอย่างรวดเร็ว UI อาจได้รับการจัดองค์ประกอบใหม่มากกว่าที่จำเป็น เช่น สมมติว่าคุณกำลังแสดงรายการที่เลื่อนได้ คุณตรวจสอบสถานะของรายการเพื่อดูว่า รายการใดเป็นรายการแรกที่มองเห็นได้ในรายการ
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton = listState.firstVisibleItemIndex > 0 AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
ปัญหาที่เกิดขึ้นคือ หากผู้ใช้เลื่อนรายการ listState จะเปลี่ยนไปเรื่อยๆ
ขณะที่ผู้ใช้ลากนิ้ว ซึ่งหมายความว่าระบบจะ
จัดองค์ประกอบรายการใหม่เรื่อยๆ แต่จริงๆ แล้วคุณไม่จำเป็นต้องจัดองค์ประกอบใหม่บ่อยขนาดนั้น คุณไม่จำเป็นต้องจัดองค์ประกอบใหม่จนกว่าจะมีรายการใหม่ปรากฏที่ด้านล่าง ดังนั้น
การคำนวณเพิ่มเติมจำนวนมากนี้จึงทำให้ UI ทำงานได้ไม่ดี
วิธีแก้ปัญหาคือการใช้สถานะที่ได้มา สถานะที่ได้มาช่วยให้คุณบอก Compose ได้ว่า การเปลี่ยนแปลงสถานะใดที่ควรทําให้เกิดการจัดองค์ประกอบใหม่ ในกรณีนี้ ให้ระบุว่าคุณสนใจเมื่อรายการแรกที่มองเห็นมีการเปลี่ยนแปลง เมื่อค่าสถานะนั้น เปลี่ยนแปลง UI จะต้องจัดองค์ประกอบใหม่ แต่หากผู้ใช้ยังไม่ได้ เลื่อนมากพอที่จะนำรายการใหม่ไปไว้ด้านบน ก็ไม่จำเป็นต้องจัดองค์ประกอบใหม่
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
เลื่อนการอ่านให้นานที่สุด
เมื่อพบปัญหาด้านประสิทธิภาพ การเลื่อนการอ่านสถานะจะช่วยได้ การเลื่อนการอ่านสถานะจะช่วยให้มั่นใจได้ว่า Compose จะเรียกใช้โค้ดที่จำเป็นขั้นต่ำ ในการจัดองค์ประกอบใหม่ เช่น หาก UI มีสถานะที่ยกขึ้นไปสูงใน ทรี Composable และคุณอ่านสถานะใน Composable ย่อย คุณจะห่อหุ้ม การอ่านสถานะในฟังก์ชัน Lambda ได้ การทำเช่นนี้จะทำให้การอ่านเกิดขึ้นเมื่อจำเป็นเท่านั้น โปรดดูการติดตั้งใช้งานในแอปตัวอย่าง Jetsnackเพื่อเป็นข้อมูลอ้างอิง Jetsnack ใช้เอฟเฟกต์คล้ายแถบเครื่องมือแบบยุบได้ ในหน้าจอรายละเอียด หากต้องการทราบเหตุผลที่เทคนิคนี้ได้ผล โปรดดูบล็อกโพสต์Jetpack Compose: การแก้ไขข้อบกพร่องของการจัดองค์ประกอบใหม่
หากต้องการให้ได้เอฟเฟกต์นี้ Title ที่ประกอบได้จะต้องมีออฟเซ็ตการเลื่อน
เพื่อชดเชยตัวเองโดยใช้ Modifier ต่อไปนี้คือโค้ด Jetsnack เวอร์ชันที่ง่ายขึ้นก่อนที่จะมีการเพิ่มประสิทธิภาพ
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack, scroll.value) // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scroll: Int) { // ... val offset = with(LocalDensity.current) { scroll.toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
เมื่อสถานะการเลื่อนเปลี่ยนแปลง Compose จะทําให้ขอบเขตการเขียนใหม่ขององค์ประกอบระดับบนสุดที่ใกล้ที่สุดไม่ถูกต้อง ในกรณีนี้ ขอบเขตที่ใกล้ที่สุดคือ Composable SnackDetail โปรดทราบว่า Box เป็นฟังก์ชันอินไลน์ จึงไม่ใช่ขอบเขตการเขียนใหม่
ดังนั้น Compose จะเขียน SnackDetail และ Composable ใดๆ ภายใน SnackDetail ใหม่ หากคุณเปลี่ยนโค้ดให้อ่านเฉพาะสถานะที่คุณใช้จริง
คุณจะลดจํานวนองค์ประกอบที่ต้องเขียนใหม่ได้
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack) { scroll.value } // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... val offset = with(LocalDensity.current) { scrollProvider().toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
ตอนนี้พารามิเตอร์การเลื่อนเป็น Lambda แล้ว ซึ่งหมายความว่า Title ยังคงอ้างอิงสถานะที่ยกขึ้นได้ แต่จะอ่านค่าได้เฉพาะภายใน Title เท่านั้น ซึ่งเป็นที่ที่จำเป็นต้องใช้ค่าดังกล่าวจริงๆ ด้วยเหตุนี้ เมื่อค่าการเลื่อนเปลี่ยนแปลง ขอบเขตการจัดองค์ประกอบใหม่ที่ใกล้ที่สุดจึงเป็น Title composable–Compose ไม่จำเป็นต้องจัดองค์ประกอบใหม่ทั้ง Box อีกต่อไป
นี่เป็นการปรับปรุงที่ดี แต่คุณทำได้ดีกว่านี้ คุณควรระมัดระวังหาก
คุณทำให้เกิดการจัดองค์ประกอบใหม่เพียงเพื่อจัดเลย์เอาต์หรือวาด Composable ใหม่ ใน
กรณีนี้ สิ่งที่คุณทำคือการเปลี่ยนออฟเซ็ตของ Titlecomposable
ซึ่งสามารถทำได้ในระยะเลย์เอาต์
@Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... Column( modifier = Modifier .offset { IntOffset(x = 0, y = scrollProvider()) } ) { // ... } }
ก่อนหน้านี้ โค้ดใช้ Modifier.offset(x: Dp, y: Dp) ซึ่งใช้
ออฟเซ็ตเป็นพารามิเตอร์ การเปลี่ยนไปใช้ตัวแก้ไขเวอร์ชัน Lambda
จะช่วยให้มั่นใจได้ว่าฟังก์ชันจะอ่านสถานะการเลื่อนในระยะเลย์เอาต์ ด้วยเหตุนี้ เมื่อสถานะการเลื่อนเปลี่ยนแปลง Compose จึงข้ามเฟสการจัดองค์ประกอบไปได้ทั้งหมด และไปยังเฟสเลย์เอาต์ได้โดยตรง เมื่อส่งตัวแปรสถานะที่เปลี่ยนแปลงบ่อย
ไปยังตัวแก้ไข คุณควรใช้ตัวแก้ไขเวอร์ชัน Lambda ทุกครั้งที่ทำได้
นี่คืออีกตัวอย่างหนึ่งของแนวทางนี้ โค้ดนี้ยังไม่ได้เพิ่มประสิทธิภาพ
// Here, assume animateColorBetween() is a function that swaps between // two colors val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .background(color) )
ในที่นี้ สีพื้นหลังของกล่องจะเปลี่ยนไปมาระหว่าง 2 สีอย่างรวดเร็ว ดังนั้น สถานะนี้จึงเปลี่ยนแปลงบ่อยมาก จากนั้นฟังก์ชันที่ประกอบกันได้จะอ่านสถานะนี้ใน ตัวแก้ไขพื้นหลัง ด้วยเหตุนี้ กล่องจึงต้องจัดองค์ประกอบใหม่ในทุกเฟรม เนื่องจากสีจะเปลี่ยนในทุกเฟรม
หากต้องการปรับปรุงส่วนนี้ ให้ใช้ตัวปรับแต่งที่อิงตาม Lambda ซึ่งในกรณีนี้คือ drawBehind
ซึ่งหมายความว่าระบบจะอ่านสถานะสีในระหว่างเฟสการวาดเท่านั้น ด้วยเหตุนี้
Compose จึงข้ามเฟสการจัดองค์ประกอบและเลย์เอาต์ไปได้เลย เมื่อสีมีการเปลี่ยนแปลง Compose จะไปยังเฟสการวาดโดยตรง
val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .drawBehind { drawRect(color) } )
หลีกเลี่ยงการเขียนย้อนหลัง
Compose มีสมมติฐานหลักว่าคุณจะไม่เขียนไปยังสถานะที่อ่านแล้ว เมื่อทำเช่นนี้ จะเรียกว่าการเขียนย้อนกลับ และอาจทำให้เกิดการจัดองค์ประกอบใหม่ในทุกเฟรมอย่างไม่สิ้นสุด
Composable ต่อไปนี้แสดงตัวอย่างข้อผิดพลาดประเภทนี้
@Composable fun BadComposable() { var count by remember { mutableIntStateOf(0) } // Causes recomposition on click Button(onClick = { count++ }, Modifier.wrapContentSize()) { Text("Recompose") } Text("$count") count++ // Backwards write, writing to state after it has been read</b> }
โค้ดนี้จะอัปเดตจำนวนที่ส่วนท้ายของ Composable หลังจากอ่านในบรรทัดก่อนหน้า หากเรียกใช้โค้ดนี้ คุณจะเห็นว่าหลังจากคลิกปุ่ม ซึ่งทำให้เกิดการจัดองค์ประกอบใหม่ ตัวนับจะเพิ่มขึ้นอย่างรวดเร็วใน ลูปที่ไม่มีที่สิ้นสุด เนื่องจาก Compose จัดองค์ประกอบใหม่ของ Composable นี้ เห็นการอ่านสถานะที่ ล้าสมัย และจึงกำหนดเวลาการจัดองค์ประกอบใหม่อีกครั้ง
คุณหลีกเลี่ยงการเขียนย้อนกลับได้โดยไม่เขียนไปยังสถานะใน
Composition หากเป็นไปได้ ให้เขียนไปยังสถานะเสมอเพื่อตอบสนองต่อเหตุการณ์
และใน Lambda เช่น ในตัวอย่าง onClick ก่อนหน้า
แหล่งข้อมูลเพิ่มเติม
- คำแนะนำด้านประสิทธิภาพของแอป: ค้นพบแนวทางปฏิบัติ ไลบรารี และเครื่องมือที่ดีที่สุดเพื่อปรับปรุงประสิทธิภาพใน Android
- ตรวจสอบประสิทธิภาพ: ตรวจสอบประสิทธิภาพของแอป
- การเปรียบเทียบ: เปรียบเทียบประสิทธิภาพของแอป
- การเริ่มต้นแอป: เพิ่มประสิทธิภาพการเริ่มต้นแอป
- โปรไฟล์พื้นฐาน: ทำความเข้าใจโปรไฟล์พื้นฐาน
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- สถานะและ Jetpack Compose
- ตัวปรับแต่งกราฟิก
- การคิดใน Compose