ทำตามแนวทางปฏิบัติแนะนำ

คุณอาจพบข้อผิดพลาดที่พบบ่อยใน 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 ก่อนหน้า

แหล่งข้อมูลเพิ่มเติม