w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.47k stars 658 forks source link

[resize-observer] Notification after element is no longer referenced #5155

Open jods4 opened 4 years ago

jods4 commented 4 years ago

As far as I can tell, when the ResizeObserver repo was archived, issues were closed but not copied over 😞

This is a copy of WICG/resize-observer#55 from @atotic

It is an important issue because depending on the answer, not calling disconnect or unobserve could lead to memory leaks.

Original issue:


We need to clarify Object Lifetimes for ResizeObserver and Elements.

Current specification discusses states that:

A ResizeObserver will remain alive until both of these conditions are met:

  • there are no scripting references to the observer.
  • the observer is not observing any targets.

The relationship whether RO has influence on Element object lifetime is not discussed.

Clarifying this is important in following corner case:

let el = document.querySelector("#id");
let ro = new ResizeObserver();
ro.observe(el);
setTimeout( _ => { 
  el.remove(); 
  el = null;
  // will there be any notifications after this timeout executes.
  // only remaining reference to el is inside RO.
}

Current Chrome code implicitly unobserves element if there are no JS references to it. This is not captured by the specification.

MustafaHaddara commented 1 year ago

Commenting because I wasn't able to find this written down in the spec, MDN, etc., and had to discover this empirically: it seems that the current implementation of ResizeObserver in Google Chrome (v109.0.5414.87) will keep the ResizeObserver around even when the element has been removed from the DOM, and then the ResizeObserver will throw starting throwing a ton of errors (I suspect one error per frame?) and will not stop until the ResizeObserver is cleaned up or we call disconnect() on it.

In my case, we're using a ResizeObserver in a React codebase to watch the height of an iframe, like so:

export const IFrame = ({ src, title }) => {
  const [iframeHeight, setIframeHeight] = useState();
  const ref = useRef();

  return (
    <iframe
      src={src}
      style={{
        width: '100%',
        height: iframeHeight,
        border: 'none',
        padding: '15px',
        backgroundColor: '#fff',
      }}
      onLoad={() => {
        const component = ref.current.contentWindow.document.getElementsByClassName('embedded-page-content')[0];

        const observer = new ResizeObserver(() => {
          setIframeHeight(`${component.scrollHeight + 30}px`);
        });

        observer.observe(component);
      }}
      title={title}
      ref={ref}
    />
  );
};

We saw that when the React component gets unmounted (ex. we navigate to a different page), the ResizeObserver would fire off tons of errors.

Our solution was:

export const IFrame = ({ src, title }) => {
  const [iframeHeight, setIframeHeight] = useState();
  const ref = useRef();
  const observerRef = useRef();

  useEffect(() => {
    // do nothing, but watch for the unload:
    return () => {
      observerRef.current?.disconnect();
    };
  }, []);

  return (
    <iframe
      src={src}
      style={{
        width: '100%',
        height: iframeHeight,
        border: 'none',
        padding: '15px',
        backgroundColor: '#fff',
      }}
      onLoad={() => {
        const component = ref.current.contentWindow.document.getElementsByClassName('embedded-page-content')[0];

        const observer = new ResizeObserver(() => {
          setIframeHeight(`${component.scrollHeight + 30}px`);
        });

        observer.observe(component);
        observerRef.current = observer;
      }}
      title={title}
      ref={ref}
    />
  );
};
atotic commented 1 year ago

current implementation of ResizeObserver in Google Chrome (v109.0.5414.87) will keep the ResizeObserver around even when the element has been removed from the DOM

RO will be retained only if there are JavaScript references to the observed elements, or RO. In your example, I believe that an RO callback has a component reference in its closure. That's what keeps the observation alive.

chandu193 commented 1 month ago

Current Chrome code implicitly unobserves element if there are no JS references to it. This is not captured by the specification.

@atotic Is this true for other browsers as well?

atotic commented 1 month ago

implicitly unobserves element if there are no JS references to it

I assume what they meant is: RO will unobserve an unreachable element. Unreachable element is an element that has been removed from DOM, and has no JS references. This makes sense, if the element is not in DOM, and is unreachable by JS, it will never change size again.

Is this true for other browsers as well?

I am not sure how other browsers implement RO. My guess is that they all do, otherwise they'd have memory leaks. How would you test this behavior? The element size will forever remain 0, 0, so the RO will never fire again. You might be able to test it by watching for memory leaks.