Skip to content

fix: Show heatmap and image trace PNGs through blob URLs - #8114

Open
JeS24 wants to merge 4 commits into
plotly:mainfrom
JeS24:fix/8097-heatmap-blob-url
Open

JeS24 wants to merge 4 commits into
plotly:mainfrom
JeS24:fix/8097-heatmap-blob-url

Conversation

@JeS24

@JeS24 JeS24 commented Oct 10, 2026

Copy link
Copy Markdown

Description

Fixes the memory growth in Chromium when heatmap or image traces redraw repeatedly.

Closes #8097.

Before this change, canvas-rendered heatmap and image traces assigned a new PNG data URL on every redraw. Chromium keeps the old images in memory until the page unloads. With frequent updates, that adds up quickly.

This PR uses blob URLs for those images and revokes the old URL when an image changes or leaves the plot. Purging the plot releases the remaining URLs.

Changes

The two traces now share an image URL helper in src/lib/image_href.ts. The heatmap change also applies to histogram2d and to contour and histogram2dcontour with contours.coloring: 'heatmap'. The internal GraphDiv type now includes _imageBlobNodes to track images that need cleanup.

Canvas images in plots marked _exportedPlot still use data URLs. Snapshot.toSVG also replaces a live plot's blob URLs with the saved PNG data. That keeps the exported SVG independent of the page that created it.

The conversion to a blob is synchronous. I kept it that way rather than using canvas.toBlob, so the image URL is still set before Plotly.react resolves. The extra cost is one base64 decode per redraw.

The heatmap and image restyle tests now compare PNG data, since a redraw creates a new blob URL even if the image is unchanged. The image tests still use the original URL for images drawn directly from source. Two new heatmap tests cover URL replacement, trace removal, purge, and SVG export through Plotly.toImage and Plotly.Snapshot.toSVG.

Testing

To reproduce, run npm start on main, open Chrome's task manager (Shift+Esc), and paste this into the console:

const gd = Tabs.fresh();
const N = 1000;
let frame = 0;
function nextZ() {
    frame++;
    const z = [];
    for (let r = 0; r < N; r++) {
        const row = new Array(N);
        for (let c = 0; c < N; c++) row[c] = r <= frame % N ? Math.sin(r / 30 + c / 50 + frame / 10) : NaN;
        z.push(row);
    }
    return z;
}
(async function update() {
    await Plotly.react(gd, [{ type: 'heatmap', z: nextZ() }]);
    setTimeout(update, 250);
})();

On main, the tab's memory footprint grows by about 100 MB per minute and doesn't come back down. Repeat on this branch and it levels off after a few minutes.

For the image trace, use { type: 'image', z } with an array of [r, g, b] pixels.

Here are the renderer private memory measurements from headless Chromium, with a forced GC before each sample:

Trace Build 0 min 10 min 30 min
heatmap, 1000×1000 main 245 MB 1191 MB —
heatmap, 1000×1000 this branch 248 MB 505 MB —
image, 400×400 RGB main 70 MB 491 MB 938 MB
image, 400×400 RGB this branch 68 MB 301 MB 331 MB

The heatmap tests ran for 12 minutes and the image tests for 30 minutes. In the image runs, the memory-infra trace showed growth in partition_alloc on main, about 0.28 MB per redraw. The JS heap stayed between 24 and 45 MB on both builds, and the screenshots were identical.

Notes

There are a few cases this doesn't cover:

  • Plotly.Snapshot.toImage removes its cloned plot without purging it. For canvas-rendered traces, that leaves the clone's image blob URLs allocated until the page unloads.
  • An image trace with a different data URL in source on every redraw still grows memory. This PR leaves user-supplied URLs alone.
  • I haven't tested memory growth from repeated Plotly.toImage calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Memory Leak - Heatmap redraws continuously grow Chromium renderer memory via unique PNG data: URLs

1 participant