使用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
相关产品推荐
相关产品推荐

