You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Visjs timeline的item模板中使用JSX导致内存泄漏问题求解

内存泄漏成因
  • 非法的API组合使用:ReactDOM.render 的返回值为类组件实例/函数组件的null,本身不属于合法的React元素,将其作为参数传入createPortal属于不符合React规范的用法,会导致React内部维护的fiber节点引用和DOM节点引用无法被正常追踪和释放。
  • 缺失组件卸载逻辑:vis.js timeline在更新、移除item节点时,只会直接操作DOM删除对应元素,不会主动触发React组件的卸载流程。每次调用ReactDOM.render往目标元素挂载组件时,旧的组件实例、关联的事件监听、状态引用都还被React内部持有,导致被删除的DOM节点无法被GC回收,最终累积为Detached HTMLDivElements。
  • 双重渲染冗余:ReactDOM.render本身已经会把JSX渲染到目标元素中,再套一层createPortal相当于重复创建了DOM关联,进一步放大了引用泄漏的问题。
规避方案

方案1:手动管控组件卸载(兼容React17及更早版本)

不要混用createPortal和ReactDOM.render,每次渲染新组件前先卸载目标节点上已有的React组件,同时在timeline销毁时做全局清理,直接返回DOM节点可避免移除portal后出现的报错问题:

// 卸载指定节点上的React组件
const unmountReactComponent = (domNode) => {
  if (domNode) ReactDOM.unmountComponentAtNode(domNode);
};

const options = {
  template: (item, element) => {
    // 先清理旧组件,避免残留引用
    unmountReactComponent(element);
    // 直接渲染JSX到目标节点
    ReactDOM.render(<Tag key={item.id}>{item.id}</Tag>, element);
    // 直接返回DOM节点给vis timeline,避免触发innerHTML报错
    return element;
  },
};

// 监听timeline的销毁事件做全局清理
timeline.on('destroy', () => {
  // 遍历所有已渲染的item节点执行卸载逻辑
  timeline.itemsData.get().forEach(item => {
    const element = timeline.itemSet.items[item.id]?.dom;
    unmountReactComponent(element);
  });
});

方案2:用React18 createRoot + 实例缓存

如果使用React18,用官方推荐的createRootAPI代替旧的render,通过WeakMap缓存每个节点对应的root实例,避免重复创建:

import { createRoot } from 'react-dom/client';
// 用WeakMap存root实例,节点被回收时自动释放对应的缓存
const rootCache = new WeakMap();

const options = {
  template: (item, element) => {
    let root = rootCache.get(element);
    if (!root) {
      root = createRoot(element);
      rootCache.set(element, root);
    }
    root.render(<Tag key={item.id}>{item.id}</Tag>);
    return element;
  },
};

// timeline销毁时清理所有缓存的root
timeline.on('destroy', () => {
  timeline.itemsData.get().forEach(item => {
    const element = timeline.itemSet.items[item.id]?.dom;
    rootCache.get(element)?.unmount();
    rootCache.delete(element);
  });
});

方案3:Portal统一托管(无手动卸载更稳定)

如果要使用Portal,不要和render混用,将所有Portal放在你的React组件树内统一渲染,由React自动管控生命周期:

const TimelineWrapper = () => {
  const timelineRef = useRef(null);
  const [portalList, setPortalList] = useState([]);

  useEffect(() => {
    const options = {
      template: (item, element) => {
        // 仅新增不存在的portal,避免重复渲染
        setPortalList(prev => {
          if (prev.some(p => p.itemId === item.id)) return prev;
          return [
            ...prev,
            {
              itemId: item.id,
              element,
              portal: ReactDOM.createPortal(<Tag>{item.id}</Tag>, element)
            }
          ];
        });
        // 监听item移除事件,自动删除对应的portal
        timelineRef.current.on('remove', (event) => {
          if (event.items.includes(item.id)) {
            setPortalList(prev => prev.filter(p => p.itemId !== item.id));
          }
        });
        return element;
      }
    };
    // 初始化timeline逻辑
    timelineRef.current = new vis.Timeline(timelineRef.current, items, groups, options);

    return () => {
      timelineRef.current.destroy();
    };
  }, []);

  return (
    <div>
      <div ref={timelineRef} />
      {/* 所有portal在组件树内渲染,React自动处理卸载 */}
      {portalList.map(p => p.portal)}
    </div>
  );
};

该方案完全符合React生命周期规范,不需要手动处理卸载逻辑,内存泄漏风险最低。

内容的提问来源于stack exchange,提问作者user16706683

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 15:00:05