2

I need to modify each cell of a 100 columns x 50 rows HTML <table> each time a key is pressed. This works, but there is a 250ms - 500ms delay before the rendering is done on my machine.

(The code snippet below has to be opened in "full page" to see this effect, you can use the arrow keys to trigger keydown events).

How to speed up the rendering of such a 5000-cell HTML table?

Note:

  • I need to keep 100 columns and fixed width (each cell takes 1% of the window width), and fixed height for each row, thus the overflow: hidden CSS rules

  • here I used one random alphanumeric for each cell, but in my code, it is another content (still one char per cell)

var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
window.onkeydown = function(e) {
    // very fast from here ...
    var start = new Date();
    var s = '';
    for (i = 0; i < 50; i++) {
        s += '<tr>';
        for (j = 0; j < 100; j++) {
            s += '<td>' + characters.charAt(Math.floor(Math.random() * 62)) + '</td>';
        }
        s += '</tr>\n';
    }
    console.log(new Date() - start);
    // ... to here: ~ 3 ms on average, no need to optimize the previous lines?
    document.getElementById('main').innerHTML = s;
    document.body.offsetHeight;  // optional, triggers reflow/repaint to measure elapsed time more accurately
    console.log(new Date() - start);
};
window.onkeydown();
* { padding: 0; margin: 0; border: 0; font-family: sans-serif; } 
body, html { width: 100%; }
td { width: 1%; overflow: hidden; } 
tr { line-height: 15px; overflow: hidden; white-space: nowrap; }
table { table-layout: fixed; width: 100%; border-collapse: separate; border-spacing: 0; } 
<table id='main'>
</table>
Basj
  • 41,386
  • 99
  • 383
  • 673
  • When I run it, the logged values are never in the double digits, let alone three or four. Are you using a particularly old browser? You may want to look at the `console.time()` and `console.timeEnd()` functions too. – Heretic Monkey Dec 04 '20 at 19:09
  • The logged values are ~ 3 milliseconds on average for me too (Chrome 86). But there is a delay **after** this `console.log()`, before the actual refresh of the display. Do you see this too @HereticMonkey? – Basj Dec 04 '20 at 19:11
  • To reproduce the problem better, do: `document.body.offsetHeight; console.log(new Date() - start);` at the end for measurement (*after* the `.innerHTML`); force repainting and reflow. Goes from 30ms-ish to 250ms-ish on my machine. – CertainPerformance Dec 04 '20 at 19:12
  • The bottleneck is in the browser rendering process, not in the JavaScript. I don't think there's anything significant you could change in the JS to improve performance. The DOM is just fundamentally slow when rendering huge numbers of elements. – CertainPerformance Dec 04 '20 at 19:14
  • @CertainPerformance Maybe there's a way to prepare everything in memory, and then insert the DOM element ready-to-display? – Basj Dec 04 '20 at 19:15
  • 2
    You can with a DocumentFragment and you can try `createElement` instead of creating an HTML string, but it doesn't really help, I tried. I think the problem is just the sheer number of elements. – CertainPerformance Dec 04 '20 at 19:16
  • 2
    You could also try using CSS grid instead of a `table` element; I've found that modern browsers render the grids faster than tables often (I built a Minesweeper clone using grid just for that reason). – Heretic Monkey Dec 04 '20 at 19:18
  • If only a part of the matrix is always visible you can have a look at virtualized scroll, depending on how much is hidden it can have a dramatic effect. – Sergiu Paraschiv Dec 04 '20 at 19:19
  • Are all the rows visible in the screen at a time? If not you can look into "windowing", i.e. rendering only the nodes that would be visible. Here is an example in React https://react-window.now.sh/#/examples/list/fixed-size though you can do it in regular JS too. – Talha Dec 04 '20 at 19:19
  • @Talha 100% of the cells are visible, I took 50 just to make things simple, but in fact I take a number of lines that fit in the viewport. – Basj Dec 04 '20 at 19:20
  • 1
    @SergiuParaschiv All the cells are visible (the number of lines might be 30 to 60 depending on the screen resolution). – Basj Dec 04 '20 at 19:21
  • Does it _need_ to be selectable text in a HTML table? Can't you use a canvas? – Sergiu Paraschiv Dec 04 '20 at 19:21
  • @SergiuParaschiv Yes it should be selectable (copy/past-able). If not, indeed, canvas would have been a good idea. – Basj Dec 04 '20 at 19:22
  • @HereticMonkey Would you have a solution with CSS grid instead? If so it could be interesting! – Basj Dec 04 '20 at 19:23
  • Without knowing what the real wold case is it is hard to give ideas on better performance. What exactly are you doing? I doubt it is generating random numbers. – epascarello Dec 04 '20 at 19:23
  • @epascarello The real world case is exactly the same, except the data is not random but base on some project data. – Basj Dec 04 '20 at 19:24
  • Does it also _have_ to be a table? – Sergiu Paraschiv Dec 04 '20 at 19:24
  • @SergiuParaschiv Not necessarily, but it has to have a fixed grid (same width / height for each item), selectable, copy/pastable. – Basj Dec 04 '20 at 19:25
  • real world? So are just filtering out rows based on what user entered? – epascarello Dec 04 '20 at 19:25
  • Is this faster for you? https://jsfiddle.net/qgcfa38s/ (for me it's twice as fast) – Sergiu Paraschiv Dec 04 '20 at 19:28
  • @SergiuParaschiv it is much faster, but sadly I can't use this because I can't use `monospace`: the table is full of UTF8 chars (symbols, etc.) that are not always in `monospace`, so I have to keep `sans-serif`. Some of these symbols are wider than others, but as I want them to stay in a strict grid, I have to use `overflow: hidden`. – Basj Dec 04 '20 at 19:31
  • But what you can do is use a different monospaced font that does support UTF-8 chars. https://stackoverflow.com/questions/1938639/monospace-unicode-font – Sergiu Paraschiv Dec 04 '20 at 19:34

2 Answers2

1

Here's an example of the same code, using console.time() and console.timeEnd() to do the timings.

When I run it in Microsoft Edge Version 88.0.705.9 (Official build) dev (64-bit), I get around a 114ms average render time...

var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
window.onkeydown = function(e) {
    // very fast from here ...
    console.time('generate HTML');
    var s = '';
    for (i = 0; i < 50; i++) {
        s += '<tr>';
        for (j = 0; j < 100; j++) {
            s += '<td>' + characters.charAt(Math.floor(Math.random() * 62)) + '</td>';
        }
        s += '</tr>\n';
    }
    console.timeEnd('generate HTML');
    console.time('render');
    // ... to here: ~ 3 ms on average, no need to optimize the previous lines?
    document.getElementById('main').innerHTML = s;
    document.getElementById('main').getBoundingClientRect();
    document.body.innerHeight;
    console.timeEnd('render');
};
window.onkeydown();
* { padding: 0; margin: 0; border: 0; font-family: sans-serif; } 
body, html { width: 100%; }
td { width: 1%; overflow: hidden; } 
tr { line-height: 15px; overflow: hidden; white-space: nowrap; }
table { table-layout: fixed; width: 100%; border-collapse: separate; border-spacing: 0; }
<table id='main'>
</table>

And here I've done a little experiment with a strict 50x100 CSS grid-based display. It seems to render a little slower at its slowest.

var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
window.onkeydown = function(e) {
    // very fast from here ...
    console.time('generate HTML');
    var s = '';
    for (i = 0; i < 50; i++) {
        for (j = 0; j < 100; j++) {
            s += '<div class="td">' + characters.charAt(Math.floor(Math.random() * 62)) + '</div>';
        }
    }
    console.timeEnd('generate HTML');
    console.time('render');
    // ... to here: ~ 3 ms on average, no need to optimize the previous lines?
    document.getElementById('main').innerHTML = s;
    document.getElementById('main').getBoundingClientRect();
    document.body.innerHeight;
    console.timeEnd('render');
};
window.onkeydown();
* { padding: 0; margin: 0; border: 0; font-family: sans-serif; } 
body, html { width: 100%; }
.td { width: 1%; overflow: hidden; min-width: 1em; } 
.wrapper { display: grid; grid-template-columns: repeat(100, 1fr); grid-template-rows: repeat(50, 1fr); }
<div id='main' class='wrapper'>
</div>
Heretic Monkey
  • 11,687
  • 7
  • 53
  • 122
0

Do this and use a monospace font that supports more UTF-8 glyphs. There are plenty of discussions on SO about which unicode monospace font to choose.

window.onkeydown = function(e) {
    // very fast from here ...
    console.time('generate HTML');
   var s = '';
for (i = 0; i < 50; i++) {;
    for (j = 0; j < 100; j++) {
        s += String.fromCharCode(0x0000 + Math.random() * 0xFFFF);
    }
    s += '\n';
}
    console.timeEnd('generate HTML');
    console.time('render');
    document.getElementById('main').innerText = s;
    document.getElementById('main').getBoundingClientRect();
    document.body.innerHeight;
    console.timeEnd('render');
};
window.onkeydown();
* { padding: 0; margin: 0; border: 0; font-family: monospace; } 
body, html { width: 100%; }
<div id='main'>
</div>
Sergiu Paraschiv
  • 9,929
  • 5
  • 36
  • 47
  • It looks nice @SergiuParaschiv, but when using thousands of UTF8 glyphs per page, the performance is slow again: what time do you get for this in full page: https://jsfiddle.net/1e4L9uw2/ ? – Basj Dec 04 '20 at 20:27
  • @Basj The problem are the unsupported glyphs. The more there are the slower it renders. If you try with `s += String.fromCharCode(0x30A0 + Math.random() * 0x30FF);` (Kanji glyphs) you'll see it's down to milliseconds again. That's why your choice of font is _very_ important. – Sergiu Paraschiv Dec 05 '20 at 10:25
  • @Basj I updated my answer to use the entire UTF-8 range. It's now up to ~60ms from ~2ms. One thing that's making your version slow is the use of HTML and `.innerHTML`. You want to avoid that. – Sergiu Paraschiv Dec 05 '20 at 10:35
  • [string.fromCharCode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCharCode) doesn't display all unicode char / UTF8, but only the 65536 first ones, with UTF16 encoding: `A sequence of numbers that are UTF-16 code units. The range is between 0 and 65535 (0xFFFF). Numbers greater than 0xFFFF are truncated`. For me, the display of the first pages are long (can be ~ 500 ms)... – Basj Dec 05 '20 at 15:22
  • ...but as soon as you have already displayed a few pages (i removed your random and I display all of these char consecutively), then everything is probably in the browser font rendering cache (does this exist?), and the next pages are super fast! Down to 10ms :) But still, if you close the browser, and reopen it, the first ~14 pages of 5000 char are slow to display. – Basj Dec 05 '20 at 15:22