Skip to content

Commit

Permalink
Replace custom virtual scroll with library for better resize performance
Browse files Browse the repository at this point in the history
  • Loading branch information
mircearoata committed May 15, 2024
1 parent 48b3058 commit f1fbd4d
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 80 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@graphql-typed-document-node/core": "^3.2.0",
"@lukeed/uuid": "^2.0.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/svelte-virtual": "^3.5.0",
"@urql/exchange-graphcache": "^6.4.0",
"@urql/exchange-persisted": "^4.1.1",
"@urql/svelte": "^4.0.4",
Expand Down
16 changes: 16 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 33 additions & 79 deletions frontend/src/lib/components/VirtualList.svelte
Original file line number Diff line number Diff line change
@@ -1,100 +1,54 @@
<script generics="T" lang="ts">
import { createVirtualizer } from '@tanstack/svelte-virtual';
import _ from 'lodash';
import { tick } from 'svelte';
// eslint-disable-next-line no-undef
export let items: T[];
export let itemHeight: number | undefined = undefined;
export let bench = 10;
export let itemHeight: number;
export let overscan = 5;
let clazz = '';
export { clazz as class };
export let containerClass = '';
let start = 0;
let end = 10;
let viewport: HTMLElement;
let container: HTMLElement;
let heightMap: number[] = [];
export let itemClass = '';
$: {
heightMap = Array.from({ length: items.length });
if(viewport && container && items.length > 0) {
tick().then(updateVisible);
}
}
function updateHeightMap() {
const virtualRows = Array.from(container?.children ?? []);
virtualRows.forEach((elem, idx) => {
heightMap[start + idx] = elem.clientHeight;
});
}
let virtualListEl: HTMLDivElement;
$: knownHeights = heightMap.filter((x) => !!x);
$: averageHeight = knownHeights.reduce((acc, curr) => acc + curr, 0) / knownHeights.length;
$: virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
count: items.length,
getScrollElement: () => virtualListEl,
estimateSize: () => itemHeight ?? 100,
overscan,
});
function getHeight(item: number): number {
return heightMap[item] ?? (itemHeight ?? averageHeight);
}
let viewportHeight: number;
$: if(viewport) {
// Add or remove elements when the viewport height changes
viewportHeight;
items;
updateVisible();
}
async function updateVisible() {
const { scrollTop } = viewport;
updateHeightMap();
let height = 0;
let newStart = 0;
while(newStart < items.length && height + getHeight(newStart) < scrollTop) {
height += getHeight(newStart);
newStart++;
}
let newEnd = newStart;
while(newEnd < items.length && height < scrollTop + viewport.clientHeight) {
height += getHeight(newEnd);
newEnd++;
}
start = Math.max(newStart - bench, 0);
end = Math.min(newEnd + bench, items.length);
}
$: top = _.range(0, start).map(getHeight).reduce((acc, curr) => acc + curr, 0);
$: bottom = _.range(end, items.length).map(getHeight).reduce((acc, curr) => acc + curr, 0);
$: visibleItems = items.map((data, index) => ({ index, data })).slice(start, end);
function itemCreated(_element: HTMLElement) {
updateHeightMap();
$: vitems = $virtualizer.getVirtualItems();
let virtualItemEls: HTMLDivElement[] = [];
$: if (virtualItemEls.length) {
virtualItemEls.forEach((el) => $virtualizer.measureElement(el));
}
</script>

<div
bind:this={viewport}
bind:this={virtualListEl}
style="overflow-anchor: none"
class="relative overflow-y-scroll h-full {clazz}"
bind:offsetHeight={viewportHeight}
on:scroll={updateVisible}
>
<div
bind:this={container}
style:padding-top="{top}px"
style:padding-bottom="{bottom}px"
class="overflow-hidden {containerClass}"
style="height: {$virtualizer.getTotalSize()}px; width: 100%;"
class="overflow-hidden"
>
{#each visibleItems as item (item.index)}
<div class="overflow-hidden" use:itemCreated>
<slot item={item.data}>Missing template</slot>
</div>
{/each}
<div
style="transform: translateY({vitems[0]
? vitems[0].start - $virtualizer.options.scrollMargin
: 0}px);"
class="absolute top-0 left-0 w-full overflow-hidden"
>
{#each vitems as row, idx (row.index)}
<div
bind:this={virtualItemEls[idx]}
class="overflow-hidden {itemClass}">
<slot item={items[row.index]}>Missing template</slot>
</div>
{/each}
</div>
</div>
</div>
6 changes: 5 additions & 1 deletion frontend/src/lib/components/mods-list/ModsList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@
{/if}
</div>
{:else}
<VirtualList containerClass="mx-4" items={displayMods} let:item={mod}>
<VirtualList
itemClass="mx-4"
itemHeight={84}
items={displayMods}
let:item={mod}>
<ModsListItem
{mod}
selected={$expandedMod == mod.mod_reference}
Expand Down

0 comments on commit f1fbd4d

Please sign in to comment.