0

At our MVC project we have this specific View (HTML/JS) where the user can manipulate a large table: select a line and make some operations including move up/down. With move up/down we are facing memory leak: page starts with ~100 MB and goes up until browser crashes (~8 GB at localhost).

Table data is generated at backend, being sent via JSON to frontend. Each line is treated via JS as below:

function LoadTableClient(selectedLine) {
    $('#table-client tbody').find('tr').remove(); //Added for clarification
    var listClientInformationJson= $('#generalData').data('listclientinformation');
    var html = '';
    $.each(JSON.parse(listClientInformationJson), function (index, item) {
        html += WriteTableClient(item, index, selectedLine);
        });
    $('#table-client').find('tbody').append(html);
}

When user move up/down it can affect several rows since each line can also be a group containing several rows inside (and other subgroups and so on). Also, we have subtotal and total columns, therefore we need to redraw the hole table.

For this move up/down operation we are triggering a JS function that requests backend via AJAX, and then it gather the response to redraw our table.

$.ajax({
    url: '...../MoveTableClientItem',
    type: "POST",
    contentType: "application/json",
    data: JSON.stringify({
        ...
    }),
    success: function (response) {
        if (response.success) {
                GetProjectInfo(); //gets JSON
                LoadTableClient(var1); //read JSON and append to table
        }

Update: GetProjectInfo function

function GetProjectInfo() {
    $.ajax({
        url: '..../GetProjectInfo',
        type: "POST",
        contentType: "application/json",
        async: false,
        data: JSON.stringify({
            ...
        }),
        success: function (response) {
            $('#generalData').data('listclientinformation', '');
            $('#generalData').data('listclientinformation', response.listClientInformationJson);
        },
        error: function (response) {
            ...
        }
    });
}

It works fine regarding the visual and functional output, the problem is the memory leak. With Chrome "Memory Inspector" we understood that each operation adds ~30 MB to memory due to rows kept in memory.

Update: see attached print showing JS memory increase. JS memory consumption

We tried Location.reload() but it was not a good user experience since the hole page is reloaded and what the user was doing is lost (open modals, screen position, etc.)

We tried $("#table-client").empty(); at "response.success" but it just didn't work (nothing changed).

Any ideas on how to solve this?

rd1218
  • 134
  • 12
  • Are you only ever appending to the dom? – windowsill Aug 19 '22 at 17:36
  • `.append()` should be `.html()` if you want to rewrite the table. – Barmar Aug 19 '22 at 17:36
  • FYI, `.data()` will automatically parse the field as JSON, you shouldn't need to call `JSON.parse()`. – Barmar Aug 19 '22 at 17:37
  • @Barmar with ```.html()``` the result seems to be the same. But without ```JSON.parse()``` it did not work – rd1218 Aug 19 '22 at 17:46
  • @windowsill Sorry, can you please clarify? In my opinion all information is going to the DOM, where some parts should/could be trashed at every move operation (obsolete parts). – rd1218 Aug 19 '22 at 17:48
  • If you continuously append to anything it will grow without bound right – windowsill Aug 19 '22 at 18:04
  • Is listclientinformation being reset every time or is it being appended to? – windowsill Aug 19 '22 at 18:06
  • It looks like you want to show only the visible rows in your table. That is, you show only (say) 20 rows of a much bigger table on the server. For this use `.html(html)` instead of `.append(html)`. With this you'd need to also manage the scrollbar to show the relative position within the big table. You'd also need to manage the auto-scroll on drag move or copy. But why reinvent the wheel? Use Handsontable or AG Grid that handle very big tables with good performance. – Peter Thoeny Aug 19 '22 at 18:26
  • I don't see how the results can be the same with .append and .html, unless there's some other code that empties the table before calling `LoadTableClient()`. – Barmar Aug 19 '22 at 18:43
  • @windowsill Ok, I've checked that ```data('listclientinformation')``` is being reset. At previous line it is set to ```data('listclientinformation', '')``` therefore making it empty. Another aspect is that the memory leak is steady each time it occurs, if it had any previous data that increase would differ (30 MB, 60 MB, 90 MB...) – rd1218 Aug 19 '22 at 18:50
  • @Barmar Yes, you are right. There is a line doing that. I've added it to the original post. The added line is: ```$('#table-client tbody').find('tr').remove()``` – rd1218 Aug 19 '22 at 18:55
  • @PeterThoeny Thank you but in this case we have to show all rows. – rd1218 Aug 19 '22 at 18:56
  • I've added a print to show this memory increase – rd1218 Aug 19 '22 at 19:06
  • You have to show GetProjectInfo please – windowsill Aug 19 '22 at 19:38
  • @rd1218: All 10k (?) rows in your viewport? Must be a tall monitor. My point was to load & show only the visible rows at any time, and still be able to show all rows by scrolling. – Peter Thoeny Aug 19 '22 at 19:41
  • @windowsill Sure, see information added – rd1218 Aug 19 '22 at 19:45
  • @PeterThoeny Yes, you do have a valid point, but our customers want to see that way, all together. For example, they are used to search (CTRL+F) using browser tool. – rd1218 Aug 19 '22 at 19:47
  • https://stackoverflow.com/a/19205450/5708566 might help – windowsill Aug 19 '22 at 20:00
  • @windowsill Thanks bu I've tested with ```$('#generalData').removeAttr('listclientinformation');``` and nothing changed – rd1218 Aug 19 '22 at 20:13

1 Answers1

0

The issue was related to saving the AJAX response at an element thus making GC (garbage collector) not effective. I managed it by creating a global variable that contains the AJAX response, see below. Now the RAM usage is decreased by GC.

let listClientInformationJson = ''; //Global variable

$(document).ready(function () {
...
//Gets new data with the function below
...
}

function GetProjectInfo() {
    $.ajax({
        url: '..../GetProjectInfo',
        type: "POST",
        contentType: "application/json",
        data: JSON.stringify({
            ...
        }),
        success: function (response) {
            listClientInformationJson = undefined; //forces erase
            listClientInformationJson = JSON.parse(response.listClientInformationJson);
        },
        error: function (response) {
            ...
        }
    });
}
rd1218
  • 134
  • 12