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

setTimeout回调无法被GC回收引发内存泄漏的解决方案

问题根因

你观测到的setTimeout回调无法被GC、堆内存持续上涨,是三个逻辑缺陷共同导致的:

  • 未在组件卸载阶段清理定时器:只要setTimeout没有被clearTimeout主动清除,事件循环就会一直持有回调函数的引用,回调闭包中捕获的整个组件实例、props、state都无法被GC回收,只要组件发生一次重挂载,旧实例的内存就会永久泄漏。
  • 轮询节奏不依赖请求完成状态:当前逻辑是到固定间隔就发起新请求,完全不管上一次请求是否结束,弱网环境下会出现大量pending状态的请求堆积,每个未完成的请求都会持有对应的response、闭包引用,进一步推高内存。
  • 未处理组件卸载后的请求回调:组件卸载后返回的fetch请求依然会执行dispatch逻辑,无意义持有Redux store和组件相关引用。
修复方案

1. 完善组件生命周期清理逻辑

在组件中增加挂载标记、请求控制器,卸载时统一清理资源:

componentDidMount() {
  // 标记组件挂载状态
  this._isMounted = true;
  // 初始化请求终止控制器
  this.abortController = new AbortController();
  // 发起首次轮询
  this.fetchDisplayContent(1000);
}

componentWillUnmount() {
  // 标记组件已卸载
  this._isMounted = false;
  // 清除未执行的定时器
  clearTimeout(this._contentInterval);
  // 终止所有未完成的请求
  this.abortController.abort();
}

2. 改造Redux请求逻辑,支持主动终止

给fetch请求增加signal参数支持,忽略主动终止请求触发的错误:

export function fetchContent(url, lastUpdated, webkey, signal) {
return (dispatch) => {
    let now = new Date();
    let rev = now.getTime()/1000;
    url += url.includes('?') ? "&rev="+rev : "?rev="+rev;
    if (webkey) url += "&webkey="+webkey;

    fetch(url, { signal })
        .then((response) => {
            if (!response.ok) throw Error(response.statusText);
            return response.json();
        })
        .then((content) => {
            if (Number(content.last_update) > Number(lastUpdated)){
                dispatch(contentFetchSuccess(content))    
            }
        }).catch((err) => {
            // 主动终止的请求不需要抛错
            if (err.name !== 'AbortError') {
                dispatch(contentHasErrored(true));   
            }
        });
};
}

注意:fetchModuleContent按照相同逻辑改造,补充signal参数接入AbortController即可。

3. 重构轮询逻辑,避免请求堆积

去掉原有的repeat参数,等上一次请求完成(无论成功/失败)后再安排下一次定时器,从根源避免请求堆积:

fetchDisplayContent = (fetchInterval = 25000) => {
    clearTimeout(this._contentInterval);
    this._contentInterval = setTimeout(async () => {
        // 组件已卸载直接终止逻辑
        if (!this._isMounted) return;

        const requestParams = [];
        let requestAction = this.props.fetchContent;
        // 按当前模式拼接请求参数
        if (this._mode === "preview") {
            const url = config.WEB_SERVER + '/display/fetchDisplayContents?mode=prvw&localLastUpdate=0&display=' + this._displayId;
            requestParams.push(url, 0, this._webkey, this.abortController.signal);
        }
        else if (this._mode === "web") {
            const url = config.WEB_SERVER + '/display/fetchDisplayContents?mode=web&localLastUpdate=0&display=' + this._displayId;
            requestParams.push(url, 0, this._webkey, this.abortController.signal);
        }
        else if (this._mode === "module") {
            const url = config.WEB_SERVER + '/display/fetchModuleContents?mode=prvw&module=' + this._module + '&project=' + this._project + '&module_id=' + this._module_id;
            requestAction = this.props.fetchModuleContent;
            requestParams.push(url, this._webkey, this.abortController.signal);
        }
        else {
            const url = typeof config.DISPLAY === "number" 
                ? this._baseURL + '/data/displayContents.json'
                : this._baseURL + '/data/display_' + this._displayId + '_contents/displayContents.json';
            requestParams.push(url, 0, this._mode, this.abortController.signal);
        }

        try {
            // 等待当前请求完成再安排下一次轮询
            await requestAction(...requestParams);
        } finally {
            // 组件仍处于挂载状态才继续轮询
            if (this._isMounted) {
                this.fetchDisplayContent();
            }
        }
    }, fetchInterval);
}
验证标准

改造完成后用浏览器性能面板录制10分钟以上,期间手动触发几次组件重挂载,满足以下特征即为修复完成:

  • 定时器回调数量不会随运行时间持续累加
  • 触发GC后堆内存可以回落到稳定基线,不会出现单边持续上涨
  • 网络面板不会出现大量堆积的pending状态请求

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 20:24:09