SCC-5375: Multiselect lazy loading#2007
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
7emansell
left a comment
There was a problem hiding this comment.
overall looks good to me, and it's working great in Storybook, just a couple notes/questions on the implementation.
I do have to ask my favorite question though... have you tried this in RC advanced search and if so is it performant
| }, 1); // Ensure focus logic runs after state update | ||
| }; | ||
|
|
||
| const onItemsListScroll = () => { |
There was a problem hiding this comment.
I think this chunk can be replaced by setting rootMargin on the IntersectionObserver– i'm new to that API but it seems like if it's set (to 200px or something) it would trigger the same anticipatory load?
There was a problem hiding this comment.
This chunk allows for a more gradual and scaled loading - without it, scrolling down just a bit loads a lot of items (even with rootMargin at 0px). But that would work too, any thoughts on which is preferred?
I'm also quite new to the API so I could definitely be missing something else, and I can continue to explore it more
There was a problem hiding this comment.
No i see what you're saying here! I think it's worth the workaround to have a better scrolling experience
There was a problem hiding this comment.
Yes, the scrolling-loading could use some polishing. I'd love to have a chat to learn more about the API and the config.
There was a problem hiding this comment.
I put these console.logs right before the loadMoreLazyItems() call in the observer and right before loadMoreLazyItems() call in the onItemsListScroll function.
The scroll handler keeps moving the sentinel away before the observer can reach it. So the observer fires only once, while the scroll handler takes over and it's running a lot.
I tried removing onItemsListScroll and I personally did not observe much of a difference, so would love to see the difference you're seeing.
Also with regards to, "scrolling down just a bit loads a lot of items" -- isn't the number of items related to the setState in loadMoreLazyItems, rather than anything to do with the rootMargin?
setLazyItemsVisible((previousVisibleItems) =>
Math.min(
previousVisibleItems +
defaultItemsVisible +
previousVisibleItems / defaultItemsVisible, // Scales with further scrolling
itemsList.length
)
);
There was a problem hiding this comment.
@oliviawongnyc I didn't realize that, thanks for pointing that out! When I was testing in research catalog using just the observer, scrolling fast would cause stuttering. After scrolling all the way to the bottom, it took some time to load more items, and by then the user would be scrolling past the panel entirely. So I think I overcorrected for that and messed up the observer in the process without realizing it.
As for the difference, I misinterpreted the comment and thought the chunk was referring to the scaling part of it. Removing the scroll handler changes the behavior slightly but doesn't make the difference I mentioned, sorry for the confusion.
I'll take a look at reducing stuttering while only using the IntersectionObserver.
There was a problem hiding this comment.
I removed the scroll handler and tweaked some of the scaling. I tried calculating the rootMargin using pixel heights in the scroll panel but that didn't work out great, so the numbers I have now are from trial and error. Any thoughts on that or the scrolling experience now @7emansell @oliviawongnyc ?
There was a problem hiding this comment.
Changes looking good to me!
As for the scaling, what happens with a fixed root margin? rootMargin: "0px 0px 100px 0px", for example? I would think we just want the observer to trigger loading before someone reaches the bottom of the visible list and not need to scale, but lmk your thoughts.
Same question about the batching. Does the increment size need to scale?
| ); | ||
| }); | ||
|
|
||
| it("should lazily load more list items while scrolling in lazy load mode", async () => { |
There was a problem hiding this comment.
Clever test!! also wondering: did you try a more involved mock of the intersection observer and it got too messy ? It seems like it would be difficult to mock
There was a problem hiding this comment.
I did not 🤔 I could try it though if that would be preferred
There was a problem hiding this comment.
No worries just wondering
There was a problem hiding this comment.
Agreed on the good test and good mock.
| export type MultiSelectWidths = typeof multiSelectWidthsArray[number]; | ||
| export const multiSelectListOverflowArray = ["scroll", "expand"] as const; | ||
| export const multiSelectListOverflowArray = [ | ||
| "scroll", |
There was a problem hiding this comment.
Interested in marty/edwin thoughts but maybe lazy-load should be a flag you can set if you've picked scroll, not a standalone type
There was a problem hiding this comment.
Ah that does make sense
There was a problem hiding this comment.
Let me review before you make this update. Without having looked too closely at the PR, Emma's comment sounds reasonable. Perhaps it can be an independent flag that works for both scroll and expand?
There was a problem hiding this comment.
this shouldn't be possible on expand right?
There was a problem hiding this comment.
Would it be wrong to always have lazy load be enabled for the scroll option? It works well and it doesn't seem like it has a big overhead.
There was a problem hiding this comment.
Yes, agreed. lazy-load should not be an option for listOverflow. It should be a boolean flag.
Here's another idea. Although, I am not sure if the timing of this is possible. Can lazy load be automatically enabled if the total options is greater than x? Otherwise, scrolling functions as is always has?
Do we know the threshold when performance takes a hit?
There was a problem hiding this comment.
I'm not sure about the threshold. I'm open to logging performance/render times compared to the number of items, but I imagine that performance could also vary based on how it's used in the consuming app?
Right now 20 + defaultItemsVisible are loaded initially per @bigfishdesign13 's suggestion (although defaultItemsVisible is only used for expand since the default appears to be ~7 for scrolling, but that might be a separate concern). Maybe this could be the threshold for when lazy loading is enabled?
There was a problem hiding this comment.
Yes, let's keep it at 20 + defaultItemVisible as the threshold for now and we can gauge its performance once it's live.
I did, without any other changes to RC Actually on second look there is a slight performance improvement in the language checkbox and input fields (if I squint). Previously I was still on DS 4.3.0 which might have slowed it down a bit |
if it makes a difference in dev mode that's awesome (thinking along the lines of our unit test duration..) Let me know if you have a branch that has this + 4.2.0 + no other changes in advanced search, i'd be interested to see. I like the idea of adding this as a non-breaking change, totally optional, to the |
Yes I pushed my changes to the |
EdwinGuzman
left a comment
There was a problem hiding this comment.
Some comments. Before I review the main file, we should decide on the approach and whether the lazy loading should always be enabled. If so, it might change the code.
| ); | ||
| }); | ||
|
|
||
| it("should lazily load more list items while scrolling in lazy load mode", async () => { |
There was a problem hiding this comment.
Agreed on the good test and good mock.
| observe: () => null, | ||
| disconnect: () => null, | ||
| }); | ||
| window.IntersectionObserver = jest |
There was a problem hiding this comment.
Emma already asked if you tested the rc npm release in Research Catalog. Did you also run tests in RC?
I noticed that whenever we mock global APIs in the DS, unit tests in consuming apps need to be updated as well. It's not bad, but we would just have to let teams know about the dependency.
There was a problem hiding this comment.
Yes, the IntersectionObserver mock is needed when lazy-load is selected. Would this be a factor at all in deciding whether lazy loading should be always enabled for scroll?
There was a problem hiding this comment.
Not a big factor but just something to call out when devs import the release version. Did you have to add it in a global pre-test jest file in RC?
There was a problem hiding this comment.
Seems like it's only needed if the number of items in the MultiSelect > defaultItemsVisible + 20, when lazy loading is triggered. Otherwise putting this block of code
const intersectionObserverMock = () => ({
observe: () => null,
disconnect: () => null,
unobserve: () => null,
})
window.IntersectionObserver = jest
.fn()
.mockImplementation(intersectionObserverMock)
either in the test files with the MultiSelect, or the global pre-test file is needed.
| export type MultiSelectWidths = typeof multiSelectWidthsArray[number]; | ||
| export const multiSelectListOverflowArray = ["scroll", "expand"] as const; | ||
| export const multiSelectListOverflowArray = [ | ||
| "scroll", |
There was a problem hiding this comment.
Would it be wrong to always have lazy load be enabled for the scroll option? It works well and it doesn't seem like it has a big overhead.
|
|
||
| const { scrollTop, clientHeight, scrollHeight } = itemsListRef.current; | ||
| const scrollThreshold = scrollHeight / 2; // Scales with further scrolling | ||
| const isAtBottom = |
There was a problem hiding this comment.
What do you think about isHalfwayThroughLoadedContent rather than isAtBottom? Then someone reading this can easily see that when isHalfwayThroughLoadedContent, loadMoreLazyItems() and I think it would act like a little bit of documentation.
There was a problem hiding this comment.
Scratch this. After looking at this longer, I don't think we should use two triggers for the loading, and think we should use the observer over this onScroll.
| Math.min( | ||
| previousVisibleItems + | ||
| defaultItemsVisible + | ||
| previousVisibleItems / defaultItemsVisible, // Scales with further scrolling |
There was a problem hiding this comment.
What are the benefits to scaling the batch?
There was a problem hiding this comment.
When I was experimenting with scrolling quickly, the scrolling speed can increase as I scroll further down. The scaling was to prevent too many items from loading at the beginning, while preventing the stuttering that I mentioned above if the user is scrolling quickly to the bottom (which I saw more on research catalog than storybook). Maybe I could set a constant higher amount to load, though @bigfishdesign13 suggested continuing with the scaling, which I can try adapting to the IntersectionObserver.
There was a problem hiding this comment.
The current configuration works well. No matter how fast I try to scroll, I never hit the bottom. I would prefer to load a constant number of options during the scrolling, but I also don't want users to hit the bottom.
The data for all of the options is always available, right? No API call to get more data from the server. So any lag that happens as the options are added is due to rendering, which could be impacted by the power of a users computer or device. If there are concerns about the scaling, let's try a static number and try to find the sweet spot.
There was a problem hiding this comment.
I realized that a lot of the slowness issues I was having without scaling was because I was testing in research catalog dev mode earlier, my mistake. I removed the scaling and set the rootMargin to 300px which seems to work well enough, let me know if there are any issues with that
| <Banner | ||
| content={ | ||
| <> | ||
| <strong>IMPORTANT:</strong> This prop can only be used with{" "} |
There was a problem hiding this comment.
Minor: update "with" to "when"?
EdwinGuzman
left a comment
There was a problem hiding this comment.
This is looking great. Just have a few comments and questions.
I think what's left to do is to always enable lazy loading when scrolling is selected. @bigfishdesign13 just want to make sure we agree on this; you suggested an additional boolean flag.
| return { | ||
| observe: observeMock, | ||
| disconnect: disconnectMock, | ||
| unobserve: jest.fn(), |
There was a problem hiding this comment.
This doesn't also need to be cleared?
| }); | ||
| }); | ||
|
|
||
| const triggerIntersection = () => { |
There was a problem hiding this comment.
Nice update. How did you catch that the mock scroll behavior was not actually working as expected?
There was a problem hiding this comment.
The test was failing because no more items were being loaded on scroll - seems like the scroll handler was taking over. I also didn't realize earlier that the IntersectionObserver doesn't work in the test environment because the environment doesn't have the browser layout engine.
|
|
||
| setLazyItemsVisible((previousVisibleItems) => | ||
| Math.min( | ||
| previousVisibleItems * 1.2 + lazyLoadIncrementNum, |
There was a problem hiding this comment.
Can you create a CONSTANT_VARIABLE for 1.2? This is to reduce magic numbers and better understand when reading this why it's set to 1.2.
| { | ||
| root: itemsListRef.current, | ||
| threshold: 0, | ||
| rootMargin: `${12 * lazyItemsVisible}px`, |
There was a problem hiding this comment.
Can you also create another constant var for 12?
| ))} | ||
| </CheckboxGroup> | ||
| {isOverflowExpand && <ExpandToggleButton />} | ||
| {isOverflowLazy && |
There was a problem hiding this comment.
It makes sense why we need this, but can you add a short comment describing why? When we revisit this component in the future, it'll be clear why this is needed in the DOM.
@EdwinGuzman Yes, always enable lazy loading when scrolling is selected. Consistency. There's no need to add an additional boolean to toggle this feature on and off. |
Fixes JIRA ticket SCC-5375
This PR does the following:
lazy-loadoption to thelistOverflowprop of theMultiSelectcomponentHow has this been tested?
Accessibility concerns or updates
Accessibility Checklist
aria-liveis used, a screenreader was used to verify the text is read.refs, focus management was reviewed.Open Questions
Checklist:
Front End Review: