From 3b10a1f47fd5838fe3b94c19673a52610b88cf1e Mon Sep 17 00:00:00 2001 From: Fuwn Date: Sun, 1 Mar 2026 14:20:08 -0800 Subject: perf: optimise list hot paths and shared timers --- src/lib/List/Anime/CleanAnimeList.svelte | 83 ++++++++++++++++++++--------- src/lib/List/Anime/DueAnimeList.svelte | 47 +++++++++++----- src/lib/List/CleanGrid.svelte | 4 +- src/lib/List/CleanList.svelte | 4 +- src/lib/List/Manga/MangaListTemplate.svelte | 45 +++++++++++----- 5 files changed, 129 insertions(+), 54 deletions(-) (limited to 'src/lib/List') diff --git a/src/lib/List/Anime/CleanAnimeList.svelte b/src/lib/List/Anime/CleanAnimeList.svelte index 561b3b1d..aee035a4 100644 --- a/src/lib/List/Anime/CleanAnimeList.svelte +++ b/src/lib/List/Anime/CleanAnimeList.svelte @@ -37,7 +37,8 @@ export let limit: number | undefined = undefined; let showRoulette = false; - let keyCacher: ReturnType; + let airingRefreshTimeout: ReturnType | undefined; + let scheduledAiringAt: number | null = null; let totalEpisodeDueCount = media .map((anime) => { if ($settings.displayTotalEpisodes && !$settings.displayTotalDueEpisodes) return 1; @@ -81,35 +82,61 @@ ? media : media.filter((m) => m.mediaListEntry?.customLists?.[selectedList]); - onMount(async () => { - if (dummy) return; + const clearAiringRefreshTimeout = () => { + if (airingRefreshTimeout) clearTimeout(airingRefreshTimeout); + + airingRefreshTimeout = undefined; + scheduledAiringAt = null; + }; + + const scheduleAiringRefresh = () => { + if (!browser) return; + + if (dummy || media.length === 0) { + clearAiringRefreshTimeout(); + + return; + } + + const nextAiringAt = media.reduce((closest, currentMedia) => { + if (currentMedia.status !== 'RELEASING' && currentMedia.status !== 'NOT_YET_RELEASED') + return closest; + + const airingAt = currentMedia.nextAiringEpisode?.airingAt; + + if (!airingAt) return closest; + if (closest === null) return airingAt; + + return airingAt < closest ? airingAt : closest; + }, null); - keyCacher = setInterval( + if (!nextAiringAt) { + clearAiringRefreshTimeout(); + + return; + } + + if (airingRefreshTimeout && scheduledAiringAt === nextAiringAt) return; + + clearAiringRefreshTimeout(); + scheduledAiringAt = nextAiringAt; + airingRefreshTimeout = setTimeout( () => { - media = media; + const now = Date.now() / 1000; - if ( - media.some( - (m) => m.nextAiringEpisode?.airingAt && m.nextAiringEpisode.airingAt < Date.now() / 1000 - ) - ) + if (media.some((m) => m.nextAiringEpisode?.airingAt && m.nextAiringEpisode.airingAt < now)) animeLists = cleanCache(user, $identity); + + scheduleAiringRefresh(); }, - (() => { - const airingAt = media - .filter( - (m) => - (m.status === 'RELEASING' || m.status === 'NOT_YET_RELEASED') && - m.nextAiringEpisode?.airingAt - ) - .find((m) => m.nextAiringEpisode?.airingAt)?.nextAiringEpisode?.airingAt; - const untilAiring = airingAt - ? Math.round((airingAt - Date.now() / 1000) * 100) / 100 - : undefined; - - return untilAiring ? (untilAiring < 0 ? 1000 : untilAiring) : 1000; - })() + Math.max(1000, nextAiringAt * 1000 - Date.now() + 250) ); + }; + + onMount(async () => { + if (dummy) return; + + scheduleAiringRefresh(); if (browser) await localforage.setItem( @@ -120,7 +147,13 @@ ); }); - onDestroy(() => clearInterval(keyCacher)); + $: if (browser && !dummy) { + media; + + scheduleAiringRefresh(); + } + + onDestroy(() => clearAiringRefreshTimeout()); const increment = (anime: Media, progress: number) => { if (!dummy && pendingUpdate !== anime.id) { diff --git a/src/lib/List/Anime/DueAnimeList.svelte b/src/lib/List/Anime/DueAnimeList.svelte index 2db8da65..0c1e128a 100644 --- a/src/lib/List/Anime/DueAnimeList.svelte +++ b/src/lib/List/Anime/DueAnimeList.svelte @@ -16,27 +16,48 @@ let animeLists: Promise; let startTime: number; let endTime: number; - - const keyCacher = setInterval( - () => { - startTime = performance.now(); - endTime = -1; - animeLists = mediaListCollection(user, $identity, Type.Anime, $anime, $lastPruneTimes.anime, { - forcePrune: true, - addNotification - }); - }, - $settings.cacheMinutes * 1000 * 60 - ); + let keyCacher: ReturnType | undefined; + let keyCacheMinutes = -1; + + const restartKeyCacher = (cacheMinutes: number) => { + if (keyCacher) clearInterval(keyCacher); + + keyCacheMinutes = cacheMinutes; + keyCacher = setInterval( + () => { + startTime = performance.now(); + endTime = -1; + animeLists = mediaListCollection( + user, + $identity, + Type.Anime, + $anime, + $lastPruneTimes.anime, + { + forcePrune: true, + addNotification + } + ); + }, + cacheMinutes * 1000 * 60 + ); + }; onMount(async () => { + restartKeyCacher($settings.cacheMinutes); + startTime = performance.now(); animeLists = mediaListCollection(user, $identity, Type.Anime, $anime, $lastPruneTimes.anime, { addNotification }); }); - onDestroy(() => clearInterval(keyCacher)); + $: if (keyCacher && keyCacheMinutes !== $settings.cacheMinutes) + restartKeyCacher($settings.cacheMinutes); + + onDestroy(() => { + if (keyCacher) clearInterval(keyCacher); + }); const cleanMedia = ( anime: Media[], diff --git a/src/lib/List/CleanGrid.svelte b/src/lib/List/CleanGrid.svelte index 1e6f5a12..4f628c3c 100644 --- a/src/lib/List/CleanGrid.svelte +++ b/src/lib/List/CleanGrid.svelte @@ -17,7 +17,7 @@ let uniqueID = new Date().getTime(); - $: sortedMedia = reverseSort ? media.reverse() : media; + $: sortedMedia = reverseSort ? [...media].reverse() : media; $: processedMedia = limit !== undefined ? sortedMedia.slice(0, limit) : sortedMedia; @@ -25,7 +25,7 @@ class="covers" style={`grid-template-columns: repeat(auto-fill, minmax(${$settings.displayCoverWidth}px, 1fr))`} > - {#each processedMedia as title, index} + {#each processedMedia as title, index (title.id)} {@const progress = (title.mediaListEntry || { progress: 0 }).progress} {@const isAboveFold = index < 6} diff --git a/src/lib/List/CleanList.svelte b/src/lib/List/CleanList.svelte index 63656ab3..bf8c44ff 100644 --- a/src/lib/List/CleanList.svelte +++ b/src/lib/List/CleanList.svelte @@ -12,11 +12,11 @@ export let lastUpdatedMedia: number; export let reverseSort = false; - $: processedMedia = reverseSort ? media.reverse() : media; + $: processedMedia = reverseSort ? [...media].reverse() : media;
    - {#each processedMedia as title} + {#each processedMedia as title (title.id)} {@const progress = (title.mediaListEntry || { progress: 0 }).progress} {#if type === 'anime' ? upcoming || notYetReleased || progress !== (title.nextAiringEpisode?.episode || 9999) - 1 : progress !== title.episodes} diff --git a/src/lib/List/Manga/MangaListTemplate.svelte b/src/lib/List/Manga/MangaListTemplate.svelte index c2fc0513..f549496d 100644 --- a/src/lib/List/Manga/MangaListTemplate.svelte +++ b/src/lib/List/Manga/MangaListTemplate.svelte @@ -45,19 +45,35 @@ let rateLimited = false; let forceFlag = false; let lastListSize = 5; - - const keyCacher = setInterval( - () => { - startTime = performance.now(); - endTime = -1; - mangaLists = mediaListCollection(user, $identity, Type.Manga, $manga, $lastPruneTimes.manga, { - addNotification - }); - }, - $settings.cacheMinutes * 1000 * 60 - ); + let keyCacher: ReturnType | undefined; + let keyCacheMinutes = -1; + + const restartKeyCacher = (cacheMinutes: number) => { + if (keyCacher) clearInterval(keyCacher); + + keyCacheMinutes = cacheMinutes; + keyCacher = setInterval( + () => { + startTime = performance.now(); + endTime = -1; + mangaLists = mediaListCollection( + user, + $identity, + Type.Manga, + $manga, + $lastPruneTimes.manga, + { + addNotification + } + ); + }, + cacheMinutes * 1000 * 60 + ); + }; onMount(async () => { + restartKeyCacher(Math.max($settings.cacheMangaMinutes, 5)); + if (browser) { const lastStoredList = (await localforage.getItem( `last${due ? '' : 'Completed'}MangaListLength` @@ -96,7 +112,12 @@ } }); - onDestroy(() => clearInterval(keyCacher)); + $: if (keyCacher && keyCacheMinutes !== Math.max($settings.cacheMangaMinutes, 5)) + restartKeyCacher(Math.max($settings.cacheMangaMinutes, 5)); + + onDestroy(() => { + if (keyCacher) clearInterval(keyCacher); + }); const cleanMedia = async (manga: Media[], displayUnresolved: boolean, force: boolean) => { progress = 0; -- cgit v1.2.3