【问题标题】:Memory leak from repeatedly applying d3 transition重复应用 d3 转换导致的内存泄漏
【发布时间】:2026-02-06 04:40:01
【问题描述】:

我有一个 SVG 地图和一个间隔,用于轮询数据更改并相应地更新地图上的颜色。除非我使用过渡淡化到新颜色,否则一切正常。然后标签会慢慢消耗越来越多的内存,直到崩溃。

我做了一个显示相同行为的简化示例:

var size = 500;
var num = 25;
var boxSize = size / num;

function color(d) {
    return '#' + Math.random().toString(16).slice(2,8);
}

var svg = d3.select('body')
    .append("svg")
    .attr("width", size)
    .attr("height", size);

var squares = svg.selectAll(".square")
    .data(d3.range(num * num))
  .enter().append("rect")
    .attr("class", "square")
    .attr("width", boxSize)
    .attr("height", boxSize)
    .attr("x", function (d) { return boxSize * (d % num);})
    .attr("y", function (d) { return boxSize * Math.floor(d / num); })
    .style("fill", color);

function shuffleColors() {
    squares.interrupt().transition().duration(500).style("fill", color);
    timer = setTimeout(shuffleColors, 1000);
}

var timer = setTimeout(shuffleColors, 1000);

https://plnkr.co/edit/p71QmO

我已经在 Linux 上的 Chromium (49) 和 Firefox (45) 中尝试过。前者似乎爆炸得更快,但这对两者都是一个问题。它既没有显示在内存分析器中,但 about:memory 显示选项卡正在增长。

我从文档中的理解是,向选择添加过渡会用相同的名称替换任何先前的过渡(包括空名称),但我的假设是为实现过渡而创建的函数实际上并没有被丢弃.但我还没有设法让他们确认或解决这个问题。

所以,一个两部分的问题:

  1. 这是对 d3 转换的正确使用,还是有更正确的方法来做我想做的事情?
  2. 如果我正确使用了转换,如何让它停止泄漏内存?

编辑:

  1. 根据 Blindman67 的评论,我将其更改为使用 setTimeout 并稍微小一些。我试图模拟的原件更小更慢,但需要数小时才能最终变大,所以我试图加快速度。这个版本似乎仍在增长,至少对 Chromium 上的我来说是这样。
  2. 据我观察,d3_selectionPrototype.transition 每次都会创建一个新的d3_transition,其 ID 会递增,但如果旧的被垃圾收集就可以了。而且我仍然无法指出它是否或为什么会被保留。

【问题讨论】:

  • 我刚刚在您的 shuffle Colors 函数中添加了一个 debugger 关键字,并将其添加到 D3 代码中。它在调用中断上做的第一件事是开始设置一个包含 1600 个中断的列表,它将每个方格作为一个单独的实体处理。我没有更深入,因为问题很明显,您正在大块地咀嚼内存,当 GC 确实开始减速时,会将 setInterval 退出,一旦发生这种情况,您就在等待崩溃。完成后使用 setTimeout 并开始下一次随机播放。不泄漏内存,您正在溢出调用堆栈
  • 我正要对interrupt()说同样的话。
  • 对我来说至少我无法重现,实时 JS 堆内存增长到大约 45MB,然后在 GC 启动时下降到 ~32MB,然后再次增长,无限。查看副本的答案。

标签: javascript d3.js memory-leaks


【解决方案1】:

我相当肯定它必须在这里做这件作品:

function shuffleColors() {
    squares.interrupt().transition().duration(500).style("fill", color);
    timer = setTimeout(shuffleColors, 1000);
}

var timer = setTimeout(shuffleColors, 1000);

每次调用函数shuffleColors() 时,它都会再次调用自身,本质上是创建一个没有基本情况的递归循环。它没有立即崩溃的原因是每次调用函数都会延迟 1000 毫秒,但是,我认为完成 squares.interrupt().transition().duration(500).style("fill", color); 的时间比调用 setTimeout() 的时间要长。因此,即使您每 1000 毫秒调用一次,它也可能会以某种方式堆积起来,因为某些颜色变化可能需要更多时间来处理。

虽然它在技术上不应该这样做,但知道 JavaScript 是如何异步的,它可能会发挥作用。我建议改为这样做并报告结果:

function shuffleColors() {
    squares.interrupt().transition().duration(500).style("fill", color);
}

var timer = setInterval(shuffleColors, 1000);

如果需要,您也可以随时致电clearInterval(timer)setInterval() 的创建正是因为您实现了 setTimeout()

编辑:这可能无法完全工作,因为您可能仍然需要等待颜色更改完成,但是,这至少是一种更清洁的方法。您也许可以实现某种wait() 函数来等待颜色更改完成。

虽然矢量 (SVG) 图像是轻量级的,但与解码 JPEG 图像相比,它需要不断更改颜色或类似内容的处理量是巨大的。

如果您将原始图像尺寸缩小很多,然后将其扩展为您的分辨率,您可能会发现更好的结果。您可以制作一个 100x100 的画布 SVG 并将其扩展为 2000x2000 之类的,这样就不必绘制这么大的图像了。

【讨论】: