feat(#84): Improve performance and efficiency of service worker

This commit is contained in:
Josh Creek
2026-01-22 21:21:57 +00:00
parent dd9ced17d8
commit c5c63ab8ef
3252 changed files with 280 additions and 21 deletions
+106
View File
@@ -0,0 +1,106 @@
type InViewParams = {
rootMargin?: string;
threshold?: number | number[];
once?: boolean;
enabled?: boolean;
onChange?: (inView: boolean, entry: IntersectionObserverEntry) => void;
};
const observers = new Map<string, IntersectionObserver>();
const callbacks = new WeakMap<Element, (entry: IntersectionObserverEntry) => void>();
function normalizeThreshold(threshold: number | number[] | undefined) {
if (threshold === undefined) return [0];
return Array.isArray(threshold) ? threshold : [threshold];
}
function buildObserverKey(rootMargin: string, threshold: number | number[]) {
return `${rootMargin}|${JSON.stringify(normalizeThreshold(threshold))}`;
}
function getObserver(rootMargin: string, threshold: number | number[]) {
const key = buildObserverKey(rootMargin, threshold);
const existing = observers.get(key);
if (existing) return existing;
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
const callback = callbacks.get(entry.target);
if (callback) {
callback(entry);
}
}
},
{ rootMargin, threshold }
);
observers.set(key, observer);
return observer;
}
export function inView(node: Element, params: InViewParams = {}) {
let options: Required<InViewParams> = {
rootMargin: '200px 0px',
threshold: 0,
once: true,
enabled: true,
onChange: () => undefined,
...params
};
let observer = getObserver(options.rootMargin, options.threshold);
const handleEntry = (entry: IntersectionObserverEntry) => {
const isIntersecting = entry.isIntersecting || entry.intersectionRatio > 0;
options.onChange(isIntersecting, entry);
if (isIntersecting && options.once) {
observer.unobserve(node);
callbacks.delete(node);
}
};
const startObserving = () => {
callbacks.set(node, handleEntry);
observer.observe(node);
};
const stopObserving = () => {
observer.unobserve(node);
callbacks.delete(node);
};
if (options.enabled) {
startObserving();
}
return {
update(next: InViewParams = {}) {
const nextOptions: Required<InViewParams> = {
...options,
...next,
onChange: next.onChange ?? options.onChange
};
const observerKeyChanged =
buildObserverKey(options.rootMargin, options.threshold) !==
buildObserverKey(nextOptions.rootMargin, nextOptions.threshold);
const enabledChanged = options.enabled !== nextOptions.enabled;
if (observerKeyChanged || enabledChanged) {
stopObserving();
}
options = nextOptions;
if (observerKeyChanged) {
observer = getObserver(options.rootMargin, options.threshold);
}
if (options.enabled) {
startObserving();
}
},
destroy() {
stopObserving();
}
};
}
+47 -17
View File
@@ -1,13 +1,16 @@
<script lang="ts">
import { PUBLIC_USE_LOCAL_POKEMON_SPRITE_FOLDER } from '$env/static/public';
import { inView } from '$lib/actions/inView';
export let pokemonName: string;
export let pokedexNumber: string | number;
export let form: string | undefined;
export let spriteKey: string | undefined;
export let shiny: boolean | undefined = false;
export let loadingStrategy: 'eager' | 'lazy' | 'inView' = 'inView';
let imagePath = null as string | null;
let isInView = false;
function isFemaleForm(value?: string) {
return /^female\b/i.test((value ?? '').trim());
@@ -19,7 +22,10 @@
let formValue = form.trim();
formValue = formValue.replace(/^female[-\s]*/i, '');
formValue = formValue.replace(/\s*\(.*?\)/g, '').replace(/\s*\[.*?\]/g, '').trim();
formValue = formValue
.replace(/\s*\(.*?\)/g, '')
.replace(/\s*\[.*?\]/g, '')
.trim();
if (!formValue || formValue.toLowerCase() === 'male') return strippedPokedexNumber;
formValue = formValue
@@ -40,20 +46,11 @@
return formValue ? `${strippedPokedexNumber}-${formValue}` : strippedPokedexNumber;
}
function computeImagePath() {
let rootFolder =
$: {
const rootFolderBase =
PUBLIC_USE_LOCAL_POKEMON_SPRITE_FOLDER === 'true'
? '/sprites/home'
: 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/home';
if (shiny) {
rootFolder += '/shiny';
}
if (isFemaleForm(form)) {
rootFolder += '/female';
}
? '/sprites-small/home'
: 'https://raw.githubusercontent.com/jcreek/LivingDexTracker/master/static/sprites-small/home';
const resolvedSpriteKey = spriteKey?.trim() || buildFallbackKey();
if (!spriteKey?.trim()) {
console.warn('Missing sprite key for pokemon entry', {
@@ -63,14 +60,47 @@
});
}
return `${rootFolder}/${resolvedSpriteKey}.png`;
let rootFolder = rootFolderBase;
if (shiny) {
rootFolder += '/shiny';
}
if (isFemaleForm(form)) {
rootFolder += '/female';
}
imagePath = `${rootFolder}/${resolvedSpriteKey}.webp`;
}
$: imagePath = computeImagePath();
$: if (loadingStrategy !== 'inView') {
isInView = true;
}
function handleInView(inView: boolean) {
if (inView) {
isInView = true;
}
}
</script>
{#if imagePath}
<img src={imagePath} alt="sprite" />
<span
use:inView={{
enabled: loadingStrategy === 'inView',
once: true,
rootMargin: '200px 0px',
onChange: handleInView
}}
>
{#if loadingStrategy === 'inView' && !isInView}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<img
src={imagePath}
alt="sprite"
loading={loadingStrategy === 'lazy' ? 'lazy' : 'eager'}
decoding="async"
/>
{/if}
</span>
{:else}
<span class="loading loading-spinner loading-xs"></span>
{/if}
+19 -1
View File
@@ -8,7 +8,10 @@ import {
precacheAndRoute,
precache
} from 'workbox-precaching';
// import { NavigationRoute, registerRoute } from 'workbox-routing';
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { ExpirationPlugin } from 'workbox-expiration';
declare let self: ServiceWorkerGlobalScope;
@@ -29,6 +32,21 @@ if (Array.isArray(manifest)) {
// clean old assets
cleanupOutdatedCaches();
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'image-cache',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 3000,
maxAgeSeconds: 60 * 60 * 24 * 30,
purgeOnQuotaError: true
})
]
})
);
// let allowlist: undefined | RegExp[];
// if (import.meta.env.DEV) allowlist = [/^\/$/];