【问题标题】:ChartJs Memory Leak | Garbage Collection does not clean Chart Object or Arrays after renderChartJs 内存泄漏 |垃圾收集在渲染后不清理图表对象或数组
【发布时间】:2023-12-16 16:14:01
【问题描述】:

我正在做一个项目,我必须使用 chartJs 创建大量图表。我注意到当图表数量上升到 1000 时,我的页面不断崩溃(取决于机器的内存)。

在使用 chrome devtools 进一步探索该问题后,似乎一旦创建图表,它们就不会被垃圾收集。每个图表对象和对象创建的数组(标签数组,其中数据数组的数据集数组)都保留在内存中,并且它们保留在内存中并且不会被垃圾收集。因为当页面中的图表数量达到其峰值时,它们不会被垃圾收集,因为页面达到内存限制后页面崩溃。

如何修复这种内存泄漏或触发垃圾回收?

编辑:请注意我不想删除生成的图表。

JSFiddle 以获得更清晰的视图

图表生成脚本如下

var COLORS = ["#265b62", "#FF6361", "#FFA600", "#66C2A4", "#EF3B2C",
        "#67000D", "#349866", "#A05195", "#78C679", "#2B8CBE",
        "#8C96C6", "#373F52", "#810F7C", "#BC5090", "#FF3D67",
        "#D5B029", "#CD9B9A", "#EC7014", "#665191", "#003F5C"];

    function getLabels(numberOfLabel) {
        var labels = [];

        for (var i=1; i<= numberOfLabel; i++) {
            labels.push('Category '+i);
        }

        return labels;
    }

    function getDataSeries(labels, numberOfSeries) {

        var dataSeries = [];
        for (var i=1; i<= numberOfSeries; i++) {
            dataSeries.push(getDataObj(i, labels.length));
        }

        return dataSeries;
    }

    function getDataObj(count, numberOfLabel) {
        var seriesLabel = 'Dataset ' + count;
        var colorName = COLORS[count % COLORS.length];

        dataArray = [];

        for (var i=1; i<= numberOfLabel; i++) {
            dataArray.push(randomScalingFactor());
        }

        var dataObj = {
            label: seriesLabel,
            backgroundColor: colorName,
            borderColor: colorName,
            data: dataArray,
            fill: false,
        };

        return dataObj;
    }

    function getConfig(chartCount, dataPoints, barOrLineCounts) {
        var chartLabels = getLabels(dataPoints);
        var dataSeries = getDataSeries(chartLabels, barOrLineCounts);

        var chartTitle = 'Chart.js Line Chart Count ' + chartCount;
        var chartType = 'line';
        var config = {
            type: chartType,
            data: {
                labels: chartLabels,
                datasets: dataSeries
            },
            options: {
                // ===============performance tweaks==========================================
                parsing: false,
                spanGaps: true,
                elements: {
                    line: {
                        tension: 0, // disables bezier curves for line datasets
                        fill: false,
                        stepped: false,
                        borderDash: []
                    },
                    point: {
                        radius: 2 // default = 3, set 0 to disable and gain performance
                    }
                },
                animation: false,
                // ===============performance tweaks==========================================
                responsive: true,
                title: {
                    display: true,
                    text: chartTitle,
                },
                tooltips: {
                    mode: 'index',
                    intersect: false,
                },
                hover: {
                    mode: 'nearest',
                    intersect: true
                },
                scales: {
                    xAxes: [{
                        display: true,
                        ticks: {
                            autoSkip: false,
                            maxRotation: 45,
                            minRotation: 45,
                            sampleSize: 35
                        },
                        stacked: chartType === 'bar',
                        scaleLabel: {
                            display: true,
                            labelString: 'Month'
                        }
                    }],
                    yAxes: [{
                        display: true,
                        ticks: {
                            // setting min and max will gain performance as chartjs wont have to compute this.
                            min: 0,
                            max: 100
                        },
                        scaleLabel: {
                            display: true,
                            labelString: 'Value'
                        }
                    }]
                }
            }
        };

        return config;
    }


    function generateCharts(chartCount, dataPoints, barOrLineCounts) {
        for (var i=1; i<= chartCount; i++) {
            var ctx = document.getElementById('canvas'+i).getContext('2d');

            var myLine = new Chart(ctx, getConfig(i, dataPoints, barOrLineCounts));
        }
    }

    window.onload = function() {

                var chartCount = 1;
        var dataPoints= 20;
        var barOrLineCounts = 10;


        $('#chartType').val("${chartType}");

        generateCharts(chartCount, dataPoints, barOrLineCounts);
    };

    var xCount =  2;
    function addChart() {
        var chartCount = 1;
        var dataPoints= 20;
        var barOrLineCounts = 10;

        var ctx = document.getElementById('canvas'+xCount).getContext('2d');
        var myLine = new Chart(ctx, getConfig(xCount, dataPoints, barOrLineCounts));

        xCount = xCount + 1;
    }

    function randomScalingFactor() {
        return Math.random() * 100;
    }

测试用例的示例 HTML。

<div class="panel-body">
   <button id="addChart" onclick="addChart()">Add Chart</button><br><br>
   <div id="container">
      <div id="chart1" class="col-sm-12">
         <canvas id="canvas1">
         </canvas>
      </div>
      <div id="chart2" class="col-sm-12">
         <canvas id="canvas2">
         </canvas>
      </div>
      <div id="chart3" class="col-sm-12">
         <canvas id="canvas3">
         </canvas>
      </div>
      <div id="chart4" class="col-sm-12">
         <canvas id="canvas4">
         </canvas>
      </div>
      <div id="chart5" class="col-sm-12">
         <canvas id="canvas4">
         </canvas>
      </div>
...
...
...
      <div id="chart17" class="col-sm-12">
         <canvas id="canvas17">
         </canvas>
      </div>
      <div id="chart18" class="col-sm-12">
         <canvas id="canvas18">
         </canvas>
      </div>
      <div id="chart19" class="col-sm-12">
         <canvas id="canvas19">
         </canvas>
      </div>
      <div id="chart20" class="col-sm-12">
         <canvas id="canvas20">
         </canvas>
      </div>
   </div>
</div>

【问题讨论】:

    标签: javascript memory-management memory-leaks chart.js


    【解决方案1】:

    为了让您的图表被垃圾收集,您必须确保没有对它们的剩余引用。

    使用chartInstance.destroy()清理Chart.js对图表对象的内部引用:documentation

    当您调用new Chart(...) 时,您必须将该引用存储在某处(例如,在一个数组中)并在完成后调用.destroy()。不要忘记从数组中删除引用。

    【讨论】:

    • 调用 .destroy() 会从页面/视图中删除图表,这是我不想要的。我想保留我生成的图表。一旦在画布中绘制图表,我希望图表对象和与对象关联的数组被垃圾收集。
    • 可以保存画布内容,销毁图表,恢复画布。不过,这将消除交互性。那样可以么?如果您对图像没问题,则使用 quickchart.io(我的一个开源项目)之类的渲染服务可能会更简单
    • 你能建议我如何按照你的建议去做吗?我可以使用 chartJs 的 .toBase64Image() 将其作为 img ,但这对内存问题没有多大帮助。
    • 调用destroy会删除图像,所以如果你想保留非交互式图表图像,你可以用toBase64Image保存它。如果您连显示图表图像的内存都买不起,那么您应该使用第三方渲染服务,例如 QuickChart。
    • 图表对象及其关联的数据和事件监听器等必须保留在内存中以进行交互。