最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Javascript performance optimization - Stack Overflow

programmeradmin5浏览0评论

I created the following js function

function csvDecode(csvRecordsList)
{
    var cel;
    var chk;
    var chkACB;
    var chkAF;
    var chkAMR;
    var chkAN;
    var csvField;
    var csvFieldLen;
    var csvFieldsList;
    var csvRow;
    var csvRowLen = csvRecordsList.length;
    var frag = document.createDocumentFragment();
    var injectFragInTbody = function () {tblbody.replaceChild(frag, tblbody.firstElementChild);};
    var isFirstRec;
    var len;
    var newEmbtyRow;
    var objCells;
    var parReEx = new RegExp(myCsvParag, 'ig');
    var tblbody;
    var tblCount = 0;
    var tgtTblBodyID;

    for (csvRow = 0; csvRow < csvRowLen; csvRow++)
    {
        if (csvRecordsList[csvRow].startsWith(myTBodySep))
        {
            if (frag.childElementCount > 0)
            {
                injectFragInTbody();
            }
            tgtTblBodyID = csvRecordsList[csvRow].split(myTBodySep)[1];
            newEmbtyRow = getNewEmptyRow(tgtTblBodyID);
            objCells = newEmbtyRow.cells;
            len = newEmbtyRow.querySelectorAll('input')[0].parentNode.cellIndex; // Finds the cell index where is placed the first input (Check-box or button)

            tblbody = getElById(tgtTblBodyID);
            chkAF = toBool(tblbody.dataset.acceptfiles);
            chkACB = toBool(tblbody.dataset.acceptcheckboxes) ;
            chkAN = toBool(tblbody.dataset.acceptmultiplerows) ;
            tblCount++;
            continue;
        }

        csvRecordsList[csvRow] = csvRecordsList[csvRow].replace(parReEx, myInnerHTMLParag); // Replaces all the paragraph symbols ¶ used into the db.csv file with the tag <br> needed into the HTML content of table cells, this way will be possible to use line breaks into table cells
        csvFieldsList = csvRecordsList[csvRow].split(myEndOfFld);

        csvFieldLen = csvFieldsList.length;
        for (csvField = 0; csvField < csvFieldLen; csvField++)
        {
            cel = chkAN ? csvField + 1 : csvField;
            if (chkAF && cel === 1) {objCells[cel].innerHTML =  makeFileLink(csvFieldsList[csvField]);} 
            else if (chkACB && cel === len) {objCells[cel].firstChild.checked = toBool(csvFieldsList[csvField]);}
            else {objCells[cel].innerHTML = csvFieldsList[csvField];}
        }
        frag.appendChild(newEmbtyRow.cloneNode(true));
    }
    injectFragInTbody();

    var recNum = getElById(tgtTblBodyID).childElementCount;
    customizeHtmlTitle();
    return csvRow - tblCount + ' (di cui '+ recNum + ' record di documenti)';
}

More than 90% of records could contain file names that have to be processed by the following makeFileLink function:

function makeFileLink(fname)
{
    return ['<a href="', dirDocSan, fname, '" target="', previewWinName, '" title="Apri il file allegato: ', fname, '" >', fname, '</a>'].join('');
}

It aims to decode a record list from a special type of *.db.csv file (= a ma-separated values where mas are replaced by another symbol I hard-coded into the var myEndOfFld). (This special type of *.db.csv is created by another function I wrote and it is just a "text" file).

The record list to decode and append to HTML tables is passed to the function with its lone parameter: (csvRecordsList).

Into the csv file is hosted data ing from more HTML tables.

Tables are different for number of rows and columns and for some other contained data type (which could be filenames, numbers, string, dates, checkbox values).

Some tables could be just 1 row, others accept more rows.

A row of data has the following basic structure:

data field content 1|data field content 2|data field content 3|etc...

Once decoded by my algorithm it will be rendered correctly into the HTML td element even if into a field there are more paragraphs. In fact the tag
will be added where is needed by the code:

csvRecordsList[csvRow].replace(par, myInnerHTMLParag)

that replaces all the char I choose to represent the paragraph symbol I have hard-coded into the variable myCsvParag.

Isn't possible to know at programming time the number of records to load in each table nor the number of records loaded from the CSV file, nor the number of fields of each record or what table field is going to contain data or will be empty: in the same record some fields could contain data others could be empty. Everything has to be discovered at runtime.

Into the special csv file each table is separated from the next by a row witch contains just a string with the following pattern: myTBodySep = tablebodyid where myTBodySep = "targettbodydatatable" that is just a hard coded string of my choice. tablebodyid is just a placeholder that contains a string representing the id of the target table tbody element to insert new record in, for example: tBodyDataCars, tBodyDataAnimals... etc.

So when the first for loop finds into the csvRecordsList a string staring with the string into the variable myTBodySep it gets the tablebodyid from the same row: this will be the new tbodyid that has to be targeted for injecting next records in it

Each table is archived into the CSV file

The first for loop scan the csv record list from the file and the second for loop prepare what is needed to pile the targeted table with data.

The above code works well but it is a little bit slow: in fact to load into the HTML tables about 300 records from the CSV file it takes a bit more of 2.5 seconds on a puter with 2 GB ram and Pentium core 2 4300 dual-core at 1800 MHz but if I ment the row that update the DOM the function needs less than 0.1 sec. So IMHO the bottle neck is the fragment and DOM manipulating part of the code.

My aim and hope is to optimize the speed of the above code without losing functionalities.

Notice that I'm targeting just modern browsers and I don't care about others and non standards-pliant browsers... I feel sorry for them...

Any suggestions? Thanks in advance.

Edit 16-02.2018

I don't know if it is useful but lastly I've noticed that if data is loaded from browser sessionstorage the load and rendering time is more or less halved. But strangely it is the exact same function that loads data from both file and sessionstorage. I don't understand why of this different behavior considering that the data is exactly the same and in both cases is passed to a variable handled by the function itself before starting checking performance timing.

Edit 18.02.2018

  1. Number of rows is variable depending on the target table: from 1 to 1000 (could be even more in particular cases)
  2. Number of columns depending on the target table: from 10 to 18-20

I created the following js function

function csvDecode(csvRecordsList)
{
    var cel;
    var chk;
    var chkACB;
    var chkAF;
    var chkAMR;
    var chkAN;
    var csvField;
    var csvFieldLen;
    var csvFieldsList;
    var csvRow;
    var csvRowLen = csvRecordsList.length;
    var frag = document.createDocumentFragment();
    var injectFragInTbody = function () {tblbody.replaceChild(frag, tblbody.firstElementChild);};
    var isFirstRec;
    var len;
    var newEmbtyRow;
    var objCells;
    var parReEx = new RegExp(myCsvParag, 'ig');
    var tblbody;
    var tblCount = 0;
    var tgtTblBodyID;

    for (csvRow = 0; csvRow < csvRowLen; csvRow++)
    {
        if (csvRecordsList[csvRow].startsWith(myTBodySep))
        {
            if (frag.childElementCount > 0)
            {
                injectFragInTbody();
            }
            tgtTblBodyID = csvRecordsList[csvRow].split(myTBodySep)[1];
            newEmbtyRow = getNewEmptyRow(tgtTblBodyID);
            objCells = newEmbtyRow.cells;
            len = newEmbtyRow.querySelectorAll('input')[0].parentNode.cellIndex; // Finds the cell index where is placed the first input (Check-box or button)

            tblbody = getElById(tgtTblBodyID);
            chkAF = toBool(tblbody.dataset.acceptfiles);
            chkACB = toBool(tblbody.dataset.acceptcheckboxes) ;
            chkAN = toBool(tblbody.dataset.acceptmultiplerows) ;
            tblCount++;
            continue;
        }

        csvRecordsList[csvRow] = csvRecordsList[csvRow].replace(parReEx, myInnerHTMLParag); // Replaces all the paragraph symbols ¶ used into the db.csv file with the tag <br> needed into the HTML content of table cells, this way will be possible to use line breaks into table cells
        csvFieldsList = csvRecordsList[csvRow].split(myEndOfFld);

        csvFieldLen = csvFieldsList.length;
        for (csvField = 0; csvField < csvFieldLen; csvField++)
        {
            cel = chkAN ? csvField + 1 : csvField;
            if (chkAF && cel === 1) {objCells[cel].innerHTML =  makeFileLink(csvFieldsList[csvField]);} 
            else if (chkACB && cel === len) {objCells[cel].firstChild.checked = toBool(csvFieldsList[csvField]);}
            else {objCells[cel].innerHTML = csvFieldsList[csvField];}
        }
        frag.appendChild(newEmbtyRow.cloneNode(true));
    }
    injectFragInTbody();

    var recNum = getElById(tgtTblBodyID).childElementCount;
    customizeHtmlTitle();
    return csvRow - tblCount + ' (di cui '+ recNum + ' record di documenti)';
}

More than 90% of records could contain file names that have to be processed by the following makeFileLink function:

function makeFileLink(fname)
{
    return ['<a href="', dirDocSan, fname, '" target="', previewWinName, '" title="Apri il file allegato: ', fname, '" >', fname, '</a>'].join('');
}

It aims to decode a record list from a special type of *.db.csv file (= a ma-separated values where mas are replaced by another symbol I hard-coded into the var myEndOfFld). (This special type of *.db.csv is created by another function I wrote and it is just a "text" file).

The record list to decode and append to HTML tables is passed to the function with its lone parameter: (csvRecordsList).

Into the csv file is hosted data ing from more HTML tables.

Tables are different for number of rows and columns and for some other contained data type (which could be filenames, numbers, string, dates, checkbox values).

Some tables could be just 1 row, others accept more rows.

A row of data has the following basic structure:

data field content 1|data field content 2|data field content 3|etc...

Once decoded by my algorithm it will be rendered correctly into the HTML td element even if into a field there are more paragraphs. In fact the tag
will be added where is needed by the code:

csvRecordsList[csvRow].replace(par, myInnerHTMLParag)

that replaces all the char I choose to represent the paragraph symbol I have hard-coded into the variable myCsvParag.

Isn't possible to know at programming time the number of records to load in each table nor the number of records loaded from the CSV file, nor the number of fields of each record or what table field is going to contain data or will be empty: in the same record some fields could contain data others could be empty. Everything has to be discovered at runtime.

Into the special csv file each table is separated from the next by a row witch contains just a string with the following pattern: myTBodySep = tablebodyid where myTBodySep = "targettbodydatatable" that is just a hard coded string of my choice. tablebodyid is just a placeholder that contains a string representing the id of the target table tbody element to insert new record in, for example: tBodyDataCars, tBodyDataAnimals... etc.

So when the first for loop finds into the csvRecordsList a string staring with the string into the variable myTBodySep it gets the tablebodyid from the same row: this will be the new tbodyid that has to be targeted for injecting next records in it

Each table is archived into the CSV file

The first for loop scan the csv record list from the file and the second for loop prepare what is needed to pile the targeted table with data.

The above code works well but it is a little bit slow: in fact to load into the HTML tables about 300 records from the CSV file it takes a bit more of 2.5 seconds on a puter with 2 GB ram and Pentium core 2 4300 dual-core at 1800 MHz but if I ment the row that update the DOM the function needs less than 0.1 sec. So IMHO the bottle neck is the fragment and DOM manipulating part of the code.

My aim and hope is to optimize the speed of the above code without losing functionalities.

Notice that I'm targeting just modern browsers and I don't care about others and non standards-pliant browsers... I feel sorry for them...

Any suggestions? Thanks in advance.

Edit 16-02.2018

I don't know if it is useful but lastly I've noticed that if data is loaded from browser sessionstorage the load and rendering time is more or less halved. But strangely it is the exact same function that loads data from both file and sessionstorage. I don't understand why of this different behavior considering that the data is exactly the same and in both cases is passed to a variable handled by the function itself before starting checking performance timing.

Edit 18.02.2018

  1. Number of rows is variable depending on the target table: from 1 to 1000 (could be even more in particular cases)
  2. Number of columns depending on the target table: from 10 to 18-20
Share Improve this question edited Feb 18, 2018 at 11:24 willy wonka asked Feb 9, 2018 at 6:03 willy wonkawilly wonka 1,6961 gold badge22 silver badges32 bronze badges 6
  • Can you provide a sample of what you think the output HTML should be after this function runs? That might help me give you a better answer, rather than just trying to reverse-engineer what you have done here. – th3n3wguy Commented Feb 16, 2018 at 0:34
  • What does injectFragInTbody do? Are you sure you need it inside the for loop? Additional advice: try not using strings & innerHTML (use document.createElement and node.appendChild instead) to see if it makes a difference (it should). – user3297291 Commented Feb 16, 2018 at 12:18
  • @user3297291 the injectFragInTbody function replaces the empty row of the table with the block of rows contained into the fragment that contains data got from file (or from browser sessionstorage). It does that at every change of the targeted table and at the end of the loop for the last table. – willy wonka Commented Feb 16, 2018 at 13:01
  • @th3n3wguy no need for a sample: the code just get the data of the variable csvRecordsList (that could be a file content or a key value of the browser session storage content, doesn't matter) and inserts it into some html tables – willy wonka Commented Feb 17, 2018 at 19:29
  • As far as the JS, use the dev tools profiler and narrow down which things take the longest. But also, DOM manipulation is a two-way street - reading from the DOM can have a significant impact depending on how many nodes your page has loaded, classes, etc. You need to pay attention to things like - when the elements are replaced, have they moved or gotten bigger/smaller? Styles and classes on that table/td's will also have a significant impact. Tables are exempt as a layout boundary but the rendering inside can still be costly, especially if you have more than ~200 rows and lots of columns. – Deryck Commented Feb 18, 2018 at 8:00
 |  Show 1 more ment

3 Answers 3

Reset to default 5 +25

In fact, building the table using DOM manipulations are way slower than simple innerHTML update of the table element.

And if you tried to rewrite your code to prepare a html string and put it into the table's innerHTML you would see a significant performance boost.

Browsers are optimized to parse the text/html which they receive from the server as it's their main purpose. DOM manipulations via JS are secondary, so they are not so optimized.

I've made a simple benchmark for you.

Lets make a table 300x300 and fill 90000 cells with 'A'. There are two functions.

The first one is a simplified variant of your code which uses DOM methods:

var table = document.querySelector('table tbody');
var cells_in_row = 300, rows_total = 300;

var start = performance.now();
fill_table_1();
console.log('using DOM methods: ' + (performance.now() - start).toFixed(2) + 'ms');

table.innerHTML = '<tbody></tbody>';


function fill_table_1() {
  var frag = document.createDocumentFragment();

  var injectFragInTbody = function() {
    table.replaceChild(frag, table.firstElementChild)
  }

  var getNewEmptyRow = function() {
    var row = table.firstElementChild;
    if (!row) {
      row = table.insertRow(0);
      for (var c = 0; c < cells_in_row; c++) row.insertCell(c);
    }
    return row.cloneNode(true);
  }

  for (var r = 0; r < rows_total; r++) {
    var new_row = getNewEmptyRow();
    var cells = new_row.cells;
    for (var c = 0; c < cells_in_row; c++) cells[c].innerHTML = 'A';
    frag.appendChild(new_row.cloneNode(true));
  }
  injectFragInTbody();
  return false;
}
<table><tbody></tbody></table>

The second one prepares html string and put it into the table's innerHTML:

var table = document.querySelector('table tbody');
var cells_in_row = 300, rows_total = 300;

var start = performance.now();
fill_table_2();
console.log('setting innerHTML: ' + (performance.now() - start).toFixed(2) + 'ms');

table.innerHTML = '<tbody></tbody>';

function fill_table_2() {// setting innerHTML
  var html = '';
  for (var r = 0; r < rows_total; r++) {
    html += '<tr>';
    for (var c = 0; c < cells_in_row; c++) html += '<td>A</td>';
    html += '</tr>';
  }
  table.innerHTML = html;
  return false;
}
<table><tbody></tbody></table>

I believe you'll e to some conclusions.

I've got two thoughts for you.

1: If you want to know which parts of your code are (relatively) slow you can do very simple performance testing using the technique described here. I didn't read all of the code sample you gave but you can add those performance tests yourself and check out which operations take more time.

2: What I know of JavaScript and the browser is that changing the DOM is an expensive operation, you don't want to change the DOM too many times. What you can do instead is build up a set of changes and then apply all those changes with one DOM change. This may make your code less nice, but that's often the tradeoff you have when you want to have high performance.

Let me know how this works out for you.

You should start by refactoring your code in multiples functions to make it a bit more readable. Make sure that you are separating DOM manipulation functions from data processing functions. Ideally, create a class and get those variables out of your function, this way you can access them with this.

Then, you should execute each function processing data in a web worker, so you're sure that your UI won't get blocked by the process. You won't be able to access this in a web worker so you will have to limit it to pure "input/output" operations.

You can also use promises instead of homemade callbacks. It makes the code a bit more readable, and honestly easier to debug. You can do some cool stuff like :

this.processThis('hello').then((resultThis) => {
    this.processThat(resultThis).then((resultThat) => {
        this.displayUI(resultThat);
    }, (error) => {
        this.errorController.show(error); //processThat error
    });
}, (error) => {
    this.errorController.show(error); //processThis error
});

Good luck!

发布评论

评论列表(0)

  1. 暂无评论