Android Coden
Android 9 min lesen

Jank in Android-Apps verstehen

Jank macht Apps trotz korrekter Logik träge. Du lernst, wie Frame-Deadlines, Rendering und glatte UI zusammenhängen.

Jank ist der sichtbare Ruckler in einer Android-App: Du tippst, scrollst oder wechselst einen Zustand, aber die Oberfläche reagiert nicht gleichmäßig. Für Lernende ist das ein wichtiger Schritt weg von „mein Code funktioniert“ hin zu „meine App fühlt sich zuverlässig an“. Gerade in Kotlin-, Jetpack- und Compose-Projekten entsteht gute Qualität nicht nur durch korrekte Daten und saubere Architektur, sondern auch dadurch, dass Rendering rechtzeitig fertig wird und die UI flüssig bleibt.

Was ist das?

Jank beschreibt verpasste Frames. Ein Frame ist ein einzelnes Bild, das Android für den Bildschirm zeichnet. Bei 60 Hz hat deine App ungefähr 16,6 Millisekunden Zeit, um den nächsten Frame vorzubereiten. Bei 90 Hz oder 120 Hz ist das Zeitfenster noch kleiner. Wenn deine App diese Frame-Deadline verpasst, zeigt das System entweder ein altes Bild länger an oder der Übergang wirkt ungleichmäßig. Genau das nimmst du als Ruckeln wahr.

Das Gemeine daran: Die fachliche Logik kann komplett richtig sein. Deine Liste lädt korrekt, dein Button speichert korrekt, deine Navigation funktioniert korrekt. Trotzdem wirkt die App langsam, wenn auf dem Main Thread zu viel passiert oder wenn die UI bei jeder kleinen Änderung unnötig viel neu berechnet und neu gezeichnet wird. Für Nutzerinnen und Nutzer zählt nicht nur, ob das Ergebnis stimmt, sondern auch, ob Interaktionen direkt und stabil wirken.

Im Android-Kontext gehört Jank zur Performance-Qualität. Er liegt nahe an Rendering, Animationen, Listen, Bildern, Datenfluss und Threading. In modernen Apps betrifft er klassische Views genauso wie Jetpack Compose. Compose nimmt dir viel UI-Verwaltung ab, aber es macht schwere Arbeit nicht automatisch billig. Wenn du große Datenmengen synchron sortierst, Bilder ungünstig dekodierst, Zustände zu breit verteilst oder in Composables teure Berechnungen wiederholst, kann die Oberfläche trotzdem stocken.

Jank hat auch einen Bezug zu Accessibility. Eine ruckelige App ist für viele Menschen schwerer zu bedienen, besonders wenn sie auf stabile visuelle Rückmeldungen, Screenreader-Abläufe oder motorisch genaue Eingaben angewiesen sind. Performance ist deshalb kein Luxus am Ende eines Projekts. Sie ist Teil der Nutzbarkeit. Gleichzeitig solltest du Messungen und Logging so gestalten, dass du keine unnötigen personenbezogenen Daten sammelst. Performance-Analyse und Datenschutz müssen zusammenpassen.

Das mentale Modell ist einfach genug, um früh damit zu arbeiten: Android hat einen Takt, und deine UI muss in diesem Takt liefern. Alles, was in diesem Zeitfenster auf dem Main Thread passiert, konkurriert mit Eingabe, Layout, Compose-Recomposition, Messung, Zeichnen und Animation. Wenn du diese Konkurrenz ignorierst, baust du Ruckler ein, auch wenn du keine Exception siehst.

Wie funktioniert es?

Android verarbeitet UI-Arbeit über den Main Thread. Dort laufen Eingaben, viele Lifecycle-Ereignisse und die Vorbereitung der Darstellung. Für jeden Bildschirm-Refresh muss die Oberfläche entscheiden, was sich geändert hat, Layout-Informationen berechnen und Zeichenbefehle vorbereiten. Das System reicht diese Arbeit dann an die Rendering-Pipeline weiter. Wenn eine Phase zu lange dauert, wird die Frame-Deadline verpasst.

In klassischen View-UIs denkst du oft an measure, layout und draw. In Compose kommen Begriffe wie Composition, Recomposition, Layout und Drawing dazu. Composition beschreibt, welche UI-Struktur aus deinem Zustand entsteht. Recomposition passiert, wenn sich beobachteter Zustand ändert und Compose betroffene Teile neu auswertet. Layout bestimmt Größen und Positionen. Drawing erzeugt die sichtbare Ausgabe. Diese Phasen sind nicht identisch mit alten Views, aber das Grundproblem bleibt gleich: Zu viel Arbeit pro Frame führt zu Jank.

Eine häufige Ursache ist blockierende Arbeit auf dem Main Thread. Dazu gehören Datenbankzugriffe ohne passenden Dispatcher, Netzwerkaufrufe, große JSON-Verarbeitung, Bildverarbeitung, Dateizugriffe oder komplexe Sortierungen. Kotlin Coroutines helfen dir, solche Arbeit sauber auszulagern, aber nur, wenn du Dispatcher bewusst nutzt. viewModelScope.launch allein garantiert nicht, dass schwere Arbeit vom Main Thread verschwindet. Wenn du CPU-lastige Berechnungen direkt darin ausführst, können sie weiterhin die UI blockieren, sofern du nicht auf Dispatchers.Default oder eine passende Schicht wechselst.

Eine zweite Ursache ist unnötige UI-Arbeit. In Compose kann das durch instabile Parameter, zu große State-Objekte oder falsch platzierte Berechnungen passieren. Wenn ein gesamter Screen neu ausgewertet wird, obwohl nur ein kleines Label eine Änderung braucht, verbrauchst du Zeit. Wenn du in jedem Composable-Aufruf eine Liste filterst oder formatierst, wiederholst du Arbeit, die besser im ViewModel, in einem abgeleiteten State oder in einer vorbereiteten UI-Modell-Schicht liegt.

Eine dritte Ursache sind Listen. LazyColumn und LazyRow sind für große Datenmengen gedacht, aber sie brauchen stabile Keys, klare Item-Strukturen und günstige Inhalte. Wenn jedes Listen-Item beim Scrollen Bilder synchron vorbereitet, komplexe Texte misst oder neue Objekte ohne Not erzeugt, steigt die Gefahr für Jank. Das betrifft besonders Feeds, Chats, Kalender, Shop-Listen und Einstellungen mit vielen Schaltern.

Animationen zeigen Jank sehr deutlich. Eine Animation ist eine Folge vieler Frames. Wenn einzelne Frames ausfallen, wirkt die Bewegung unruhig. Darum solltest du Animationen nicht mit teurer Zustandsarbeit koppeln. Eine expandierende Karte sollte nicht gleichzeitig eine große Datenmenge neu laden, eine Datenbank migrieren oder ein komplettes Diagramm neu berechnen. Die UI darf reagieren, während schwere Arbeit im Hintergrund passiert und ihr Ergebnis später gezielt in den Zustand fließt.

In der Architektur hilft dir eine klare Trennung. Repositorys beschaffen Daten. Use Cases oder Interactoren bereiten fachliche Ergebnisse vor. ViewModels formen daraus UI-State. Composables rendern diesen State möglichst direkt. Je weniger Überraschungsarbeit in der UI-Schicht steckt, desto besser kannst du Jank erkennen und vermeiden. Diese Trennung ist kein Selbstzweck. Sie reduziert die Arbeit im kritischen Frame-Zeitfenster.

Für Release-Praxis ist wichtig: Jank zeigt sich oft erst auf echten Geräten, mit echten Daten und in normalen Nutzungsmustern. Ein schneller Emulator auf einem starken Entwicklungsrechner verdeckt Probleme. Ein günstiges Gerät mit vollem Speicher, hoher Display-Refresh-Rate oder aktivem Energiesparmodus zeigt sie eher. Deshalb solltest du Performance nicht nur subjektiv beim Entwickeln prüfen, sondern mit Profiling und wiederholbaren Szenarien.

In der Praxis

Stell dir einen Screen vor, der eine Aufgabenliste anzeigt. Du bekommst Rohdaten aus dem Repository und willst sie nach Fälligkeit sortieren, filtern und in lesbare Texte umwandeln. Eine typische ungünstige Lösung wäre, diese Arbeit direkt im Composable zu erledigen. Das sieht kurz aus, ist aber gefährlich: Bei jeder Recomposition kann die Arbeit erneut laufen. Wenn die Liste groß wird oder häufig State-Änderungen auftreten, spürst du Jank beim Scrollen oder beim Tippen in einen Filter.

Besser ist, die teure Vorbereitung in die ViewModel-Schicht zu verlagern und nur fertige UI-Modelle zu rendern. CPU-lastige Arbeit läuft dabei nicht auf dem Main Thread.

data class TaskUi(
    val id: String,
    val title: String,
    val dueLabel: String,
    val isOverdue: Boolean
)

class TaskViewModel(
    private val repository: TaskRepository
) : ViewModel() {

    val uiState: StateFlow<List<TaskUi>> =
        repository.observeTasks()
            .mapLatest { tasks ->
                withContext(Dispatchers.Default) {
                    tasks
                        .filter { !it.archived }
                        .sortedBy { it.dueAt }
                        .map { task ->
                            TaskUi(
                                id = task.id,
                                title = task.title,
                                dueLabel = formatDueDate(task.dueAt),
                                isOverdue = task.dueAt < Clock.System.now()
                            )
                        }
                }
            }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = emptyList()
            )
}

@Composable
fun TaskListScreen(viewModel: TaskViewModel) {
    val tasks by viewModel.uiState.collectAsStateWithLifecycle()

    LazyColumn {
        items(
            items = tasks,
            key = { it.id }
        ) { task ->
            TaskRow(task = task)
        }
    }
}

@Composable
private fun TaskRow(task: TaskUi) {
    ListItem(
        headlineContent = { Text(task.title) },
        supportingContent = { Text(task.dueLabel) }
    )
}

Das Beispiel zeigt mehrere Regeln. Erstens: Die UI rendert fertigen Zustand. Zweitens: Sortierung, Filterung und Formatierung laufen außerhalb der direkten Rendering-Arbeit. Drittens: LazyColumn bekommt stabile Keys, damit Compose Items beim Scrollen besser zuordnen kann. Viertens: collectAsStateWithLifecycle bindet den Flow passend an den UI-Lebenszyklus, statt unnötig weiter Daten zu sammeln, wenn der Screen nicht aktiv ist.

Eine wichtige Stolperfalle ist die falsche Sicherheit durch Coroutines. Viele Einsteiger sehen viewModelScope.launch und denken, damit sei die UI automatisch geschützt. Das stimmt nicht. Coroutines strukturieren Nebenläufigkeit, aber sie wählen nicht magisch den richtigen Thread für deine Arbeit. Wenn du schwere CPU-Arbeit im Main-Kontext ausführst, bleibt sie teuer. Nutze passende Dispatcher, und verschiebe Arbeit so weit wie sinnvoll vor das Rendering.

Eine zweite Stolperfalle ist „kleine“ Arbeit, die sehr oft passiert. Eine einzelne Datumsformatierung wirkt harmlos. In 300 Listen-Items, bei jedem Scroll-Schritt und bei wiederholter Recomposition wird daraus ein Problem. Dasselbe gilt für reguläre Ausdrücke, Währungsformatierung, Bildgrößenberechnung, String-Verkettung in Schleifen oder das Erzeugen vieler temporärer Objekte. Jank entsteht oft nicht durch eine riesige Operation, sondern durch viele mittelkleine Operationen im falschen Moment.

Eine praktische Entscheidungsregel lautet: Alles, was nicht direkt zum Anzeigen eines bereits vorbereiteten UI-Zustands gehört, gehört kritisch geprüft. Muss diese Arbeit wirklich im Composable laufen? Muss sie wirklich bei jeder Recomposition laufen? Muss sie wirklich auf dem Main Thread laufen? Wenn du eine dieser Fragen mit Nein beantwortest, verschiebe oder speichere das Ergebnis. In Compose helfen dir je nach Fall remember, derivedStateOf, stabile UI-Modelle, saubere State-Hoisting-Strukturen und klar begrenzte Composables. Diese Werkzeuge ersetzen aber nicht die Grundfrage nach der Kostenstelle.

Beim Debuggen solltest du Jank nicht nur nach Gefühl beurteilen. Nutze Android Studio Profiler, System Tracing oder geeignete Frame-Metriken, um lange Frames zu finden. Reproduziere konkrete Szenarien: schnelles Scrollen in einer langen Liste, Öffnen eines komplexen Screens, Starten einer Animation, Eingabe in ein Suchfeld während Daten gefiltert werden. Beobachte, ob CPU-Spitzen, Layout-Arbeit, Garbage Collection oder Main-Thread-Blöcke zusammen mit den Rucklern auftreten.

Für Tests kannst du nicht jeden Frame perfekt absichern, aber du kannst Risiken reduzieren. Schreibe Unit-Tests für Datenvorbereitung im ViewModel oder Use Case, damit du diese Logik nicht in Composables versteckst. Nutze UI-Tests für zentrale Flows, um unerwartete Wartezeiten oder blockierende Zustände früh zu bemerken. In Code-Reviews solltest du besonders auf neue Arbeit in Composables achten: Sortierungen, Filter, Datenbankaufrufe, große Schleifen, Bildverarbeitung und instabile Listen-Keys sind klare Prüfpunkte.

Beachte auch den Zusammenhang mit Sicherheit und Datenschutz. Performance-Messung ist hilfreich, aber Log-Ausgaben, Traces und Analyseereignisse dürfen keine sensiblen Inhalte enthalten. Du brauchst meist technische Kennzahlen wie Dauer, Screen-Name, Gerätekategorie oder Frame-Status, nicht den Inhalt privater Nachrichten oder Eingaben. Saubere Performance-Arbeit respektiert also die Privatsphäre und hält Debug-Daten begrenzt.

Im Alltag erkennst du eine reife Arbeitsweise daran, dass du Jank früh behandelst. Du wartest nicht bis kurz vor Release, wenn schon viele Screens schwer zu ändern sind. Du prüfst neue Listen mit realistischen Datenmengen, testest Animationen auf einem mittelklassigen Gerät und schaust dir verdächtige Composables im Review genauer an. So bleibt Performance ein normaler Teil deiner Entwicklung, nicht eine hektische Rettungsaktion am Ende.

Fazit

Jank ist das Signal, dass deine App den Rhythmus des Bildschirms nicht zuverlässig trifft. Für dich als Android-Entwickler bedeutet das: Denke nicht nur in korrekten Funktionen, sondern auch in Frame-Deadlines, Rendering-Kosten und gleichmäßiger Bedienung. Prüfe bei deinem nächsten Screen gezielt, welche Arbeit auf dem Main Thread läuft, welche Composables bei State-Änderungen neu ausgewertet werden und ob Listen stabile Keys sowie vorbereitete UI-Modelle nutzen. Nimm einen konkreten Flow, öffne den Profiler, reproduziere schnelles Scrollen oder eine Animation und notiere die Ursache langer Frames. Danach kannst du im Code-Review begründet erklären, ob ein Ruckler durch Datenarbeit, Recomposition, Layout, Drawing oder eine ungünstige Architektur entsteht.

Quellen (4)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.