소스 구문만으로 판정한다. 빌드·외부도구 없이 즉시 동작하고, 마스킹·스코프 게이트로 오탐을 차단한다.
판정 기준 — key 없는 items() 는 아이템 삽입/리오더 시 위치 기반 매칭이 어긋나 전체 아이템이 재구성되고 아이템 상태(remember)가 잘못 재사용된다.
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/lists#item-keys
판정 기준 — index 를 key 로 쓰면 key 없는 것과 동일하게 리오더/삽입 시 전 아이템이 밀려 재구성된다 — key 의 목적(아이템 정체성 추적)을 무효화.
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/lists#item-keys
판정 기준 — 한 Lazy 스코프에 서로 다른 아이템 종류가 섞였는데 contentType 이 없으면 재사용 풀이 종류를 구분하지 못해 스크롤 시 컴포지션 재사용 효율이 떨어진다. 단일 종류 리스트면 문제 아님(verdict=false).
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/lists#item-keys
판정 기준 — remember 없는 mutableStateOf 는 recomposition 마다 새 상태로 리셋된다 — 상태가 유실되는 사실상 버그.
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/state#state-in-composables
판정 기준 — 인자 위치의 listOf(...) 는 매 recomposition 새 인스턴스라 수신 컴포저블의 인자 비교가 항상 실패한다. 원소가 전부 컴파일타임 상수면 remember 승격, 변수 포함이면 remember(키) 또는 상위 hoisting 을 제안.
수정 옵션
⚡ strong skipping — strong skipping 활성 시 unstable 파라미터 컴포저블도 skippable 이 되지만, 비교가 인스턴스 동일성(===)이라 매 recomposition 새 인스턴스면 여전히 skip 에 실패한다 — 문제의 본질이 'non-skippable'에서 '인스턴스 재생성'으로 이동할 뿐 사라지지 않는다.
근거 — https://developer.android.com/develop/ui/compose/performance/bestpractices#use-remember
판정 기준 — collectAsState 는 백그라운드에서도 Flow 수집을 유지해 리소스를 낭비한다. 성능(recomposition)보다는 수명주기 리소스 문제 — 설명에서 과장하지 말 것.
수정 옵션
근거 — https://developer.android.com/topic/libraries/architecture/compose#streams
판정 기준 — 상수 key(Unit/true) 자체는 '첫 composition 1회만 실행' 의도로 정당할 수 있다. 본문이 파라미터/상태 등 변하는 값을 참조하는데 key 에 없으면 stale 값 캡처 — 그때만 문제로 판정하고, 의도적 1회 실행이면 verdict=false.
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/side-effects#restarting-effects
판정 기준 — ExoPlayer/MediaCodec/Bitmap 등 무거운 생성이 Lazy 아이템 컴포지션 시점에 있으면 스크롤 중 아이템 등장마다 생성 비용이 프레임을 밀어낸다.
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/performance/bestpractices#use-remember
판정 기준 — mutableStateOf(mutableListOf(...)) 는 컬렉션 내용을 바꿔도 State 가 변경을 감지하지 못해 recomposition 이 안 일어난다 — 성능이 아니라 갱신 누락 버그. remember 여부와 무관하게 문제.
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/state#state-in-composables
판정 기준 — Int/Long/Float/Double/Boolean 리터럴을 mutableStateOf 로 감싸면 매 읽기/쓰기마다 오토박싱된다. 고빈도 갱신 상태(스크롤·애니메이션)일수록 효과 큼 — 저빈도면 과장 금지.
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/state#state-in-composables
판정 기준 — items(list.sortedBy { ... }) 처럼 컴포지션 시점에 정렬/필터/매핑을 수행하면 매 recomposition 전체 변환이 재실행된다. 리스트가 크거나 recomposition 이 잦으면 프레임을 밀어낸다.
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/performance/bestpractices#use-remember
판정 기준 — composed {} 는 요소마다 컴포지션을 만들어 재사용이 안 되고 비교 비용이 크다 — 커스텀 modifier 는 Modifier.Node API 권장.
수정 옵션
근거 — https://mrmans0n.github.io/compose-rules/rules/#avoid-modifier-composed
AST 는 구조 앵커(후보)만 잡고, 타입/맥락 판정(이벤트 람다 여부·실제 안정성 등)은 AI 가 수행한다. AI 가 기각한 후보는 리포트의 'AI 기각' 섹션에 사유와 함께 남는다.
판정 기준 — 이미 읽은 상태를 컴포지션 본문에서 다시 쓰면 recomposition 무한 루프. 단 **onClick 등 이벤트 람다/effect 블록 안의 쓰기는 정상**이다 — 스니펫에서 쓰기가 람다 안인지 본문 직접 실행인지 판정하고, 람다 안이면 verdict=false.
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/performance/bestpractices#avoid-backwards
판정 기준 — 컴포지션 본문에서 scope.launch 를 직접 호출하면 매 recomposition 코루틴이 새로 뜬다. **이벤트 람다(onClick 등) 안의 launch 는 정상** — 스니펫에서 호출 위치가 본문 직접 실행일 때만 verdict=true.
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/side-effects#restarting-effects
판정 기준 — 컴포지션에서 StateFlow.value 를 직접 읽으면 값이 바뀌어도 recomposition 이 트리거되지 않는다(갱신 누락). 수신 타입이 StateFlow 계열일 때만 문제 — MutableState.value 나 일반 프로퍼티면 verdict=false.
수정 옵션
근거 — https://developer.android.com/topic/libraries/architecture/compose#streams
판정 기준 — 스크롤 인덱스처럼 프레임마다 변하는 상태의 비교식을 본문에서 직접 계산하면 매 프레임 recomposition 된다. 파생 결과의 변화 빈도가 원본보다 훨씬 낮을 때만 derivedStateOf 가 이득 — 아니면 verdict=false(과잉 적용도 비용).
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/performance/bestpractices#use-derivedstateof
판정 기준 — Modifier.offset(x,y)/background(color) 의 인자가 자주 변하는 상태에서 파생되면 값 변경마다 컴포지션부터 다시 돈다. 람다 버전은 layout/draw 단계에서만 읽어 컴포지션을 건너뛴다. 인자가 상수/저빈도 값이면 verdict=false.
수정 옵션
근거 — https://developer.android.com/develop/ui/compose/performance/bestpractices#defer-reads
판정 기준 — 읽기전용 List/Set/Map 인터페이스는 컴파일러가 unstable 로 추론한다. ImmutableList/PersistentList(kotlinx.collections.immutable)·@Immutable 래퍼면 stable — 타입 표기가 그중 하나면 verdict=false.
수정 옵션
⚡ strong skipping — strong skipping 활성 시 unstable 파라미터 컴포저블도 skippable 이 되지만, 비교가 인스턴스 동일성(===)이라 매 recomposition 새 인스턴스면 여전히 skip 에 실패한다 — 문제의 본질이 'non-skippable'에서 '인스턴스 재생성'으로 이동할 뿐 사라지지 않는다.
근거 — https://developer.android.com/develop/ui/compose/performance/stability/fix#immutable-collections
판정 기준 — var 프로퍼티가 하나라도 있으면 그 클래스는 unstable. 모든 프로퍼티가 val + stable 타입이면 stable. Compose 가 var 변경을 감지하지 못해 화면이 갱신 안 되는 버그로도 이어진다.
수정 옵션
⚡ strong skipping — strong skipping 활성 시 unstable 파라미터 컴포저블도 skippable 이 되지만, 비교가 인스턴스 동일성(===)이라 매 recomposition 새 인스턴스면 여전히 skip 에 실패한다 — 문제의 본질이 'non-skippable'에서 '인스턴스 재생성'으로 이동할 뿐 사라지지 않는다.
근거 — https://developer.android.com/develop/ui/compose/performance/stability/fix
판정 기준 — ViewModel 통째 전달은 하위를 unstable 로 만들고 재사용/프리뷰를 막는다. 단 최상위 화면(Screen) 컴포저블이 VM 을 받는 것은 관례 — 하위 컴포넌트로 '다시 전달(forwarding)'하는 경우만 문제로 판정.
수정 옵션
근거 — https://mrmans0n.github.io/compose-rules/rules/#hoist-all-the-things
Compose 컴파일러가 확정한 사실(추정 아님). AST 후보와 정확 매칭되면 해당 후보를 confirmed 로 승격한다.
판정 기준 — restartable 인데 skippable 이 아닌 컴포저블 — unstable 파라미터가 원인이면 unstable-param finding 이 따로 나오므로, 이 finding 은 그 외 원인(stability 미상 파라미터 등)일 때만 산출된다. 스니펫에서 원인 후보를 짚어 설명.
수정 옵션
⚡ strong skipping — strong skipping 활성 프로젝트에선 non-skippable 자체가 드물다 — 남아 있다면 @NonSkippableComposable 표기이거나 특이 케이스.
근거 — https://developer.android.com/develop/ui/compose/performance/stability/diagnose
판정 기준 — 컴파일러가 unstable 로 확정한 클래스. 메시지의 원인 필드(var 또는 unstable 타입)를 짚어, 이 클래스를 파라미터로 받는 모든 컴포저블에 영향이 전파됨을 설명.
수정 옵션
⚡ strong skipping — strong skipping 활성 시 unstable 파라미터 컴포저블도 skippable 이 되지만, 비교가 인스턴스 동일성(===)이라 매 recomposition 새 인스턴스면 여전히 skip 에 실패한다 — 문제의 본질이 'non-skippable'에서 '인스턴스 재생성'으로 이동할 뿐 사라지지 않는다.
근거 — https://developer.android.com/develop/ui/compose/performance/stability/fix
판정 기준 — 컴파일러가 unstable 로 확정한 파라미터(추정 아님). 타입이 왜 unstable 인지(읽기전용 컬렉션·var 프로퍼티·외부 모듈 클래스)를 스니펫에서 짚어 설명.
수정 옵션
⚡ strong skipping — strong skipping 활성 시 unstable 파라미터 컴포저블도 skippable 이 되지만, 비교가 인스턴스 동일성(===)이라 매 recomposition 새 인스턴스면 여전히 skip 에 실패한다 — 문제의 본질이 'non-skippable'에서 '인스턴스 재생성'으로 이동할 뿐 사라지지 않는다.
근거 — https://developer.android.com/develop/ui/compose/performance/stability/diagnose
단말 perfetto 트레이스에서 실측. 컴포저블 이름이 정확 매칭되면 정적 finding 을 measured 로 승격한다.
판정 기준 — perfetto 실측으로 잡힌 메인스레드 원인 슬라이스(tier3). 슬라이스명의 의미(Recomposer:recompose=재구성, compose:lazy:prefetch:measure=Lazy 프리페치 측정, animation=애니메이션 프레임, AndroidOwner:measureAndLayout=측정/배치, Compose:onForgotten=dispose 리소스 해제)와 통상 원인·완화책을 설명. fix_code 는 원인 코드가 특정될 때만 제시하고 불확실하면 생략.
수정 옵션
근거 — https://developer.android.com/topic/performance/vitals/render
tier3 실측 finding 은 원인 슬라이스 종류에 따라 카테고리가 붙는다 (메시지 접두 + json jank_category).
| 카테고리 | 표기 | 완화 방향 |
|---|---|---|
| recompose | 재구성 | stability 규칙(unstable 파라미터/비-remember 생성) 수정으로 재구성 축소 |
| lazy-prefetch | Lazy 측정 | 아이템 경량화 + contentType + 고정 크기(Modifier.height)로 측정 비용 축소 |
| measure-layout | 측정/배치 | 레이아웃 중첩 축소·intrinsic 측정 회피·고정 크기 지정 |
| animation | 애니메이션 | 프레임당 작업량 축소·graphicsLayer 로 draw 단계 이동 |
| dispose | 해제 | onForgotten 해제 비용 — 무거운 리소스 해제를 백그라운드로 이동 |
| other | 기타 | — |