How to improve performance when rendering map polygons on React Native projects
Unluckily, the react-native-maps library isn’t as maintained, complete or optimised in comparison to other JS map libraries like Leaflet. When using react-native-maps to render polygons with thousands of vertices, we run across with serious bottlenecks which triggered the app crash due to the consumption of all the available device RAM.
Preparing stress tests on iOS and Android devices
Thanks to the profiling & debugging tools we discovered and learnt to use, we carried out some stress tests profiling the affected flows on iOS and Android devices in order to determine the cause of the crashes.
For the first stress tests, I used what we call turtle and rabbit devices (slow and average), iPad Mini 2 as a 🐢 and iPhone SE (2016) as 🐇. The flow to reproduce was trying to get rendered a polygon shape with 12k vertices.
iPad Mini 2 total RAM is 1GB, according to Xcode, the max RAM available for the app is 700MB. It seems that the allowed RAM to consume for apps on iOS is below 70%, and the recommended value would be not to exceed the 50%, according to some users tests (source):
I run the same test on iPhone SE (2016) which has 2GB RAM, which successfully loaded the shape and the maximum RAM peak reached was of almost 615MB, not reaching the allowed limit RAM. The flat part of the diagram is the app with the shape loaded on screen waiting for input:
You can also check that on iPad Mini it took almost 6 minutes to try to load the 12k vertices shape and finally crashing. On iPhone SE (2016) it “only” took 3 minutes and more than a half to successfully load & render the shape on screen.
On Android, it’s not that clear which is the RAM percentage available for the apps to use. At first, we thought that the default available memory defined by the OS was of 512MB, but it seems it’s not true, as it depends on the device manufacturer:
“All this is why the available RAM listing in settings isn’t the same as the total amount of RAM installed inside your phone. The full amount really is inside, but a portion of it (usually about 1GB or so) is reserved. Your apps get to fight over the rest.”
“As you use your phone, you’ll use many of the same apps more than others. These apps will tend to stay resident in RAM and be running so they are available in an instant. Having that RAM free instead means the apps would need to restart the processes that allow you to interact with them, and that’s slower and uses more battery power than keeping them resident in RAM.
It’s a true saying for your Android (or iOS) phone, but not your Windows computer or Chromebook (also a Linux-kernel-based OS ) because they manage RAM differently.”
How much RAM does your Android phone actually need?
We first used Alcatel One Touch Idol 3 as 🐢 and Xiaomi Mi A1 as 🐇, the following Profiler screenshots summarize the results, just like the way it happened on iOS:
1st bottleneck discovered: A bug in our code 🐞
After this first battery of stress tests, we discovered that one bottleneck was related to the holes of the shape, as setHoles was called repeatedly. We corrected and improved the code related with the shape polygon and holes rendering, and we achieved a great significant performance boost 🎉
This is a great example of how profiling & debugging tools can help us finding bugs that we introduced ourselves on our code.
2nd bottleneck discovered: Too many vertices 💥
Well, that was a bit obvious after the first stress tests 😂 Even so, we wondered why react-native-maps library suffered so much with this, while on an equivalent project that used Leaflet this problem didn’t exist 🤔
We discovered the existence of a tool called turf/simplify that returns a simplified version with less vertices of the geometry provided, according to an adjustable tolerance index. We also learnt that simplify was being used on Leaflet library (Applying it to PolyLine, test).
So, we decided to give it a try to simplify, after some tests on several shapes, we learnt that we couldn’t apply this logarithm uniformly to all the geometries, if the geometries had few vertices, this is how they looked:
We could apply it only to shapes with lots of vertices but defining different tolerance indexes depending on the shape total number of vertices (including holes), the ranges defined ended being these ones:
Vertices number & tolerance indexes applied>= 500 && < 800 : 0.00001>= 800 && < 1000 : 0.00005>= 1000 : 0.0001
By applying different tolerance indexes, we tried to achieve that the simplified figure looked the less polygonal and closer to the original one.
With these improvements we really did a good job with the RAM consumption and loading speed of shapes, this is a summary of how many vertexes we put away with the geometries used to test it:
Take into account that if the shape has a higher quantity of vertices close to each other, more vertices are discarded by the simplify tool.
On the next table, these are some examples on Android of how many RAM we saved when trying to load some of these geometries thanks to these improvements:
Android Manifest largeHeap parameter
We discovered the existence of an Android Manifest parameter called largeHeap=”true” which overwrites the default maximum heap size to “more”, making the app process to surpass the limit from 512MB on the Alcatel device.
Imho, this parameter sounded like… instead of diving on which are the causes of the app low performance on some flows and fixing them, you can use this quick patch 😕, and the official doc confirmed this feeling:
android:largeHeapWhether your application's processes should be created with a large Dalvik heap. This applies to all processes created for the application. It only applies to the first application loaded into a process; if you're using a shared user ID to allow multiple applications to use a process, they all must use this option consistently or they will have unpredictable results.
Most apps should not need this and should instead focus on reducing their overall memory usage for improved performance. Enabling this also does not guarantee a fixed increase in available memory, because some devices are constrained by their total available memory.To query the available memory size at runtime, use the methods getMemoryClass() or getLargeMemoryClass().
Nevertheless, I think this parameter could be interesting to use on apps that make a heavy use of image or video processing tasks.
More info:
Request large heap for your android app
What are the advantages of setting largeHeap to true?
Next possible improvements to take into account
We achieved great results with simplify, but this isn’t enough, we need to continue improving it 💪
On the case of geometries with more than 17K to 50K vertices, the devices are not able to load the data of these objects on memory (saving the data sent from backend to the mobx stores) and end crashing while reaching the top RAM limit, the OS provides the app:
The next proposals to improve and, expectedly, get ridden of this problem are:
- Let the backend take care of creating the simplified shape instead of creating it from the app. This way, the app receives and saves on memory the data with the optimised shape avoiding wasting the device RAM from the very first moment.
- On the app, instead of saving the shapes data on memory, save them to the database and load to memory only the shapes the app is currently using.
Conclusions and considerations
In order to apply new improvements related with react-native-maps polygons perfomance, they all need to be related with lowering the RAM use of the app, either by reducing the shapes number of vertices or/and by reducing the data we save into the stores and moreover, saving that data on the databases and query and load what we need to the memory.
The use of profiling tools helps developers check if the new improvements actually did work out or if they didn’t.
Performance has always been an underestimated topic. It doesn’t mind the programming sector, either videogames development, web or apps: Most developers and project managers don’t give a fuck about performance.
Some of them tend to think just on the happy path of the users devices (the last iPhone or Samsung Galaxy model) while not taking into account the real number of their users using a 🐢 device or, on the other side, not profiling how many RAM the project is sucking from the devices, driving to overheating problems and “unexpected” crashes.
Others, tend to trust on the magic that the framework, libraries or engine used does, but on most projects that isn’t enough.
Luckily, each time there are more and more projects and people that really worry about performance and overall, about offering the best experience to their users from the moment a new set of features is published. I hope you, reader, become one of them, in case you still aren’t 💪