用于查找 JavaScript 内存泄漏的实用函数
2 分•作者: EGreg•6 个月前
我曾经很难理解内存泄漏是从哪里来的,尤其是在 iOS Safari 上。我进入开发者工具 > 时间线标签,看到内存一直在增加,但不知道怎么增加的,也不知道在哪里。所以我写了这个函数来遍历各种软件添加的所有全局对象,避免重复访问同一个对象。这个函数是异步的,这样可以避免占用太多的用户体验。你可以运行它来开始查看引用在哪里泄漏了。
```javascript
Q = {};
Q.globalNames = Object.keys(window); // 快照基线
Q.globalNamesAdded = function () {
const current = Object.keys(window);
const baseline = Q.globalNames;
const added = [];
for (let i = 0; i < current.length; i++) {
if (!baseline.includes(current[i])) {
added.push(current[i]);
}
}
return added;
};
Q.walkGlobalsAsync = function (filterFn, options = {}) {
const seen = new WeakSet();
const found = new Set();
const pathMap = new WeakMap();
const maxDepth = options.maxDepth || 5;
const includeStack = options.includeStack || false;
const logEvery = options.logEvery || 100;
const startingKeys = Q.globalNamesAdded
? Q.globalNamesAdded()
: Object.keys(window);
let totalChecked = 0;
let matchesFound = 0;
function walk(obj, path = 'window', depth = 0) {
if (!obj || typeof obj !== 'object') return;
if (seen.has(obj)) return;
seen.add(obj);
totalChecked++;
if (totalChecked % logEvery === 0) {
console.log(`已检查 ${totalChecked} 个对象,找到 ${matchesFound}`);
}
if (filterFn(obj)) {
found.add(obj);
matchesFound++;
if (includeStack) {
pathMap.set(obj, path);
console.log(`[找到] ${path}`, obj);
} else {
console.log(`[找到]`, obj);
}
}
if (depth >= maxDepth) return;
const skipKeys = obj instanceof HTMLElement
? new Set([
'parentNode', 'parentElement', 'nextSibling', 'previousSibling',
'firstChild', 'lastChild', 'children', 'childNodes',
'ownerDocument', 'style', 'classList', 'dataset',
'attributes', 'innerHTML', 'outerHTML',
'nextElementSibling', 'previousElementSibling'
])
: null;
for (const key in obj) {
if (skipKeys && skipKeys.has(key)) continue;
try {
walk(obj[key], path + '.' + key, depth + 1);
} catch (e) {}
}
}
let i = 0;
function nextBatch() {
const batchSize = 10;
const end = Math.min(i + batchSize, startingKeys.length);
for (; i < end; i++) {
try {
walk(window[startingKeys[i]], 'window.' + startingKeys[i], 0);
} catch (e) {}
}
if (i < startingKeys.length) {
setTimeout(nextBatch, 0); // 安排下一个批次
} else {
console.log(`完成。找到 ${matchesFound} 个被保留的对象。`);
if (includeStack) {
console.log([...found].map(obj => ({
object: obj,
path: pathMap.get(obj)
})));
} else {
console.log([...found]);
}
}
}
nextBatch();
};
```
下面是如何使用它:
```javascript
Q.walkGlobalsAsync(
obj => obj instanceof HTMLElement && !document.contains(obj),
{ includeStack: true, maxDepth: 4, logEvery: 50 }
);
```
但是——请注意,即使你能找到闭包本身,这个方法也**不会**找到被闭包保留的对象,你仍然需要手动检查它们的代码。
查看原文
I had a tough time understanding where memory leaks are coming from, especially on iOS safari. I'd go into Dev Tools > Timelines tab and see the memory go up, but not sure how or where. So I wrote this function to traverse all the global objects that have been added by various software, avoiding revisiting the same objects more than once. The function is async so as not to tie up the UX too much. You can run it to start seeing where the references are being leaked.<p><pre><code> Q = {};
Q.globalNames = Object.keys(window); // snapshot baseline
Q.globalNamesAdded = function () {
const current = Object.keys(window);
const baseline = Q.globalNames;
const added = [];
for (let i = 0; i < current.length; i++) {
if (!baseline.includes(current[i])) {
added.push(current[i]);
}
}
return added;
};
Q.walkGlobalsAsync = function (filterFn, options = {}) {
const seen = new WeakSet();
const found = new Set();
const pathMap = new WeakMap();
const maxDepth = options.maxDepth || 5;
const includeStack = options.includeStack || false;
const logEvery = options.logEvery || 100;
const startingKeys = Q.globalNamesAdded
? Q.globalNamesAdded()
: Object.keys(window);
let totalChecked = 0;
let matchesFound = 0;
function walk(obj, path = 'window', depth = 0) {
if (!obj || typeof obj !== 'object') return;
if (seen.has(obj)) return;
seen.add(obj);
totalChecked++;
if (totalChecked % logEvery === 0) {
console.log(`Checked ${totalChecked} objects, found ${matchesFound}`);
}
if (filterFn(obj)) {
found.add(obj);
matchesFound++;
if (includeStack) {
pathMap.set(obj, path);
console.log(`[FOUND] ${path}`, obj);
} else {
console.log(`[FOUND]`, obj);
}
}
if (depth >= maxDepth) return;
const skipKeys = obj instanceof HTMLElement
? new Set([
'parentNode', 'parentElement', 'nextSibling', 'previousSibling',
'firstChild', 'lastChild', 'children', 'childNodes',
'ownerDocument', 'style', 'classList', 'dataset',
'attributes', 'innerHTML', 'outerHTML',
'nextElementSibling', 'previousElementSibling'
])
: null;
for (const key in obj) {
if (skipKeys && skipKeys.has(key)) continue;
try {
walk(obj[key], path + '.' + key, depth + 1);
} catch (e) {}
}
}
let i = 0;
function nextBatch() {
const batchSize = 10;
const end = Math.min(i + batchSize, startingKeys.length);
for (; i < end; i++) {
try {
walk(window[startingKeys[i]], 'window.' + startingKeys[i], 0);
} catch (e) {}
}
if (i < startingKeys.length) {
setTimeout(nextBatch, 0); // Schedule next batch
} else {
console.log(`Done. Found ${matchesFound} retained objects.`);
if (includeStack) {
console.log([...found].map(obj => ({
object: obj,
path: pathMap.get(obj)
})));
} else {
console.log([...found]);
}
}
}
nextBatch();
};
</code></pre>
Here is how you use it:<p><pre><code> Q.walkGlobalsAsync(
obj => obj instanceof HTMLElement && !document.contains(obj),
{ includeStack: true, maxDepth: 4, logEvery: 50 }
);
</code></pre>
However -- note that this will NOT find objects retained by closures, even if you can find the closures themselves you're going to have to check their code manually.