Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Many IntersectObserver object created #22

Open
izderadicka opened this issue Aug 14, 2022 · 5 comments
Open

Many IntersectObserver object created #22

izderadicka opened this issue Aug 14, 2022 · 5 comments

Comments

@izderadicka
Copy link

I have been looking into code and if I understand code correctly new IntersectObserver is created for every component, that has inview action.

My scenario has one root element which has many (thousands) of child elements/components , which lazy load an image, when they get into view.

As per this article: https://www.bennadel.com/blog/3954-intersectionobserver-api-performance-many-vs-shared-in-angular-11-0-5.htm it looks that there is notable performance advantage for single IntersectObserver having many observed elements, comparing to many IntersectObservers, each one just with one observed element.

What do you think?

@maciekgrzybek
Copy link
Owner

Hey @izderadicka thanks for that insight. I'll take a look and get back to you :)

@brahma-dev
Copy link

Quick and dirty fix.

export const inview = (node: HTMLElement, options = {}) => {
	//adapted from https://github.com/maciekgrzybek/svelte-inview
	let defaultOptions = {
		root: null,
		rootMargin: '0px',
		threshold: 0,
		unobserveOnEnter: false,
	}
	const { root, rootMargin, threshold, unobserveOnEnter } = {
		...defaultOptions,
		...options,
	};

	let prevPos = {
		x: undefined,
		y: undefined,
	};

	let scrollDirection: ScrollDirection = {
		vertical: undefined,
		horizontal: undefined,
	};

	if (typeof IntersectionObserver !== 'undefined' && node) {
		// Global variable to hold observers;
		window._inview_observers = window._inview_observers || {};

		// Unique id for each observer (defaults to root for viewport)
		let observerid = "root";
		if (root != null) {
			observerid = root.getAttribute("data-svelte-inview-id");
			if (!observerid) {
				observerid = "_" + Math.floor(Math.random() * 100000);
				root.setAttribute("data-svelte-inview-id", observerid);
			}
		}
		window._inview_observers[observerid] = window._inview_observers[observerid] || new IntersectionObserver(
			(entries, _observer) => {
				entries.forEach((singleEntry) => {
					if (prevPos.y > singleEntry.boundingClientRect.y) {
						scrollDirection.vertical = 'up';
					} else {
						scrollDirection.vertical = 'down';
					}

					if (prevPos.x > singleEntry.boundingClientRect.x) {
						scrollDirection.horizontal = 'left';
					} else {
						scrollDirection.horizontal = 'right';
					}

					prevPos = {
						y: singleEntry.boundingClientRect.y,
						x: singleEntry.boundingClientRect.x,
					};

					const detail = {
						inView: singleEntry.isIntersecting,
						entry: singleEntry,
						scrollDirection,
						node: singleEntry.target,
						observer: _observer,
					};

					singleEntry.target.dispatchEvent(new CustomEvent('change', { detail }));

					if (singleEntry.isIntersecting) {
						singleEntry.target.dispatchEvent(new CustomEvent('enter', { detail }));

						unobserveOnEnter && _observer.unobserve(node);
					} else {
						singleEntry.target.dispatchEvent(new CustomEvent('leave', { detail }));
					}
				});
			},
			{
				root,
				rootMargin,
				threshold,
			}
		);


		// This dispatcher has to be wrapped in setTimeout, as it won't work otherwise.
		// Not sure why is it happening, maybe a callstack has to pass between the listeners?
		// Definitely something to investigate to understand better.
		setTimeout(() => {
			node.dispatchEvent(
				new CustomEvent('init', { detail: { observer: window._inview_observers[observerid], node } })
			);
		}, 0);

		window._inview_observers[observerid].observe(node);

		return {
			destroy() {
				window._inview_observers[observerid].unobserve(node);
			},
		};
	}
}

@maciekgrzybek
Copy link
Owner

@brahma-dev it is A solution, but definitely not THE solution :) Keeping things in a global variable doesn't sound like good idea to me, although it might be a solution for a quick fix :)
I thought about this whole thing @izderadicka and I'd like to go with the svelte store, but I won't really have time soon to do this. If you want to give it a shot, please, be my guest :) I'll let you know if I'll have time earlier to work on this.

@rgon
Copy link

rgon commented Jul 17, 2023

@maciekgrzybek here's an improved version of @brahma-dev 's code that creates a single store that holds all observers. It appears to behave properly.

import { tick } from 'svelte';
import { writable, get, type Readable } from 'svelte/store';
import type {
  ObserverEventDetails,
  Options,
  Position,
  ScrollDirection,
  Event,
  LifecycleEventDetails,
} from './types';

export function createMapStore<T>(initialValue: Record<string, T> = {}) : Readable<Record<string, T>> & {
    get: (k:string) => T;
    set: (key:string, value:T) => void;
    remove: (k:string) => void;
    update: (fn: (m:Record<string, T>) => Record<string, T>) => void;
} {
    const store = writable(initialValue);

    return {
        subscribe: store.subscribe,
        update: store.update,

        get: (k:string) => get(store)[k],
        set: (key:string, value:T) => store.update(m => Object.assign({}, m, {[key]: value})),
        remove(k:string) {
            store.update(s => {
                delete s[k];
                return s;
            });
        },
    }
}

// Store to hold 'global' observers
export const inview_observers = createMapStore<IntersectionObserver>({});

export default function inview (node: HTMLElement, options:Options = {}){
	//adapted from https://github.com/maciekgrzybek/svelte-inview
	const { root, rootMargin, threshold, unobserveOnEnter } = {
		...defaultOptions,
		...options,
	};

	let prevPos: Position = {
		x: undefined,
		y: undefined,
	};

	let scrollDirection: ScrollDirection = {
		vertical: undefined,
		horizontal: undefined,
	};

    // This ensures it's running in the browser, so we're safe to modify the shared store
	if (typeof IntersectionObserver !== 'undefined' && node) {
		let observerid:string = "root";
		// Unique id for each observer (defaults to root for viewport)
		if (root != null) {
			if (!root.getAttribute("data-svelte-inview-id")) {
				observerid = "_" + Math.floor(Math.random() * 100000);
				root.setAttribute("data-svelte-inview-id", observerid);
			}
		}

        if (!inview_observers.get(observerid)) inview_observers.set(observerid, new IntersectionObserver(
			(entries, _observer) => {
				entries.forEach((singleEntry) => {
					if ((prevPos.y ?? 0) > singleEntry.boundingClientRect.y) {
						scrollDirection.vertical = 'up';
					} else {
						scrollDirection.vertical = 'down';
					}

					if ((prevPos.x ?? 0) > singleEntry.boundingClientRect.x) {
						scrollDirection.horizontal = 'left';
					} else {
						scrollDirection.horizontal = 'right';
					}

					prevPos = {
						y: singleEntry.boundingClientRect.y,
						x: singleEntry.boundingClientRect.x,
					};

					const detail = {
						inView: singleEntry.isIntersecting,
						entry: singleEntry,
						scrollDirection,
						node: singleEntry.target,
						observer: _observer,
					};

					singleEntry.target.dispatchEvent(createEvent('inview_change', detail));

					if (singleEntry.isIntersecting) {
						singleEntry.target.dispatchEvent(createEvent('inview_enter', detail));

						unobserveOnEnter && _observer.unobserve(node);
					} else {
						singleEntry.target.dispatchEvent(createEvent('inview_leave', detail));
					}
				});
			},
			{
				root,
				rootMargin,
				threshold,
			}
		))

        inview_observers.get(observerid).observe(node);

        tick().then(() => {
            node.dispatchEvent(
				createEvent('inview_init', { detail: { observer: inview_observers.get(observerid), node } })
			);
		});

        return {
            destroy() {
                if (inview_observers.get(observerid)) {
                    inview_observers.get(observerid)?.unobserve(node);
                    inview_observers.remove(observerid);
                }
            },
        }
    } else {
        return {
            destroy() {},
        }
    }
}

@maciekgrzybek
Copy link
Owner

@rgon that looks great, can you prepare a PR for that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants