When working with Sections, it is not unfrequent to ask yourself whether what you’re doing is performant, efficient, or idiomatic. The API is short and straightforward, which makes you feel like the problems have to be very subtle and hard to find. The reality is much simpler: Sections is built on top of Android’s RecyclerView and inherits all of its properties, which are common knowledge amongst Android developers. You could build your own if you need to, and it won’t be much different from what Sections gives you today.
In this document we’ll introduce the two key qualities of performance in Sections: identity and immutability. These qualities of a data model identity is what is used by Sections to define whether a section needs to be redrawn. The more unnecessary redraws, the worse your component will perform. Skipping a redraw for a component that has meaningfully changed is not acceptable.
Once you read through this document if you feel you need to know more and deeper about RecyclerView, we recommend the original articles on RecyclerView and DiffUtils, the foundations of Sections. You’ll find they have an extensive API full of flexibility and pitfalls we’ve condensed and simplified for Facebook use cases.
Immutability is a quality of a data model, where its values cannot change over time. An example of mutable data is an
ArrayList, which can add new elements. An example of immutable data is Java’s
String, which cannot be modified in-place, and whose operations always create a new
As a performance-oriented person your intuition may be that creating objects incurs on a penalty and mutability is obviously faster. This is certainly true for cases with small locality, such as single-class CPU-bound algorithms. For large-scale cases where concurrency is involved, the complexity added to prevent data races and other nasty behaviours heavily offsets the cost of a few additional allocations per second.
So, what can we do to minimise the number of additional allocations of immutable data models? Never request them if they’re not necessary! To do this on Sections we visit your data model to find the smallest granularity we have to request a redraw for. This is done using the data model’s identity.
Identity is another quality of a data model that defines whether it is equal to another data model, or even itself. The action of comparing two data models to find the most granular differences is referred as diffing.
What the equality is for a data model is to be defined by its creator. It can be defined by its reference in memory (referencial equality), the hash of its contents (hash equality), the value of a field such as id (identifier equality), or even delegate to the equality of each of its fields (structural equality).
Let’s see one example,
User, which has a list of
We could define the identity of
User as its reference, which implies:
Or we could define it as the structure or hash of its contents, which becomes:
Or we could just use the
user identifier as the identity:
So, choosing the right identity for your data model is key for performance.
Notice that these go from more strict to less strict: reference, structure and hash, then identifier.
Java classes use the
equals function to define their identity, and the default implementation delegates to referential equality. Do remember to
@Override it accordingly.
SingleComponentSection is built as a thin wrapper, so it follows the Component rules for identity. If a wrapped Component has not significantly changed it won’t trigger a redraw.
The diffing algorithm for Components starts by checking the referential equality of the component with
==, then it traverses its Props and checks their structural equality with
The main API of Sections is a wrapper around Android’s
RecyclerView operations, calling notifications for updates, insertions and removals behind the scenes. It takes a
List of elements and uses a traversal algorithm that takes into account all the kinds of identity equality listed above by using
equals and user defined Events.
First it checks whether it needs to do a granular identity check on the data model, and if the check passes it visits the contents of the data model.
The check for granularity does referential equality with
==, then checks identifier equality using the event
OnCheckIsSameItemEvent, and lastly it falls back to structural equality with
equals. Remember that Java’s
equals defaults to referential equality. If any of them passes, it goes one level further to check for equality on its contents using the event
These two new events
OnCheckIsSameContentEvent exist for the benefit of data models where it’s not possible to override equals directly, i.e. using GraphQL-generated models directly.
Their API is similar to other Litho Events, where a function needs to be annotated as the listener in the ComponentSpec. The model to compare has to be annotated with
This means that we’ve made the algorithm check if two
BookmarkFolderItemComponentGraphQL have the same reference with
==, then if they have the same
id identifier, and lastly if they match their structure with
equals. If either of these checks pass, then we dig deeper to compare the structure of their bookmark field.
With this knowledge in mind, we can talk about tips and pitfalls to take into account to prevent over- and under- redraws.
Same as with any other Component, Sections have a key that can be overloaded to the user’s benefit. If the key is unstable, meaning that it changes at a different frequency than its contents, it will result in overdraws. Double check both for the Section and the RecyclerCollectionComponent.
Any bugs or unexpected behaviours inherited from the Android implementation remain. It is not uncommon to encounter a known issue, so be sure to also search the Android bugtracker.
One frequent support request asks why adding elements before the first element doesn’t scroll the view to the top on the first redraw? This is an inherited behaviour that is easily solved by
requestFocus to scroll to the current top position.
This happens frequently when events are not triggered when the caller and the listener are on different
ComponentContext. Check that your container
ComponentTree and the Components you're displaying belong in the same context.
We will use the example of a
RecyclerCollectionComponent that is acted externally, by selecting and highlighting a bookmark after it has been added from a post in another Component.
The best approach here is to rely on
DataDiffSection diffing to modify the right row. That will mean either modifying or wrapping the current data model to include a new property of whether it’s highlighted.
These additional object allocations are cheaper than a full redrawn for single-row changes.
We will use the example of a “Night Mode“ for your
RecyclerCollection. You’d like for the background to change between both day and night modes when a button that’s not part of the
RecyclerCollection is clicked.
This approach causes the list to lose its State. Prefer partial redraws wherever possible.
The easiest way of triggering a redraw from scratch is by making the color state part of the Section key. When your Component triggers the event and the algorithm traverses the view, it’ll see that the key has change and trigger a full redraw of the relevant Section.