97

I have created RecyclerView example basing on Creating Lists and Cards guide. My adapter have a pattern implementation only for inflate the layout.

The problem is the poor scrolling performance. This in a RecycleView with only 8 items.

In some tests I verified that in Android L this problem does not occurs. But in the KitKat version the decreasing of performance is evident.

falvojr
  • 3,060
  • 4
  • 31
  • 54
  • 1
    Try to use ViewHolder design pattern for scrolling performance : http://developer.android.com/training/improving-layouts/smooth-scrolling.html – Haresh Chhelana Nov 28 '14 at 12:27
  • @HareshChhelana thanks for you answer! But I'm already using ViewHolder pattern, according the link: https://developer.android.com/training/material/lists-cards.html – falvojr Nov 28 '14 at 12:37
  • 2
    Can you share some code about your adapter setup and the XML file for your layouts. This does not look normal. Also, did you profile and see where the time is spent? – yigit Nov 29 '14 at 04:51
  • @yigit thanks for you comment. But my adapter and XML setup are identical to the link mentioned in my question: [Creating Lists and Cards](https://developer.android.com/training/material/lists-cards.html). I believe that unfortunately is a bug in recycleview. – falvojr Jan 10 '15 at 17:33
  • 2
    I am facing allmost the same problems. Except its fast in pre Lollipop and incredible(really) slow in Android L. – Servus7 Jan 28 '15 at 21:47
  • 1
    can you also share the version of the library you are importing. – Droidekas Apr 06 '16 at 12:48
  • @Droidekas, thanks for you comment! The performance problem was identified in version 21 lib, but even at 23 the problem still exists. – falvojr Apr 06 '16 at 14:27
  • So you are still facing the issue? – Droidekas Apr 06 '16 at 14:28
  • Yes, there was a slight improvement but the problem is still noticeable. – falvojr Apr 06 '16 at 14:49
  • Adding your code would help.I use a much more complicated recycler view and manage to keep it above 30 fps consistently.Adding your code would help. – Droidekas Apr 06 '16 at 14:50

16 Answers16

249

I've recently faced the same issue, so this is what I've done with the latest RecyclerView support library:

  1. Replace a complex layout (nested views, RelativeLayout) with the new optimized ConstraintLayout. Activate it in Android Studio: Go to SDK Manager -> SDK Tools tab -> Support Repository -> check ConstraintLayout for Android & Solver for ConstraintLayout. Add to the dependencies:

    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    
  2. If possible, make all elements of the RecyclerView with the same height. And add:

    recyclerView.setHasFixedSize(true);
    
  3. Use the default RecyclerView drawing cache methods and tweak them according to your case. You don't need third party library to do so:

    recyclerView.setItemViewCacheSize(20);
    recyclerView.setDrawingCacheEnabled(true);
    recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
    
  4. If you use many images, make sure their size and compression are optimal. Scaling images may also affect the performance. There are two sides of the problem - the source image used and the decoded Bitmap. The following example gives you a hint how to decode аn image, downloaded from the web:

    InputStream is = (InputStream) url.getContent();
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    Bitmap image = BitmapFactory.decodeStream(is, null, options);
    

The most important part is specifying inPreferredConfig - it defines how many bytes will be used for each pixel of the image. Keep in mind that this is a preferred option. If the source image has more colors, it will still be decoded with a different config.

  1. Make sure onBindViewHolder() is as cheap as possible. You can set OnClickListener once in onCreateViewHolder() and call through an interface a listener outside of the Adapter, passing the clicked item. This way you don't create extra objects all the time. Also check flags and states, before making any changes to the view here.

    viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              Item item = getItem(getAdapterPosition());
              outsideClickListener.onItemClicked(item);
          }
    });
    
  2. When data gets changed, try to update only the affected items. For example instead of invalidating the whole data set with notifyDataSetChanged(), when adding / loading more items, just use:

    adapter.notifyItemRangeInserted(rangeStart, rangeEnd);
    adapter.notifyItemRemoved(position);
    adapter.notifyItemChanged(position);
    adapter.notifyItemInserted(position);
    
  3. From Android Developer Web Site :

Rely on notifyDataSetChanged() as a last resort.

But if you need to use it, maintain your items with unique ids:

    adapter.setHasStableIds(true);

RecyclerView will attempt to synthesize visible structural change events for adapters that report that they have stable IDs when this method is used. This can help for the purposes of animation and visual object persistence but individual item views will still need to be rebound and relaid out.

Even if you do everything right, chances are that the RecyclerView is still not performing as smoothly as you would like.

Galya
  • 6,294
  • 6
  • 27
  • 45
  • 23
    one vote for adapter.setHasStableIds(true); method that really helped to make recyclerview fast. – Atula Nov 24 '16 at 06:54
  • that was a book, completely wiki – Mehdi Khademloo Apr 16 '17 at 01:20
  • @Galya I tried all your recommendations, the best performance makes "drawing cache" from point 3, scrolling works very smooth, the problem is when I try to update some item from recycler adapter using adapterItems.notifyItemChanged(itemPosition), item does not update :( – Alex Apr 17 '17 at 17:34
  • 1
    Part 7 is totally wrong! setHasStableIds(true) does will do nothing except you use adapter.notifyDataSetChanged(). Link: https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#notifyDataSetChanged() – localhost Aug 14 '17 at 14:45
  • @Galya really good answer. I've built my [talk about RecyclerView performance](https://stackoverflow.com/a/45939145/2308720) taking your items into account. Thanks a lot! – Oleksandr Nov 13 '17 at 09:35
  • 1
    I can see why the `recyclerView.setItemViewCacheSize(20);` can improuve the performance. But `recyclerView.setDrawingCacheEnabled(true);` and `recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);` though! I am not sure these will change anything. These are [`View`](https://developer.android.com/reference/android/view/View.html#setDrawingCacheEnabled(boolean)) specific calls that allows you to programmatically retrieve the drawing cache as a bitmap and use it for your advantage later. `RecyclerView` does not seem to do anything about it. – Abdelhakim AKODADI Nov 13 '17 at 20:33
  • 2
    @AbdelhakimAkodadi, the scrolling gets smooth with cache. I've tested it. How could you say otherwise, it's obvious. Of course, if someone scrolls like crazy nothing will help. I just show the other options like setDrawingCacheQuality, which I don't use, because image quality is important in my case. I don't preach DRAWING_CACHE_QUALI‌​TY_HIGH, but suggest whosoever is interested to delve deeper and tweak the option. – Galya Nov 14 '17 at 12:07
  • Look at the documentation of [`View#setDrawingCacheEnabled (boolean)`](https://developer.android.com/reference/android/view/View.html#setDrawingCacheEnabled%28boolean%29). It explicitly says that `Calling draw(android.graphics.Canvas) will not draw from the cache when the cache is enabled. To benefit from the cache, you must request the drawing cache by calling getDrawingCache() and draw it on screen if the returned bitmap is not null.` – Abdelhakim AKODADI Nov 14 '17 at 17:55
  • That would be the case if you're creating your own RecyclerView. Fortunatelly it has its own caching mechanism, that's why you set setItemViewCacheSize() – Galya Nov 15 '17 at 09:49
  • Excellent answer! – Javlon Tulkinov Nov 18 '17 at 09:33
  • @Galya please stop making people waste their memory. look at the source code of [`RecyclerView`](https://android.googlesource.com/platform/frameworks/support/+/refs/heads/master/v7/recyclerview/src/main/java/android/support/v7/widget/RecyclerView.java). Do you see any call to `View#getDrawingCache()`? The answer is no. So please remove these two lines of code from tip #3. In addition to not having any performance improvements it adds memory overhead. – Abdelhakim AKODADI Nov 28 '17 at 14:30
  • I really appreciate the effort you make to help people by writing this answer. And it helped me too by the way. Thanks a lot. But as a community we have to be sure and exact about the information we share. – Abdelhakim AKODADI Nov 28 '17 at 14:55
  • @Galya @ AbdelhakimAkodadi this is a good article [link](https://blog.workable.com/recyclerview-achieved-60-fps-workables-android-app-tips/) form the Workable engineers mentioning the cache optimization. Personally I think with hardware accelerated devices the caching optimization doesn't have much effect but it wouldn't hurt to add it. – Karim Fikani Jan 25 '18 at 00:04
  • @KarimFikani Yes, thank you for motioning the article. I see this caching trick everywhere in articles across the net. Here is [another one](https://medium.com/@p.tournaris/recyclerview-how-we-achieved-60-fps-tips-in-workables-android-app-recyclerviews-c646c796473c). However I cannot find any source that says that it has any performance benefit. People are just using it just in case... – Abdelhakim AKODADI Feb 17 '18 at 00:44
  • `setHasStableIds(true)` is interchanging the items while scrolling.. @Atula – Santanu Sur Apr 23 '18 at 12:28
  • @SantanuSur, you need to provide a valid unique id for each item in [getItemId(int position)](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#getItemId(int)) in your RecyclerView.Adapter. – Galya Apr 30 '18 at 04:16
  • 1
    that again drops the performance @Galya ( setting stable ids and `overrding getItemId(pos)` – Santanu Sur Apr 30 '18 at 14:17
  • @SantanuSur, look more deeply into point 7 of the answer. Using stable ids and notifyDataSetChanged() is generally not recommended. It may help in some cases with the UI performance in the sense that a trackability of the views will be maintained for the user, but it won't help with the CPU/GPU usage. That being said you can see that many devs are satisfied with the result. – Galya May 01 '18 at 12:38
  • @Galya i just asked ..if there is any other solution..using stableIds..did increase the performance ...but because of a large or a huge list..it caused this problem of mixing up the items .. :) – Santanu Sur May 01 '18 at 12:48
  • 3
    setDrawingCacheEnabled() and setDrawingCacheQuality() are deprecated. Instead use hardware acceleration. https://developer.android.com/reference/android/view/View.html#setDrawingCacheEnabled(boolean) – Shayan_Aryan Dec 28 '18 at 13:41
  • `recyclerView.setHasFixedSize(true);` made a BIG difference for me. Note that it **does not require to have items of the same size**! However, it does requires the `RecyclerView` to have fixed width and height (i.e. not `wrap_content`). – MathieuMaree Jan 24 '19 at 12:58
  • **Note** that setting value to `setItemViewCacheSize` will prevent some call to `onBindViewHolder` at some point which will be a problem because you will not able to update/load when new added item in the list this happens like in a chat app where the list may display a lot of layout/chat bubble. Check this: [link](https://stackoverflow.com/a/38019962/10053046) and [link](https://stackoverflow.com/a/49460900/10053046) will help you to understand. – Mihae Kheel Sep 28 '19 at 14:25
  • i am trying this but not work i think i am some missing.. i am also remove load image code but not work. please help check my question https://stackoverflow.com/questions/60880908/add-same-fragment-more-than-two-time-then-recyclerview-scroll-lagginghang @Galya – Kishan Viramgama Mar 27 '20 at 11:14
13

I solved this problem by adding the following flag:

https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#setHasStableIds(boolean)

falvojr
  • 3,060
  • 4
  • 31
  • 54
waclaw
  • 433
  • 1
  • 7
  • 18
  • 3
    This makes a slight performance improvement for me. But RecyclerView is still dead-dog slow--much slower than an equivalent custom ListView. – SMBiggs Jun 10 '16 at 07:23
  • 1
    Ahh, but I discovered why my code is so slow--it has nothing to do with setHasStableIds(). I'll post an answer with more info. – SMBiggs Jun 10 '16 at 15:54
13

I discovered at least one pattern that can kill your performance. Remember that onBindViewHolder() is called frequently. So anything you do in that code has the potential to slam your performance to halt. If your RecyclerView does any customization, it's very easy to accidentally put some slow code in this method.

I was changing the background images of each RecyclerView depending on the position. But loading images takes a bit of work, causing my RecyclerView to be sluggish and jerky.

Creating a cache for the images worked wonders; onBindViewHolder() now just modifies a reference to a cached image instead of loading it from scratch. Now the RecyclerView zips along.

I know that not everyone will have this exact problem, so I'm not bothering to load code. But please consider any work that is done in your onBindViewHolder() as a potential bottle-neck for poor RecyclerView performance.

SMBiggs
  • 11,034
  • 6
  • 68
  • 83
  • I'm having the same problem. Right now I'm using Fresco for image loading and caching. Do you have another, better solution to load and cache images within RecyclerView. Thanks. – Androidicus Jun 28 '16 at 07:06
  • I am unfamiliar with Fresco (reading...their promises are nice). Maybe they have some insights on how to best use their caches with RecyclerViews. And I see you're not the only one with this problem: https://github.com/facebook/fresco/issues/414 . – SMBiggs Jun 28 '16 at 17:40
11

I had a talk about RecyclerView's performance. Here are slides in English and recorded video in Russian.

It contains a set of techniques (some of them are already covered by @Darya's answer).

Here is a brief summary:

  • If Adapter items have fixed size then set:
    recyclerView.setHasFixedSize(true);

  • If data entities can be represented by long (hashCode() for instance) then set:
    adapter.hasStableIds(true);
    and implement:
    // YourAdapter.java
    @Override
    public long getItemId(int position) {
    return items.get(position).hashcode(); //id()
    }
    In this case Item.id() would not work, because it would stay the same even if Item's content has changed.
    P.S. This is not necessary if you are using DiffUtil!

  • Use correctly scaled bitmap. Don't reinvent the wheel and use libraries.
    More info how to choose here.

  • Always use the latest version of RecyclerView. For instance, there were huge performance improvements in 25.1.0 - prefetch.
    More info here.

  • Use DiffUtill.
    DiffUtil is a must.
    Official documentation.

  • Simplify your item's layout!
    Tiny library to enrich TextViews - TextViewRichDrawable

See slides for more detailed explanation.

Oleksandr
  • 6,226
  • 1
  • 46
  • 54
11

In addition to @Galya's detailed answer, I want to state that even though it may be an optimization issue, it is also true that having the debugger enabled can slow things down a lot.

If you do everything to optimize your RecyclerView and it still doesn't work smoothly, try switching your build variant to release, and check how it works in a non-development environment (with the debugger disabled).

It happened to me that my app was performing slowly in debug build variant, but as soon as I switched to the release variant it worked smoothly. This doesn't mean that you should develop with the release build variant, but it is good to know that whenever you are ready to ship your app, it will work just fine.

blastervla
  • 539
  • 6
  • 19
  • This comment was really helpful for me! I have tried everything to improve my recyclerview's performance but nothing really helped, but once I have switched to the release build I realized everything is just fine. – Tal Barda Feb 22 '18 at 17:24
  • 1
    I faced the same issue even without the debugger attached but as soon as moved to release build,issue was no longer to be seen – Farmaan Elahi Aug 08 '18 at 06:14
10

I'm not really sure if the usage of setHasStableId flag is going to fix your issue. Based on the information you provide your performance issue could be related to a memory issue. Your application performance in terms of user interface and memory is quite related.

Last week I discovered my app was leaking memory. I discovered this because after 20 minutes using my app I noticed the UI was performing really slow. Closing/opening an activity or scrolling a RecyclerView with a bunch of elements was really slow. After monitoring some of my users in production using http://flowup.io/ I found this:

enter image description here

The frame time was really really high and the frames per second really really low. You can see that some frames needed about 2 seconds to render :S.

Trying to figure it out what was causing this bad frame time/fps I discovered I had a memory issue as you can see here:

enter image description here

Even when the average memory consumption was close to the 15MB at the same time the app was dropping frames.

That's how I discovered the UI issue. I had a memory leak in my app causing a lot of garbage collector events and that's was causing the bad UI performance because the Android VM had to stop my app to collect memory every single frame.

Looking at the code I had a leak inside a custom view because I was not unregistering a listener from the Android Choreographer instance. After releasing the fix, everything became normal :)

If your app is dropping frames due to a memory issue you should review two common errors:

Review if your app is allocating objects inside a method invoked multiple times per second. Even if this allocation can be performed in a different place where your application is becoming slow. An example could be creating new instances of an object inside a onDraw custom view method on onBindViewHolder in your recycler view view holder. Review if your app is registering an instance into the Android SDK but not releasing it. Registering a listener into a bus event could also be possible leak.

Disclaimer: The tool I've been using to monitor my app is under development. I have access to this tool because I'm one of the developers :) If you want access to this tool we will release a beta version soon! You can join in our web site: http://flowup.io/.

If you want to use different tools you can use: traveview, dmtracedump, systrace or the Andorid performance monitor integrated into Android Studio. But remember that this tools will monitor your connected device and not the rest of your user devices or Android OS installations.

3

In my RecyclerView, I use Bitmap Images For background of my item_layout.
Everything @Galya said is true (and I thank him for his great answer). But they didn't work for me.

This is what solved my problem:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options);

For more information please read this Answer.

James Dunn
  • 8,064
  • 13
  • 53
  • 87
Sajjad
  • 2,593
  • 16
  • 26
3

Its also important to check the parent layout in which you put in your Recyclerview. I had a similar scrolling issues when I testing recyclerView in a nestedscrollview. A view that scrolls in another view that scroll can suffer in performance during scrolling

saintjab
  • 1,626
  • 20
  • 31
3

I solved it by this line of code

recyclerView.setNestedScrollingEnabled(false);
eli
  • 8,571
  • 4
  • 30
  • 40
  • 2
    Ironically this simple option is what most people are probably looking for, whereas by looking at the upvotes they are profiling or looking for such complex solutions – nibbana Dec 21 '21 at 10:50
2

In my case, I found out that the notable cause of the lag is frequent drawable loading inside #onBindViewHolder() method. I solved it just by loading the images as Bitmap once inside the ViewHolder and access it from the mentioned method. That is all I did.

Wei
  • 1,028
  • 8
  • 27
2

In mycase I have complex recyclerview childs. So It affected the activity loading time (~5 sec for activity rendering)

I load the adapter with postDelayed() -> this will give the good result for activity rendering. after activity rendering my recyclerview load with smooth.

Try this answer,

    recyclerView.postDelayed(new Runnable() {
        @Override
        public void run() {
            recyclerView.setAdapter(mAdapter);
        }
    },100); 
Ranjithkumar
  • 16,071
  • 12
  • 120
  • 159
1

I see in the comments that you are already implementing the ViewHolder pattern, but I will post an example adapter here that uses the RecyclerView.ViewHolder pattern so you can verify that you are integrating it in a similar way, again your constructor can vary depending on your needs, here is an example:

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder> {

    Context mContext;
    List<String> mNames;

    public RecyclerAdapter(Context context, List<String> names) {
        mContext = context;
        mNames = names;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        View view = LayoutInflater.from(viewGroup.getContext())
                .inflate(android.R.layout.simple_list_item_1, viewGroup, false);

        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int position) {
        //Populate.
        if (mNames != null) {
            String name = mNames.get(position);

            viewHolder.name.setText(name);
        }
    }

    @Override
    public int getItemCount() {

        if (mNames != null)
            return mNames.size();
        else
            return 0;
    }

    /**
     * Static Class that holds the RecyclerView views. 
     */
    static class ViewHolder extends RecyclerView.ViewHolder {
        TextView name;

        public ViewHolder(View itemView) {
            super(itemView);
            name = (TextView) itemView.findViewById(android.R.id.text1);
        }
    }
}

If you have any trouble working with RecyclerView.ViewHolder make sure you have the appropriate dependencies which you can verify always at Gradle Please

Hope it resolves your problem.

Joel
  • 838
  • 5
  • 12
1

This helped me getting more smooth scrolling:

override the onFailedToRecycleView(ViewHolder holder) in the adapter

and stop any ongoing animations (if any) holder."animateview".clearAnimation();

remember to return true;

Roar Grønmo
  • 2,926
  • 2
  • 24
  • 37
1

Adding to @Galya's answer, in bind viewHolder,I was using Html.fromHtml() method. apparently this has performance impact.

Sami Adam
  • 76
  • 3
0

if you are using images in recycler view try using the function below to make images more lightweight

        fun getImageThumbnail(context: Context, imageFileName: String): Bitmap? {

        val options = BitmapFactory.Options()
        options.inSampleSize = 2
        options.inPreferredConfig = Bitmap.Config.RGB_565;

        val directory = context.filesDir
        val file = File(directory, imageFileName)
        if(!file.exists()) return null
        val bitmap:Bitmap = BitmapFactory.decodeStream(FileInputStream(file),null, options)!!
        var bigger = bitmap.width
        if(bitmap.width<bitmap.height) bigger = bitmap.height
        val zarib = (200.toDouble()/bigger)
        val width = (bitmap.width * zarib).toInt()
        val height = (bitmap.height * zarib).toInt()

        return ThumbnailUtils.extractThumbnail(bitmap,width,height)
    }
khoshrang
  • 146
  • 11
-1

i Solve this issue by using the only one line in with Picasso library

.fit()

Picasso.get().load(currentItem.getArtist_image())

                    .fit()//this wil auto get the size of image and reduce it 

                    .placeholder(R.drawable.ic_doctor)
                    .into(holder.img_uploaderProfile, new Callback() {
                        @Override
                        public void onSuccess() {


                        }

                        @Override
                        public void onError(Exception e) {
                            Toast.makeText(context, "Something Happend Wrong Uploader Image", Toast.LENGTH_LONG).show();
                        }
                    });