'Svelte Performance: Derived from custom stores of arrays
I'm making a Trello clone in Svelte using custom and derived stores. Svelte reactivity watches for assignment, so I need to reassign my array stores, but I noticed stores derived from writeable arrays recalculate the entire array. Surely I can be more performant here, especially if only a single array value is updated?
// The store in question, edited for terseness
const cards = writeable([]);
// adding to the card somewhere in code
cards.update((cards) => (cards = [...cards, card]));
// the derived store, that loops through all cards (because of reduce)
// this groups the card by its listId
const cardsGroupedByList = derived(cards, ($cards) => {
return $cards.reduce((list, card) => {
console.log('recalculating cards');
const data = list.get(card.listId) || [];
return list.set(card.listId, [...data, card]);
}, new Map());
});
In the console, cardsGroupedByList will print n times, but Svelte only renders the corresponding component once, so this is purely data manipulation. Is there a better way to do this?
Here's a working Svelte REPL with multiple lists and cards.
Solution 1:[1]
One potential solution is to have 2 custom stores. It only updates once, but definitely loses the elegance of a derived store.
// cards now need their set and add methods modified
// otherwise I risk my group running out of sync
export const cards = (() => {
const { subscribe, set, update } = writable<Card[]>([]);
return {
subscribe,
set: (cards: Card[]) => {
cardsGroupedByList.set(cards); // set the group, reduce once
set(cards);
},
add: (card: Card) => {
cardsGroupedByList.update(card); // add to the group
update((cards) => (cards = [...cards, card]));
},
};
})();
// groupBy becomes a custom store that retains internal state
// run the risk of the group becoming out of sync with cards
// could have issues when the card moves -- would need to add/remove or recalc
export const cardsGroupedByList = (() => {
const init = new Map();
const { subscribe, set, update } = writable(init);
function groupBy(groups, card) {
console.log('calculating groups'); // same console log, only happens once
const data = groups.get(card.listId) || [];
return groups.set(card.listId, [...data, card]);
}
return {
subscribe,
set: (cards: Card[]) => set(cards.reduce(groupBy, init)),
update: (card) => {
update((groups) => groupBy(groups, card));
},
};
})();
Solution 2:[2]
In your specific use case you dont need to create an additionnal store and you could simply reduce in the App script :)
App.svelte
<script>
import { cards } from './card-store.js';
import Card from './Card.svelte';
import CardForm from './CardForm.svelte';
let cardsList
$: cardsList = $cards.reduce((acc, card) => {
acc[card.listId] = acc[card.listId] || []
acc[card.listId].push(card)
return acc
}, {})
</script>
{#each [1,2,3] as idx}
<h2>List: {idx}</h2>
<ul>
{#each cardsList[idx] as card}
<Card {...card} />
{/each}
</ul>
<CardForm listId={idx} />
{/each}
And remove the cardsGroupedByList entry from the card-store
edit: Im not sure this is much different from what you do since the structure is recomputed each time anyway :/
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | bholtbholt |
| Solution 2 | Ji aSH |
