Slow Kotlin code rarely comes from one big mistake. It usually comes from small habits that repeat across many screens. A list that rebuilds too often can cause visible lag. A coroutine that runs on the wrong thread can block the user interface. An object that stays in memory can grow into a leak. This guide explains five fixes that address those patterns directly.
Start by measuring before you change anything. Use the profiler to find the slowest screen and the slowest method. A guess about performance often points to the wrong code. A measurement points to the real bottleneck. Record the frame time and memory use before the fix so you can prove that the fix helped.
Fix one pattern at a time and test after each change. A single change makes the result easy to judge. Many changes at once hide which change helped and which change hurt. Keep a short log of each change and its measured effect. That log becomes a useful record for code review.
Stop Rebuilding Lists On Every Frame
Lists cause lag when the app rebuilds every item for a small change. Use stable item keys so the list can reuse items that did not change. Update only the changed item instead of replacing the whole list. This approach reduces layout work and it reduces drawing work. Users notice the difference as smoother scrolling.
Move heavy work out of the item layout code. Format text once and cache the result when the same text appears again. Load images with size limits that match the item size. Avoid creating new objects inside the drawing code because allocation during scrolling adds pressure on memory. Small changes in list code often produce the largest visible gain.
- Use stable keys for list items
- Update changed items only
- Cache formatted text for repeated rows
- Load images at the size the screen needs
- Avoid object creation inside drawing code
Use Coroutines On The Right Thread
Coroutines make async code easier to read but they do not fix thread mistakes by themselves. Network calls and file reads belong on a background thread. User interface updates belong on the main thread. A coroutine that does file work on the main thread will cause stutter even if the code looks clean. Check the dispatcher for every coroutine that touches slow work.
Cancel coroutines when the screen leaves. A coroutine that keeps running after the user leaves can waste network data and memory. Tie the coroutine scope to the screen lifecycle so cancellation happens automatically. Test cancellation by leaving a screen during a slow load. The app should stop the work cleanly without a crash.
Thread discipline also matters for network work described in the material on proxy traffic inspection. Slow network calls become more visible when you watch request timing. Use that timing data to decide which calls need caching and which calls need a background thread. A measured network fix often improves perceived speed more than a code tweak alone.
Reduce Allocations In Hot Paths
Allocation in a hot path creates work for the memory manager. A hot path runs many times per second during scrolling or animation. Avoid creating lists and strings inside such code when a reused buffer can do the job. Reuse objects carefully and reset their state before each use. This technique needs care but it pays off in smooth animation.
Watch for hidden allocations in simple code. A string template can create several objects. A collection filter can create a new list for each frame. A lambda can capture variables and create an object. None of these patterns is wrong on its own. The problem appears only when the pattern repeats in a hot path. Profile the hot path and remove the allocations that appear most often.
Avoid Boxing And Unnecessary Wrapping
Boxing turns a simple number into an object. Generic collections can box numbers when you store them as objects. This cost is small for one value but it grows when a list holds thousands of numbers. Use primitive arrays for large numeric data when the code allows it. Use specialised collections when the language provides them for your use case.
Wrapping also appears in data models that add layers without benefit. A model that wraps another model for one field adds allocation and indirection. Simplify the model when the extra layer serves no purpose. Keep data classes flat for hot data and use wrappers only where they add clear meaning or safety. Simpler models are easier to read and faster to process.
Cache Smartly And Invalidate Clearly
Caching can fix repeated work but a bad cache can cause stale data and memory growth. Cache results that are expensive to compute and safe to reuse. Set a clear rule for when cached data expires. Invalidate the cache when the source data changes. A cache without an invalidation rule becomes a source of bugs.
Use a size limit for every cache that can grow. An unbounded cache can grow until the app runs out of memory. A small limit with a clear eviction rule keeps memory stable. Log cache hits and misses during testing to see whether the cache helps. A cache with low hits adds complexity without benefit and should be removed.
Measure Memory After Each Fix
Performance work is incomplete without a memory check. Run the app through the slowest user flow after each fix. Watch memory use over time and look for growth that does not fall back. A steady rise during repeated actions points to a leak. A spike that falls back after the action is normal and less worrying.
Test on a low-end device when you can. A fix that looks fine on a fast phone may still lag on a slower phone. Low-end testing reveals allocation problems and thread problems early. Record frame time and memory use for both device types. A fix that helps both types is a strong fix.
val items = listOf(1, 2, 3)
val total = items.sum()
println(total)
The snippet above is an original example written for this guide. It shows generic code that you can adapt to your own project. It does not copy code from any outside source.
The snippet is intentionally short so the focus stays on the pattern rather than the syntax. Replace the list with your own data and measure the result. Use the profiler to confirm that the change improves the screen that you target.
Review Code With Performance In Mind
Code review is a good place to catch performance problems before they reach users. A reviewer should look for list rebuilds and for work on the main thread and for allocations in hot paths. A review comment should name the pattern and suggest a concrete alternative. A vague comment about speed helps no one. A specific comment about updating only the changed item gives the author a clear next step. Performance review works best when the team agrees on a short list of patterns to check in every review.
Use measurements in review discussions rather than opinions alone. A claim that code is slow needs a profiler result to support it. A claim that a fix helps needs a before and after measurement. Measurements keep reviews fair and they keep the team focused on real bottlenecks. A review that relies on taste alone can waste time on code that users never notice. A review that relies on data improves the screens that matter most.
Keep performance notes in the project documentation after a review finds a pattern. A note should describe the pattern and the fix and the measured effect. Future authors can read the note before they write similar code. A shared note prevents the same mistake from returning in a new feature. Over time a small set of notes becomes a team standard that new members can learn quickly. Standards built from real measurements last longer than rules copied from elsewhere.
Performance notes also help new team members avoid old mistakes. A new author who reads a note about list updates will write better list code from the start. A new author who reads a note about coroutine threads will choose the right dispatcher without trial and error. Shared notes shorten onboarding and they raise the quality of first contributions. Keep notes short and link each note to a measured example so new readers trust the advice.
Common Mistakes
A common mistake is optimising code that does not affect users. A fast helper method that runs once per session does not need optimisation. Focus on screens and flows that users repeat many times. A list screen and a feed screen deserve more attention than a settings screen that opens rarely.
Another mistake is adding a cache for every slow call without an invalidation rule. A stale cache can show old prices or old messages. Users trust the app less when data looks wrong. Add a cache only after you define when it expires and when it clears.
Some developers also move all work to background threads without checking the result. Background work still needs a clear path back to the user interface. A result that never reaches the screen is worse than a slow result. Test the full flow from trigger to visible update after each thread change.
Finally do not optimise by removing readability everywhere. A small gain that makes code hard to maintain can cost more later. Keep the fast path clear and add a short comment that explains why the code uses a special technique. Future readers will thank you.
Frequently Asked Questions
Should I optimise every Kotlin file?
No you should optimise only the code that measurements show as slow. Most code runs rarely and does not affect user experience. Focus on hot paths and repeated screens first.
Do coroutines make code faster by themselves?
No coroutines make async code easier to manage but they do not change thread cost. You still need the right dispatcher and proper cancellation. A coroutine on the wrong thread can still block the interface.
When should I use a cache?
Use a cache when a result is expensive to compute and safe to reuse for a short time. Define an expiry rule and a size limit before you add the cache. Remove the cache if hit rates stay low.
Conclusion
Kotlin performance improves when you fix repeated patterns rather than chasing single lines. Use stable list updates and correct coroutine threads and fewer allocations in hot paths. Cache with clear rules and measure memory after each change. Your concrete next step is to profile your slowest screen today and fix one pattern from this guide. Measure the result and then move to the next pattern. For related tooling you can study essential reverse engineering tools and for protection work you can study app signature verification and for emulator testing you can study Android Studio emulator setup as next steps.


