Skip to content

[BUG] Memory leak via anchorsBySelect #1102

Closed
@ivan-palatov

Description

@ivan-palatov

Bug description
The Tooltip component accumulates anchors to elements in anchorsBySelect state without ever releasing old, already unused elements. It causes applications to have massive memory leaks and Detached elements in memory, which eventualy leads to the death of the app.

Version of Package
v5.21.5

To Reproduce
Sorry, I dont have time to setup a proper bench for it, but this should suffice.

  1. open codesandbox
  2. focus the window on the right and hold the right arrow for a bit
  3. open dev tools and make a memory snapshot. See lots of detached nodes
  4. repeat steps 2-3 and see the detached nodes increase (not all of those are because of react-tooltip, but many are. Some are from devtools, some from codesandbox dev env)

Expected behavior
Detached nodes should not be increasing. anchorsBySelect should not be updated with the old + new, old values must be filtered first.

Screenshots
The screenshot from our app in production using react-tooltip. If we remore <Tooltip /> from the root of the app, those detached nodes are gone.
image

Additional context
The problem happens because you are not filtering out the old anchorsBySelect state and just add the new elements on top of it here

So those elements are never actualy released from memory and always hang there, causing massive memory leaks.

To fix this either make use of the removedNodes from MutationObserver like so

    const documentObserverCallback: MutationCallback = (mutationList) => {
      const oldAnchors: HTMLElement[] = [];
      const newAnchors: HTMLElement[] = [];
      for (const mutation of mutationList) {
        if (
          mutation.type === "attributes" &&
          mutation.attributeName === "data-tooltip-id"
        ) {
          const newId = (mutation.target as HTMLElement).getAttribute(
            "data-tooltip-id"
          );

          if (newId === id) {
            newAnchors.push(mutation.target as HTMLElement);
          }
        }

        if (mutation.type !== "childList") {
          continue;
        }

        for (const node of Array.from(mutation.removedNodes)) {
          oldAnchors.push(
            node as HTMLElement,
            // eslint-disable-next-line unicorn/prefer-spread
            ...(Array.from(
              (node as HTMLElement).querySelectorAll(selector)
            ) as HTMLElement[])
          );

          if (!activeAnchor || !node?.contains?.(activeAnchor)) {
            continue;
          }

          setRendered(false);
          handleShow(false);
          setActiveAnchor(null);

          if (tooltipShowDelayTimerRef.current) {
            clearTimeout(tooltipShowDelayTimerRef.current);
          }

          if (tooltipHideDelayTimerRef.current) {
            clearTimeout(tooltipHideDelayTimerRef.current);
          }
        }

        if (!selector) {
          continue;
        }

        const elements = Array.from(mutation.addedNodes).filter(
          (node) => node.nodeType === 1
        );
        newAnchors.push(
          // the element itself is an anchor
          ...(elements.filter((element) =>
            (element as HTMLElement).matches(selector)
          ) as HTMLElement[]),
          // the element has children which are anchors
          ...elements.flatMap(
            (element) =>
              // eslint-disable-next-line unicorn/prefer-spread
              Array.from(
                (element as HTMLElement).querySelectorAll(selector)
              ) as HTMLElement[]
          )
        );
      }

      if (newAnchors.length > 0 || oldAnchors.length > 0) {
        setAnchorsBySelect((anchors) => [
          ...anchors.filter((anchor) => !oldAnchors.includes(anchor)),
          ...newAnchors,
        ]);
      }
    };

Or just do a document.querySelectorAll(selector) on mutation, which, tbh, could be faster than those iterations, transforms and array merges.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions