First of all, you should measure whether any action is even necessary.
If so, you could try simply creating an observer in each component, and only if that is too expensive extract the observer instance.
To do that you can create the observer at the top level of your application and set a context that passes the observer on to all descendants. Then in the icon component you can get the context and call observe in onMount and unobserve in onDestroy. You would want to observe some container element in the component.
You may have to add something like an EventTarget to the context so the icons have something to get events from. In the observer callback a new event can then be dispatched to that to notify the components. The arguments from the callbacks have to be passed on, so the icon can check whether it was among the intersected elements. (Event subscription and unsubscription should be done in onMount/onDestroy.)
Example:
// In root
const context = setContext('intersection-observer', writable(null));
onMount(() => {
const event = new EventTarget();
const observer = new IntersectionObserver(
entries => event.dispatchEvent(new CustomEvent('intersect', { detail: entries })),
{ root: null, rootMargin: '0px', threshold: 0 },
);
$context = {
observe: element => observer.observe(element),
unobserve: element => observer.unobserve(element),
onIntersect: event,
};
return () => observer.disconnect();
});
<!-- Component that uses the observer -->
<script>
import { getContext } from 'svelte';
import { onDestroy } from 'svelte';
const intersectionContext = getContext('intersection-observer');
const cleanup = [];
let root;
let visible = false;
onDestroy(() => cleanup.forEach(fn => fn()));
$: if ($intersectionContext && root) {
$intersectionContext.observe(root);
cleanup.push(() => $intersectionContext.unobserve(root));
$intersectionContext.onIntersect.addEventListener('intersect', onIntersect);
cleanup.push(() =>
$intersectionContext.onIntersect
.removeEventListener('intersect', onIntersect)
);
}
function onIntersect(e) {
const entries = e.detail;
const entry = entries.find(entry => entry.target === root);
if (entry)
visible = entry.isIntersecting;
}
</script>
<div bind:this={root}>
<!-- Render expensive content here using {#if visible} -->
{visible ? 'Visible' : 'Invisible'}
</div>
REPL
(This is a bit more complicated than what I described because this is designed to support SSR. To support SSR, APIs like IntersectionObserver cannot be used outside of onMount.)
If you do not mind a bit more convention-based magic, you can just tag elements in some way, e.g. using a data attribute and then send events straight to those elements.
// In root
onMount(() => {
const intersectionObserver = new IntersectionObserver(
entries => entries.forEach(entry =>
entry.target.dispatchEvent(
new CustomEvent('intersect', { detail: entry })
)
),
{ root: null, rootMargin: '0px', threshold: 0 },
);
const mutationObserver = new MutationObserver(mutations =>
mutations.forEach(m => {
m.addedNodes.forEach(node => {
if (node instanceof HTMLElement &&
node.dataset.intersect != null &&
node.dataset.intersectInitialized == null) {
intersectionObserver.observe(node);
node.dataset.intersectInitialized = 'true';
}
});
m.removedNodes.forEach(node => {
if (node instanceof HTMLElement) {
intersectionObserver.unobserve(node);
}
});
})
);
[...document.querySelectorAll('[data-intersect]')].forEach(node => {
intersectionObserver.observe(node);
node.dataset.intersectInitialized = 'true';
});
mutationObserver.observe(document.body, { childList: true, subtree: true });
return () => {
mutationObserver.disconnect();
intersectionObserver.disconnect();
};
});
<script>
let visible = false;
</script>
<div data-intersect
on:intersect={e => visible = e.detail.isIntersecting}>
<!-- Render expensive content here using {#if visible} -->
{visible ? 'Visible' : 'Invisible'}
</div>
REPL