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

使用node-fetch与jsdom时出现内存泄漏问题排查

内存泄漏问题排查与修复方案

核心问题定位

你遇到的内存泄漏主要来自三个方面:

  • JSDOM实例未主动销毁,Window对象被长期持有
  • Promise未正确resolve,导致回调链引用无法释放
  • 每次运行创建新的HTTP/HTTPS Agent,keepAlive连接未清理

具体修复步骤

1. 显式销毁JSDOM实例

jsdom创建的Window对象不会自动被垃圾回收,必须主动调用close()方法释放资源。在Promise.all完成后,立即销毁实例并解除引用:

Promise.all(atagpromises)
.then(data=>{
  var serializedDom = dom.serialize();
  // 处理serializedDom的逻辑
  // 销毁JSDOM实例
  dom.window.close();
  // 解除引用,帮助垃圾回收
  dom = null;
})
.catch(err=>{
  console.log("Error in atagpromises: "+err);
  // 出错时也要销毁实例
  if (dom) {
    dom.window.close();
    dom = null;
  }
});

2. 修复Promise的resolve逻辑

当前fetch的then块里没有调用resolve,导致Promise一直处于pending状态,持有alist、dom等变量的引用。必须在成功和失败分支都正确resolve:

fetch(oldURL, {
  // ... 配置
})
.then(response => {
  var newURL = response.url;
  console.log("new link "+newURL);
  alist.href = newURL ;
  // 必须resolve,否则Promise永远pending
  resolve(newURL);
})
.catch(err=>{
  console.log("error");
  console.log(err);
  resolve(oldURL);
});

3. 优化Agent的创建与清理

每次调用函数时创建新的Agent,且开启了keepAlive,会导致连接池无法释放。可以将Agent改为模块级单例(如果长期运行),或者在每次运行结束后销毁:

// 模块级单例Agent,全局复用
const httpAgent = new http.Agent({keepAlive: true});
const httpsAgent =  new https.Agent({ca: rootCas});

// 或者在函数结束后销毁Agent(如果不需要复用)
Promise.all(atagpromises)
.then(data=>{
  // ... 处理逻辑
  httpAgent.destroy();
  httpsAgent.destroy();
})
.catch(err=>{
  // ... 错误处理
  httpAgent.destroy();
  httpsAgent.destroy();
});

4. 修复循环变量作用域问题

循环中使用var alist会导致闭包共享同一个变量引用,改用let确保每次循环的变量独立:

for (let alist of dom.window.document.querySelectorAll("a")){
  // ... 循环逻辑
}

5. 改用Promise式文件读取

回调式fs.readFile会创建嵌套闭包,增加引用泄漏风险,改用fs.promises.readFile让代码更简洁,也有助于垃圾回收:

export const emailstripperSO = async function(){
  try {
    const storeBody = await fs.promises.readFile(path.join(__dirname, '/config/Homepage.html'), 'utf8');
    let dom = new JSDOM(storeBody);
    // ... 后续逻辑
  } catch (err) {
    console.log("读取文件错误: ", err);
  }
};

修改后的完整代码示例

import {JSDOM} from 'jsdom';
import fetch from 'node-fetch';
import UserAgent from 'user-agents';
import path from 'path';
import sslrootcas from 'ssl-root-cas';
import {fileURLToPath} from 'url';
import http from 'node:http';
import https from 'node:https';
import fs from 'fs/promises'; // 改用promises版本

const rootCas = sslrootcas.create();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
rootCas.addFile(path.resolve(__dirname,'intermediate.pem'));

// 模块级单例Agent,全局复用(如果长期运行)
const httpAgent = new http.Agent({keepAlive: true});
const httpsAgent = new https.Agent({ca: rootCas});

export const emailstripperSO = async function(){
  try {
    const storeBody = await fs.readFile(path.join(__dirname, '/config/Homepage.html'), 'utf8');
    let dom = new JSDOM(storeBody);
    let atagpromises = [];

    for (let alist of dom.window.document.querySelectorAll("a")){
      let atagpromise = new Promise ((resolve,reject)=>{
        let oldURL = alist.href;
        if (oldURL.split(":")[0] && ["http", "https", "mailto"].includes(oldURL.split(":")[0])){
          if (oldURL.substr(0,4)!=="http"){
            return resolve(oldURL);
          }

          fetch(oldURL, {
            method: "GET",
            headers: {"User-Agent": new UserAgent()},
            redirect: "manual",
            agent: function(_parsedURL) {
              if (_parsedURL.protocol === 'http:') return httpAgent;
              if (_parsedURL.protocol === 'https:') return httpsAgent;
            }
          })
          .then(response => {
            var newURL = response.url;
            console.log("new link "+newURL);
            alist.href = newURL ;
            resolve(newURL); // 必须resolve
          })
          .catch(err=>{
            console.log("error");
            console.log(err);
            resolve(oldURL);
          });
        } else {
          console.log("Skipped visiting <"+oldURL+">");
          resolve(oldURL);
        }
      });         
      atagpromises.push(atagpromise);
    };

    await Promise.all(atagpromises);
    const serializedDom = dom.serialize();
    // do more stuff with serializedDom here

    // 销毁JSDOM实例
    dom.window.close();
    dom = null;
  } catch (err) {
    console.log("Error in process: "+err);
    // 出错时确保销毁dom(如果存在)
    if (dom) {
      dom.window.close();
      dom = null;
    }
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 12:25:14