-
Notifications
You must be signed in to change notification settings - Fork 58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(posts): TVJS scroll performance enhancement #458
Changes from 10 commits
ee664d1
9788126
f022409
47b5815
86dec61
7fa3d2d
c7f1484
7005ac7
d646e5e
e22a7d0
df10f2e
548bd63
729d451
5437a4b
bd0f7c4
080e6d1
ebf9276
8c347a8
e9ad2ce
947d938
d43efd3
69fd222
aa40051
128238b
0513aa0
bc534de
f10ee2d
f34477d
f6facdf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,115 @@ | ||||||||
--- | ||||||||
layout: post | ||||||||
title: How we improved scroll performance on Smart TV apps | ||||||||
description: From an R&D project came a new scroll implementation for our Smart TV apps, with better performance and experience. | ||||||||
author: [m_bernier] | ||||||||
tags: [TV, performance, javascript, react, web, frontend] | ||||||||
color: rgb(251,87,66) | ||||||||
--- | ||||||||
|
||||||||
A core experience of a Bedrock app for the end user is browsing the catalogue. Scrolling vertically through blocks of content, and scrolling horizontally through lists of items. TVs do not offer high performance and provide poor user experience during heavy resource actions. Namely, we noticed that scrolling horizontally in a list was laggy and unpleasant. This article focuses on performance optimization to enhance the horizontal scroll experience. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
because I felt that this block was not easy to read/understand There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would add "on TV" at the very end of the paragraph. Yes it's a repetition, but makes the goal of the article 100% clear. |
||||||||
|
||||||||
# [Context](#context) | ||||||||
On TV, we scroll horizontally by focusing each item sequentially when the user presses the left or right arrow button on their remote. | ||||||||
|
||||||||
Scrollable lists can be of various sizes and even include paginated content. In cases of paginated content, the next page is fetched preemptively during scroll, when the focus reaches a certain threshold. | ||||||||
|
||||||||
Our old scroll component worked as follows: we would render a whole list of items, in a parent component handling scroll. When scrolling horizontally, the focus would switch to the next item. This would notify the parent component in charge of scroll, that would move the whole list laterally. The movement was computed from the measurements of the focused item, the size of the list, and the size of the container. | ||||||||
BernierMaxence marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
There are multiple chances for improvement in this implementation. | ||||||||
BernierMaxence marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
1. Since every item was rendered in the DOM, moving the whole list was very heavy. Subsequently, a whole page of lists was itself pretty heavy to render. | ||||||||
2. Because the whole list is rendered, a new page fetched means the new items are immediately all rendered to the DOM when a new page is fetched, imposing a heavy load to display content that is not even on the screen. | ||||||||
BernierMaxence marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
# [Virtualization](#virtualization) | ||||||||
To address the first shortcoming of the initial approach, we introduced virtualization. Virtualization is a technique to render only the items that are visible on the screen. | ||||||||
|
||||||||
For context, the content of the list is stored in a redux store, normalized: to select a specific item from the store, all you need is its index in the array of items for the corresponding list. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rephrase
Suggested change
|
||||||||
|
||||||||
```javascript | ||||||||
const ItemComponent = ({ position }) => { | ||||||||
const item = useSelector(selectItemByIndex(position)); | ||||||||
|
||||||||
return <Item {...item} />; | ||||||||
} | ||||||||
``` | ||||||||
The virtualized scroll renders items based on a static array, each cell of the array being a slot for the item it’s going to display. | ||||||||
```javascript | ||||||||
const ScrollComponent = () => { | ||||||||
const SCROLLER_BASE_ARRAY = Array.from( | ||||||||
{ length: nbItemsToDisplay }, | ||||||||
(_, index) => index - 1 | ||||||||
); | ||||||||
|
||||||||
return SCROLLER_BASE_ARRAY.map(index => { | ||||||||
const [focusOffset, setFocusOffset] = useState(0); | ||||||||
// focusOffset is a state updated upon user input: | ||||||||
// + 1 when the right arrow is clicked, -1 when the left arrow is clicked | ||||||||
const position = index + focusOffset; | ||||||||
|
||||||||
|
||||||||
return ( | ||||||||
<ItemComponent | ||||||||
key={position} | ||||||||
position={position} | ||||||||
/> | ||||||||
); | ||||||||
}); | ||||||||
} | ||||||||
``` | ||||||||
|
||||||||
![Schema representing 4 empty slots](/images/posts/2024-11-22-tvjs-scroll-performance-enhancement/empty-slots.png) | ||||||||
|
||||||||
Each cell is connected to the store and uses its own index as selection parameter to get the corresponding item in the store (Cell of index 0 gets the first item, cell of index 1 gets the second…) | ||||||||
BernierMaxence marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
![Schema representing 4 slots with rendered items inside](/images/posts/2024-11-22-tvjs-scroll-performance-enhancement/filled-slots.png) | ||||||||
|
||||||||
At this point, only a subset of the list is rendered, as many items as the static array has cells. | ||||||||
|
||||||||
Horizontal scroll is managed by incrementing the selection index upon user input (e.g., pressing the right arrow key). Using the same array, each cell now selects from the store the item for its index plus an “offset,” that describes how much the list is scrolled. | ||||||||
BernierMaxence marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
![Schema representing 4 slots with rendered items inside](/images/posts/2024-11-22-tvjs-scroll-performance-enhancement/filled-slots-with-offset.png) | ||||||||
|
||||||||
By switching items to a cell over at every user input, we achieve a visual scroll, with only a subsection of the list displayed on screen. | ||||||||
BernierMaxence marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
![Animation showing a scrolling list.gif](/images/posts/2024-11-22-tvjs-scroll-performance-enhancement/scrolling.gif) | ||||||||
|
||||||||
# [Optimised Rendering with React Keys](#optimised-rendering-with-react-keys) | ||||||||
|
||||||||
The heart of the implementation is to fill each cell with a new item at each scroll. From the point of view of a single cell, when we scroll, the item it displays is new. But we know that the item already existed in the DOM, just one cell over. This is where we can leverage React's keys mechanism. Items rendered use a key that combines the original cell index with the current scroll offset. These keys help React reconcile the item in cell 1 before render as the item in cell 2 after render as the same item, thus reusing the same DOM node. As a result, we get 0 re-renders for the items that are shifting places, significantly reducing the performance impact of a scroll. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Un petit saut de ligne pour couper le paragraphe et fluidifier la lecture et un peu de wording
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Je sais même pas quoi faire comme schema 😆 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oui c'est vrai que c'est pas simple à illustrer ça 🤔 |
||||||||
|
||||||||
<figure> | ||||||||
<img src="/images/posts/2024-11-22-tvjs-scroll-performance-enhancement/profiling.png" alt="profiling"/> | ||||||||
<figcaption>☝️Profiling during a single scroll right. The only items rendering are the ones with focus change (item losing focus and item gaining focus) and the new item that wasn’t on the screen. Every other item is unbothered by a horizontal scroll</figcaption> | ||||||||
</figure> | ||||||||
--- | ||||||||
|
||||||||
# [Optimised pagination](#optimised-pagination) | ||||||||
|
||||||||
A nice win from virtualization is the impact on pagination. Only a subset of items are rendered on the screen. Also, the list itself only needs to know about that subset of items since it uses a constant array to display its items. This means that a new page fetched has 0 impact on renders: the new items are added to the store, but the React component itself has no knowledge of that operation and triggers no re-renders. | ||||||||
|
||||||||
# [Results](#results) | ||||||||
_Note: measurements presented here are taken with the Chrome DevTools performance tab, with x6 CPU throttle and network connection limited to fast 4G to mimic a low-performance TV device and keep a steady test environment. Times are scripting and rendering times added._ | ||||||||
|
||||||||
We can compare a few benchmarks to exhibit the gains from the new scroller. | ||||||||
|
||||||||
|
||||||||
Scrolling right is obviously less expensive now. Here, measurements were taken from a single scroll right, in a 72 items list. | ||||||||
|
||||||||
|Before|After, with new scroll| | ||||||||
|-|-| | ||||||||
|462ms|41ms (-91%)| | ||||||||
|
||||||||
But more closely to the app's actual use, here is a scenario measuring the cost of scrolling right through a list of 72 items, with 8 pages fetched during scroll. | ||||||||
|
||||||||
| Before | After | | ||||||||
|-|-| | ||||||||
| 11615ms | 8631ms (-26%) | | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Le top du top serait d'avoir un .gif avec les deux cas où on peut voir la différence 😇 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. J'ai essayé, mais un gif c'est pas du tout représentatif : on se ne rend pas compte de la vitesse à laquelle on appuie sur la flèche, et il y a pas beaucoup de frames donc ça ne capture pas le lag 😬 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah oui, dommage 😢 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Je vais essayer ! |
||||||||
|
||||||||
Here, we include everything else a list does when scrolling (fetching new pages, additional handlers...), so the gain is less, but still significant. | ||||||||
|
||||||||
Scrolling down in a page with lighter lists is also more efficient. Here, measurements were taken during a scroll down 25 lists. | ||||||||
BernierMaxence marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
Beyond benchmarks, on-device tests were also conclusive: the scroll is smoother, we almost eliminate the lag caused by a pagination fetch. Overall, it feels better to scroll through a list. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🥖 It could be nice to have video before/after from a device to visually see the improvment, what do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Videos are tough : since you don't have the information of how fast the arrows are pressed, or the delay between the press and the scroll on screen, it's hard to really gauge the performance |
||||||||
# [Conclusion](#conclusion) | ||||||||
This frontend R&D project successfully addressed the scrolling performance issues on TV. The new scrolling solution dramatically improved performance by limiting re-renders. This optimization ensured a smoother scrolling experience, enhancing usability on TV devices. From this experience, we also moved on to implementing the same virtualization on the horizontal scroll of the catalogue, which presented its own challenges but was also a success. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Je crois que c'est mieux d'utiliser une photo de chez nous plutôt que depuis slack