Note that there are some explanatory texts on larger screens.

plurals
  1. POJavascript/HTML: Optimizing large tables / lots of elements
    text
    copied!<p>I need to display visually patterns created by certain configurations of numbers, as well as the relationships between them. To do this, I created a table and used the rows/columns as the x/y coordinates (encoded in ID). To many of the TD elements, I give a meaningful background color and attach mouseover/out handlers to show a popup with that TD's assigned number and information about it. (The cells are much too small to actually contain the number; they are more a visual device and I use said background colors so the pattern can be easily seen.)</p> <p>I need to be able to zoom in and out of this table, as well as manage it in other ways. However, when dealing with large tables (1000 rows and columns, or more) it takes a huge amount of time for the browser to render it at each new zoom level. Doing a little research, I saw that TABLE elements take a bit more juice because the browser has to recalculate based on cell content and available width within the table. Setting 'table-layout' to 'fixed' and giving the table a width should keep that from happening, but it still took forever.</p> <p>Then I thought making a table with DIV elements instead might work, as that would require less total elements and there's no auto-resizing like there is with tables - I can hardcode the dimensions. I tried some code I found on the web using "display: table" (had never heard of that before), as well as non-table-ified DIVs (love making up words...).</p> <p>Here's a test page I had created to test the three and time them (<a href="http://dl.dropbox.com/u/16709081/table.html" rel="nofollow noreferrer">view it here</a>):</p> <pre><code>&lt;!-- Only tested in Google Chrome on Windows 7! I have not bothered testing in other browsers as this is a private project and all members of the project use Chrome. --&gt; &lt;html&gt; &lt;head&gt; &lt;title&gt;Table Test&lt;/title&gt; &lt;style&gt; #controls td { font-family: Courier New, monospace; } input[type=button] { width: 100%; } input[type=text] { width: 50px; } /* styles the pure table version */ table { table-layout: fixed; border-collapse: collapse; } .td { border: 1px solid black; width: 48px; height: 48px; vertical-align: top; } /* styles the DIV version using "display: table" */ .div-table { display: table; border: solid black; border-width: 1px 0px 0px 1px; } .div-table-row { display: table-row; width: auto; clear: both; } .div-table-col { float: left; display: table-column; width: 48px; height: 48px; border: solid black; border-width: 0px 1px 1px 0px; } /* styles the pure DIV version */ div#grid { border: solid black; border-width: 1px 0px 0px 1px; } .cell { display: inline-block; width: 49px; height: 49px; padding: 0px; margin: 0px; border: solid black; border-width: 0px 1px 1px 0px; } &lt;/style&gt; &lt;script type="text/javascript"&gt; function Stopwatch() { // basic Stopwatch object to time functions var startTime = null; var stopTime = null; var running = false; function getTime() { var day = new Date(); return day.getTime(); } this.start = function() { if (running == true) return; else if (startTime != null) stopTime = null; running = true; startTime = getTime(); return startTime; } this.stop = function() { if (running == false) return; stopTime = getTime(); running = false; return stopTime; } this.duration = function() { if (startTime == null || stopTime == null) return 'Undefined'; else return (stopTime - startTime) / 1000; } } var stopwatch = new Stopwatch(); var x = 50; // global dimension variables var y = 100; function updateDims() { // updates the global dimension variables // returns true/false to indicate success x = parseInt(document.getElementById('x').value); y = parseInt(document.getElementById('y').value); if (typeof x == 'number' &amp;&amp; x &gt; 0 &amp;&amp; typeof y == 'number' &amp;&amp; y &gt; 0) return true; return false; } function makeTableTable() { // creates the pure table version if (!updateDims()) return false; // grab desired x/y dimensions // grab count of how many times function has been run to calculate average var count = document.getElementById('count1'); count = count.value = parseInt(count.value) + 1; // grab previous average time to calculate new average var avg = parseFloat(document.getElementById('avg1').innerHTML); // start the stopwatch, grabbing initial start time, and begin concatenating var start = stopwatch.start(); var html = '&lt;table id="grid" cellpadding="0" cellspacing="0"&gt;'; for (var i = 0; i &lt; y; i++) { // for each row... html += '&lt;tr&gt;'; for (var j = 0; j &lt; x; j++) { // build desired number of cells html += '&lt;td class="td" id="'+j+'/'+[y-[i+1]]+'"&gt;' + [1+j+i*x] + '&lt;/td&gt;'; } html += '&lt;/tr&gt;'; // close row j = 0; } html += '&lt;/table&gt;'; // stop the watch and record duration it took to concatenate stopwatch.stop(); document.getElementById('concat1').innerHTML = stopwatch.duration(); // start the watch again and insert HTML stopwatch.start(); document.getElementById('output').innerHTML = html; // stop the watch, grabbing the final end time, and record duration for innerHTML var end = stopwatch.stop(); document.getElementById('insert1').innerHTML = stopwatch.duration(); // find total time from initial start time and final end time document.getElementById('total1').innerHTML = (end - start) / 1000; // calculate average time if (count &gt; 1) document.getElementById('avg1').innerHTML = Math.round(100000*(avg*(count-1)+(end-start)/1000)/count)/100000; else document.getElementById('avg1').innerHTML = (end-start) / 1000; } function makeDivTable() { // creates the DIV version using "display: table" if (!updateDims()) return false; var count = document.getElementById('count2'); count = count.value = parseInt(count.value) + 1; var avg = parseFloat(document.getElementById('avg2').innerHTML); var start = stopwatch.start(); var html = '&lt;div id="grid" class="div-table"&gt;'; for (var i = 0; i &lt; y; i++) { html += '&lt;div class="div-table-row"&gt;'; for (var j = 0; j &lt; x; j++) { html += '&lt;div class="div-table-col" id="'+j+'/'+[y-[i+1]]+'"&gt;' + [1+j+i*x] + '&lt;/div&gt;'; } html += '&lt;/div&gt;'; j = 0; } html += '&lt;/div&gt;'; stopwatch.stop(); document.getElementById('concat2').innerHTML = stopwatch.duration(); stopwatch.start(); document.getElementById('output').innerHTML = html; var end = stopwatch.stop(); document.getElementById('insert2').innerHTML = stopwatch.duration(); document.getElementById('total2').innerHTML = (end - start) / 1000; if (count &gt; 1) document.getElementById('avg2').innerHTML = Math.round(100000*(avg*(count-1)+(end-start)/1000)/count)/100000; else document.getElementById('avg2').innerHTML = (end-start) / 1000; // update width of outer DIV document.getElementById('grid').style.width = 50 * x + 'px'; } function makeDivDiv() { // creates the pure DIV version if (!updateDims()) return false; var cells = x*y; // will iterate through total number of cells, // rather than by row and column var count = document.getElementById('count3'); count = count.value = parseInt(count.value) + 1; var avg = parseFloat(document.getElementById('avg3').innerHTML); var start = stopwatch.start(); var html = '&lt;div id="grid"&gt;'; for (var i = 0; i &lt; cells; i++) { var id = [i-x*Math.floor(i/x)+1] + '/' + [y-Math.floor(i/x)]; html += '&lt;div class="cell" id="' + id + '"&gt;' + [i+1] + '&lt;/div&gt;'; } html += '&lt;/div&gt;'; stopwatch.stop(); document.getElementById('concat3').innerHTML = stopwatch.duration(); stopwatch.start(); document.getElementById('output').innerHTML = html; var end = stopwatch.stop(); document.getElementById('insert3').innerHTML = stopwatch.duration(); document.getElementById('total3').innerHTML = (end - start) / 1000; if (count &gt; 1) document.getElementById('avg3').innerHTML = Math.round(100000*(avg*(count-1)+(end-start)/1000)/count)/100000; else document.getElementById('avg3').innerHTML = (end-start) / 1000; document.getElementById('grid').style.width = 50 * x + 'px'; } &lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;table id="controls" border="1"&gt; &lt;tr&gt; &lt;td&gt;x: &lt;input type="text" id="x" value="50" /&gt;&lt;/td&gt; &lt;td&gt;y: &lt;input type="text" id="y" value="100" /&gt;&lt;/td&gt; &lt;td colspan="7"&gt;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;input type="button" onclick="makeTableTable()" value="Standard table" /&gt; &lt;input type="hidden" id="count1" value="0" /&gt;&lt;/td&gt; &lt;td&gt;Concatenation:&lt;/td&gt; &lt;td id="concat1"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;td&gt;Insertion:&lt;/td&gt; &lt;td id="insert1"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;td&gt;Total:&lt;/td&gt; &lt;td id="total1"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;td&gt;Average:&lt;/td&gt; &lt;td id="avg1"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;input type="button" onclick="makeDivTable()" value="Div w/ 'display:table'" /&gt; &lt;input type="hidden" id="count2" value="0" /&gt;&lt;/td&gt; &lt;td&gt;Concatenation:&lt;/td&gt; &lt;td id="concat2"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;td&gt;Insertion:&lt;/td&gt; &lt;td id="insert2"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;td&gt;Total:&lt;/td&gt; &lt;td id="total2"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;td&gt;Average:&lt;/td&gt; &lt;td id="avg2"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;/tr&gt; &lt;tr&gt; &lt;td&gt;&lt;input type="button" onclick="makeDivDiv()" value="Div Table" /&gt; &lt;input type="hidden" id="count3" value="0" /&gt;&lt;/td&gt; &lt;td&gt;Concatenation:&lt;/td&gt; &lt;td id="concat3"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;td&gt;Insertion:&lt;/td&gt; &lt;td id="insert3"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;td&gt;Total:&lt;/td&gt; &lt;td id="total3"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;td&gt;Average:&lt;/td&gt; &lt;td id="avg3"&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/td&gt; &lt;/tr&gt; &lt;/table&gt;&lt;br /&gt; &lt;div id="output"&gt;&lt;/div&gt; &lt;/body&gt; &lt;/html&gt; </code></pre> <p>First try it with small values, such as the defaults. Then try changing x and y to 1000, and try the default browser zoom function. It should take quite a while to both generate the table as well as zoom in or out.</p> <p>The interesting thing to note, though, is that the times reportedly taken are obviously not the times it actually takes - one can see that just by doing ball park second counting (good ol' "one Mississippi"). A grid of 400x400 consistently took approximately 13 seconds, 5 seconds, and 7 seconds respectively when the reported average times after several runs were about 2.3, 2.7, and 2.4. All the code relating to timing it doesn't affect it much. You can delete it all or comment it all out (<a href="http://dl.dropbox.com/u/16709081/table-notime.html" rel="nofollow noreferrer">view it</a>) and it was the same (13, 5, and 7 for me).</p> <p>So two questions:</p> <p>First, why the discrepancy in those numbers? The timers should be timing everything going on in the function. Aside from that first call to updateDims, the main two things that happen are a string concatenation and an assignment to innerHTML. (There's also a change to the grid's width, but I don't see how that would take much time.) Is it simply browser rendering time, taking place once the function has completed and so not timed? Some evidence in favor of that is the much longer apparent time for the pure table version, meaning the browser probably IS recalculating all the TD widths and such.</p> <p>Second, what can be done about it? Anything? The concatenation is the short part of it, so that's not an issue. (And I already tried pushing into an array and joining, and concatenation was consistently faster, at least for me.) Also, if the stopwatch is to be believed, inserting it into innerHTML doesn't take that long. It's whatever is happening afterwards. I had thought about employing a Backgrounder technique, so even if it takes a while to update, at least it won't lock the browser that whole time and some noticeable activity would be going on. But if the locking point is happening "outside" of the function, I don't see how that could work. I could design my own zoom feature, but I'm not confident it would be any faster than the browser's. Preliminary tests say not.</p> <p>Any other alternatives to what I'm trying to do (although it's probably pretty vague to you; sorry) are certainly appreciated!</p> <p>&nbsp;</p> <p>Some other pages on StackOverflow:</p> <p><a href="https://stackoverflow.com/questions/1364213/are-large-html-tables-slow">Are large html tables slow?</a> makes it sound like tables are normally fast to render. However, that may only be for tables already hard-coded into the HTML file, rather than generated like mine.</p> <p><a href="https://stackoverflow.com/questions/3739819/javascript-performance-with-creating-large-tables">Javascript performance with creating large tables</a> is very similar to my case, except that the accepted solution, having a paginated table, isn't an option for me as that would defeat the purpose. And my table doesn't contain data. However, one of the respondents did imply that, with such a large table, DOM methods may be faster than HTML parsing (even though I've seen elsewhere that the reverse is true; probably with sane amounts of elements). I may test that when I get the chance. What do you think?</p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload