Improving Feed Scroll Speeds ⚡️
Authored by Vishnu Sundaresan, co-founder @ Snowball
One of the early, annoying issues we faced with Snowball was a choppy feed caused by a multitude of issues. React Native is so highly abstracted that bugs that are not immediately solvable are often hard to isolate. Our take-away? Stick to the basics.
After dinner, my roommate slammed open my door. ”Why is your feed so bad?! I can’t seem to scroll for longer than 5 seconds before I lose patience”. He then proceeded to list all other defects and issues that a choppy and laggy app causes. And so begins my journey into making our feed scroll smoother.
For most bugs, in what was then a fledgling react native app, the mental process for dealing with issues wasn’t complex:
Reproduce the issue and repeat reproduction until you can notice the parameters, logic or configuration that is most linked to the bug. Then, based on your prior knowledge, you either know what’s wrong, or you have to do a little digging.
This bug was easily reproducible. You just needed to scroll through our feed and the scroll jumped pretty often, creating a “choppy” experience. You could even isolate it to singular parts of the feed that the scroll jumped in. And thus begins the ballad of our hypotheses ideation and testing
#1 Hypothesis: It’s us. We introduced something recently in our code that created this effect.
So I bisected our changes and code, removing character by character, trying to find the inflection point of our bug affecting the code. It went pretty deep! Removing everything including and up to the post comments, images and user details, just post IDs were shown. Still a bad delay → It wasn’t a recent change, we were only just noticing it now.
So what could it be? Combing the internet gives us a few potential ideas to chase.
#2 Hypothesis: It’s not us. FlatLists are pretty slow. Use better alternatives.
Inspired from this Reddit post, I explored the idea that the base component we used, React Native’s in-built list component - the FlatList, has its own limitations that can’t be easily bypassed. There are some powerful libraries built to solve list loading and scroll right out of the box, linked here for convenience:
I won’t be diving into each solution and how they help, but these are the most popular implementations you’ll find online of a performant list.
In general, in their own way they implement the philosophy of recycling views. The idea that creating a new view for a list item is expensive - you can’t keep doing it and expecting no loss in performance. So instead, let’s recycle created views and change the content inside it to the next items in a list!
This makes sense! Our logs have rich images, comments, user details, user profile pictures, and some local state. They definitely need to be recycled to save on performance and keep the list smooth. This is it.
Implementing each is light work, testing even easier. Result? The same, choppy feed. back to the drawing board. And the internet.
#3 Hypothesis: Maybe it is us. Maybe FlatLists aren’t awful
The next thing we read was the react native documentation itself. Turns out flatlists can be good, you just need to know how to configure it
This required some profiling and evaluation of our app. How many sub-views did we need for our app? How many logs can a user reasonably see in a single page load? How fast do we need to render new ones?
React Native dev tools helped a lot here! I profiled our components, measured reasonable metrics are found a couple of valid ranges for our app. Implementation? 1 day.
Result? No change. The same, choppy feed.
It’s us, it’s them. Why won’t it just work?!
You might imagine our frustration at this stage. I couldn’t find better solutions online and so faced the most dreaded debugging step: Fixing it myself. We tried a few more tricks (Appendix A) but no major improvements.
Hitting the point of no inspiration is an awful place to be, and I tried to find senior react native developers to reach out to for advice, but was unable to find anyone in my network or vicinity.
One fine day while just looking at some projects for fun, I managed to land on the repository for the BlueSky social app. To my surprise it was open sourced - and in React Native!
In fact, they had the same stack as us. The exact same libraries. But BlueSky’s app is so smooth!! Their feed so fast!! Something had to give. I decided to a deep dive into their code to see how they implement their feed.
After a good weekend of mapping out their feed libraries, dependencies, logic, I found that their data loading was very similar. Their list component was the same as us (a FlatList! With some optimal settings). In fact their feed component and ours had 1 difference, they measured list item layouts by a custom function, and we didn’t.
With some more research, I found that list rendering in apps suffer from lack of knowing list sizes before hand, so the lists have to render first before the list component can calculate size.
Woah. That’s a huge performance drain!? Easy to fix too!
We have 4 combinations of logs - with/without images and with/without comments. I was able to measure them using screen dimensions for each type and calculate it well ahead of list rendering (during data fetching itself!). This made sense. This had to do it.
Result: Still laggy.
Huh?!
At this point, most would be baffled. I was too.
This was around the time I was repeating the sob story of this bug to any developer I was talking to, like a guy hung on a broken-up ex.
It was only until I met another fellow developer who reminded of an important debugging method - Minimal Reproducible Example (MRE). Didn’t we try this at the start? Sort of.
At the start, I tried approaching it top down, removing until the bug is found. But I conveniently forgot the bottom up approach (I know, rookie mistake).
Salvation 🪽
That was all I was missing. An MRE of the app. If you’ve read this far, you deserve knowing the issue.
Image Resolutions were too high for the screen to reasonably display.
Yep. That was it. It wasn’t the list, it wasn’t the scroll, it wasn’t data fetching, it wasn’t the component, it wasn’t the tooling, it wasn’t even react native..
It was just basic image scaling.
I swapped out the Image component (expo-images
for react-native
Image
) and boom, we have lightning fast speeds. What was interesting is other seemingly unrelated bugs got solved too! Navigation and routing was fast! Loading any page with any image was much faster. UX-wise it a few seconds saved is a huge.
Final Reflections
Writing this and even reflecting on that ridiculous exercise makes two things obvious -
- It’s easy to get wrapped in solving without proving basic facts.
- The React Native Community is awful in my home country, Singapore 🇸🇬
The first one should be obvious to anyone reading this. A big rookie mistake is skipping basic debugging steps when dealing with unfamiliar frameworks. MREs are so, so useful.
But also, seriously, why aren’t there more react native communities??
Most forums are outdated. If not for Expo, this would be such a difficult framework to use in the first place.
HMU if you’re working with React Native and want to form a community!
Email me here
But that’s all for us folks. Thanks for reading all the way if you did and be sure to check out (the now improved!) Snowball App!
Appendix A: List Normalization
Fortunately, there was still one more trick up our sleeve: List Data Normalization, based on an article I found on this common issues faced when re-rendering. I’ve had experience and a basic understanding of React and React concepts, so I knew about how list renders worked. Our issue was not with list items being mutated but scroll. Still, implementing it was a fun exercise (~1/2 day) and did improve load speeds and saved us when we added in post reactions!