I've got a for loop in which I am appending rows to a table. Although I append each tr one at a time in the loop, the browser does not render any of them until the loop is finished.
At first, I thought it might be the browser rendering too quickly for me to notice, but after increasing the number of rows to, say, 10000, there's a significant slowdown, then the entire table shows at once.
Link: http://jsfiddle.net/xyan/ad2tV/
Increment counter to increase the number of rows.
Also, if you change counter to 3 (or another small number) and uncomment the alert(), it will pause the loop and show each row being added one at a time.
HTML:
<div></div>
Javascript:
var table = $('<table />').appendTo($('div'));
var counter = 1000;
var html = [];
var j = 0;
for (var i = 1 ; i < (counter + 1) ; i++) {
html[j++] = '<tr>';
html[j++] = '<td>'+i+'-1</td><td>'+i+'-2</td><td>'+i+'-3</td>';
html[j++] = '<tr>';
table.append(html.join(''));
//alert('pause');
html = [];
j = 0;
}
CSS:
table td {
padding: 3px;
border: 1px solid red;
}
Note:
I've found a way to force the loop to slow down, allowing the rows to be added one at a time.
Link: http://jsfiddle.net/xyan/8SCP9/
var html = '';
var numbertorun = 15;
var delay = 500;
(function loop (i) {
setTimeout(function () {
html = '<tr><td>'+i+'-1</td><td>'+i+'-2</td></tr>';
$('table').append(html);
if (--i) loop(i);
}, delay)
})(numbertorun);
So I guess my question isn't how to do this, but why the rows aren't inserted one at a time in the original for loop.
I'm guessing some background is needed to understand this question.
Each time you add a new row to the table in your webpage, your script will alter the model of your document in your browsers memory. The API you are using for this is called DOM (Document Object Model).
An individual update to DOM doesn't always necessarily mean that a page will be redrawn. It is up to the browser to decide, when a given set of DOM alterations will cause the browser to re-render the page.
I don't know for fact, but I'm guessing that browsers typically update views perhaps somewhere around 40-60 times per second. The thing is this: if your table printing takes less time than 1/60th of a second, you don't get to see the partial updates.
By adding a timer you of course can slow down the progress of creating the table. As you perceived, this will allow you to see the page redrawn at different points during the for loop.
If you wish to force browser to redraw the UI, there are some JavaScript hacks you could try applying. One such hack for instance is documented in Ajaxian: Forcing a UI redraw from JavaScript.
If you deliberatively wish to run your program slowly and add rows slowly, say one row per second, while potentially doing some other stuff with JavaScript at the same time, you need to run the loop either by triggering new iterations somehow or by using another (JavaScript) thread.
A schematic example of using setTimeout to slow down printing (prints out 1 row per sec, just tweak second parameter in window.setTimeout to change delay):
var table = $('<table id="table1"/>').appendTo($('div'));
var i = 0;
var counter = 1000;
function addRow() {
table.append('<tr><td>'+i+'-1</td><td>'+i+'-2</td><td>'+i+'-3</td></tr>');
i++;
if(i<counter) {
window.setTimeout( addRow, 1000 );
}
}
addRow();
jQuery append method is similar to javascript appendChild method. Both these methods traverse through the DOM and insert the element at the end of the parent node. This task takes time and doing this inside a Loop will cause frequent changes in DOM Structure which the DOM will not able to replicate. So by inserting your rows using settimeout gives the browser ample time to re-load the content. Hence you see the one by one insertion of rows in the second approach.
Each time an element is added to the page, the browser needs to redraw the entire page. In rendering terms, this is an extremely intensive process and is quite slow. So if you make small changes little by little that will make the page appear really slowly. What the browser is doing is to group the little changes into big changes. The end result is that the page will render much more quickly in aggregate.
The browser is doing this:
http://www.tvidesign.co.uk/blog/improve-your-jquery-25-excellent-tips.aspx#tip6
Related
First of all a disclaimer, I'm not a dev. I'm halfway through The Odin Project and have covered some HTML and CSS, but, have not yet started on JS. In order to help with my learning I've created my own blog. My aim is for each blog post to have its own stylesheet (so with each new post I learn a little more about CSS).
Anyway, I plan to write a post about the benefits of using an eReader, specifically the Kindle. I've styled the page to look like a Kindle Oasis, and I'd like the reader to be able to step through the article contents via the Kindle's next/prev buttons, but, as I'm not a dev, this is where I'm stuck. Via Stack overflow I've managed to add some JS that will display page 1, 2 and 3 via dedicated buttons for each dive element, but, what I really need is to step through x number of pages via the prev/next buttons.
Here's what I have so far: https://codepen.io/dbssticky/pen/yLVoORO. Any help would be much appreciated. What I should do of course is finish The Odin Project and come up with a solution on my own, but, I'd really like to get this Kindle article published sooner rather than later. Hence my rather cheeky request for assistance.
Here's the JS I'm currently using:
function swapContent(id) {
const main = document.getElementById("main_place");
const div = document.getElementById(id);
const clone = div.cloneNode(true);
while (main.firstChild) main.firstChild.remove();
main.appendChild(clone);
}
You have the right idea and it just needs a few adjustments to get the previous/next functionality.
Currently your div IDs are following the format operation1, operation2, and so on. Since you want the previous/next functionality you'll need to change your 'swapping' function, which currently takes the full ID, to use the numeric portion only.
Add a new function which appends the number to 'operation' instead of using the whole thing:
function goToPage(pageNumber){
const main = document.getElementById("main_place");
const div = document.getElementById("operation" + pageNumber);
const clone = div.cloneNode(true);
while (main.firstChild) main.firstChild.remove();
main.appendChild(clone);
}
And then change your Page 1/2/3 buttons to use goToPage(1), goToPage(2) and so on.
Now for the previous/next functionality you'll need a way to track which page you're on, so that you can figure out which page to load.
Add a variable at the top (outside functions)
var currentPage = 0;
Then add a line in your goToPage function to track the page you're on.
currentPage = pageNumber;
Now that you're tracking you can add a previous and next function.
function goNextPage(){
goToPage(currentPage-1);
}
function goPreviousPage(){
goToPage(currentPage+1);
}
Then call it from the previous and next buttons.
<button onClick="goNextPage()" class="next-button"></button>
<button onClick="goPreviousPage()" class="previous-button"></button>
Here's a codepen: https://codepen.io/srirachapen/pen/WNZOXQZ
It's barebones and you may have to handle things like non existent div IDs.
HTML
<button class="next-button" onclick="nextContent()"></button>
<button class="previous-button" onclick="prevContent()"></button>
JS
var pageid = 0;
var maxpage = 3;
function nextContent() {
if(pageid == maxpage) return
pageid++
swapContent(`operation${pageid}`)
}
function prevContent() {
if(pageid == 1) return
pageid--
swapContent(`operation${pageid}`)
}
you can try this to switch between pages. But you may need to edit the "swapContent" method more sensibly.
Track the Current Page
Whatever solution you use to render pages & links (manual hardcoded links & content vs externally-stored & auto-generated), one thing is unavoidable: You need to track the current page!
var currentPage = 0
Then, any time there's a page change event, you update that variable.
With the current page being tracked, you can now perform operations relative to it (e.g. +1 or -1)
I'd suggest making a goToPage(page) function that does high-level paging logic, and keep your swapContent() function specifically for the literal act of swapping div content. In the future, you may find you'd want to use swapContent() for non-page content, like showing a "Welcome" or "Help" screen.
Example:
function goToPage(page) {
// Update `currentPage`
currentPage = page
// ... other logic, like a tracking event or anything else you want you occur when pages change
// Do the actual content swap, which could be your existing swapContent()
swapContent('operation'+page)
}
You'd invoke the function like so:
goToPage(3) // Jump to a specific page
goToPage(currentPage + 1) // Go to the next page
goToPage(currentPage - 1) // Go to the prev page
You can make separate helper functions like "goToNextPage()" if you desire, but for sure you start with a fundamental page-change function first.
I want to select all text frames and then ALIGN its content to the right of the page of all the ODD numbered pages of my document in InDesign using only Javascript.
here's my progress so far and I know I can determine the odd numbers but still can't make selecting a page work thus no progress to select its text frames too.
main();
function main() {
var myDocument = app.documents.item(0);
var myPage = myDocument.pages.item(0);
var i = 0;
for (i = 1; i < myDocument.pages.count(); i = i + 2) {
\\ select the page, then find all text frames in that page, then align right
}
}
any help is appreciated. thank you.
To get all of the frames on a page:
var myFrames = myDocument.pages[i].textFrames;
Then you can loop through the frames and their paragraphs and apply (use different counter variables, e.g "c" and "b")
myFrames[c].paragraphs[b].justification = Justification.RIGHT_ALIGN;
You can also try everyItem()
myDocument.pages[i].textFrames.everyItem().paragraphs.everyItem().justification = Justification.RIGHT_ALIGN;
Here is a simplest solution:
var pages = app.activeDocument.pages;
for (var i = 1; i < pages.length; i = i + 2) {
app.select(pages[i].textFrames);
try {
app.menuActions.item("$ID/Horizontal Page Align Left").invoke()
} catch(e) {}
}
It relies on selection objects and invoke the menu action. Sometimes this is not the best idea (that is why try/catch). The solution can be more complicated and robust. It depends on your limitations and additional details.
Update
I didn't get that you need to align CONTENT of the frames rather than the frames. It can be done, but the solution differs for linked and non-linked text frames. Except when one paragraph belongs two neighboring pages.
I've come across an interesting issue, which may be browser related (but I'm not sure about that). I'm working on a very large webpage, which is a word-processor style app. I have this structure:
<article>
<div>...</div>
<div>...</div>
<div>...</div>
... 5000 more divs ...
</article>
When I first load the page, I have a function that does two things. First, it analyses the content of each div's html using regex and applies a class to the div if it matches a particular regex, resulting in something like this:
<article>
<div class="type1">...</div>
<div class="type2">...</div>
<div class="type3">...</div>
... 5000 more divs ...
</article>
The second part of the function then calculates the height of each div and adds it to a counter. Once it passes a certain number, I insert a piece of HTML that serves as a page break, then rinse and repeat:
// Pagination variables
var document_current_page_height = 0;
var constant_default_page_height = 1000;
var page_number = 0;
// Iterate over page elements
object_range.each(function()
{
// Check whether page is too long
if (document_current_page_height + $(this).outerHeight(true) > constant_default_page_height)
{
// Create page break
$(this).before("<section contenteditable=\"false\"><h3 class=\"page-footer\">" + document_page_footer" + "</h3><figure><hr /><hr /></figure><h1 class=\"page-header\">" + document_page_header + "</h1><h2 class=\"page-number\">" + page_number + "</h2></section>");
// Adjust variables
page_number++
document_current_page_height = 0;
}
// Increment current page height
document_current_page_height += $(this).outerHeight(true));
});
This code works perfectly, but here's the issue...
When run as it is supposed to be run, it takes about 2 seconds. However, if I run the same code, but skip the first part of the process (which adds classes to the divs), the code runs in 0.2 seconds.
I was further able to isolate the problem by commenting the $(this).before line of code. When this is done, both speed tests are near enough identical (within 100ms).
What I don't get, is that the $(this).before line of code does not reference the heights / styles / classes of the divs, so why the delay when the divs have a class?
Is it simply a case of the browser struggling with the divs when they have classes? Any thoughts? (I'm using Safari 9.1).
Thanks!
I found a solution to my issue, though it doesn't actually address why the browser behaves differently...
Instead of using $(this).before, I ended up adding the relevant $(this) to a vanilla JS array and then calling before later on:
// Pagination variables
var document_current_page_height = 0;
var constant_default_page_height = 1000;
var page_number = 0;
var page_break_array = [];
// Iterate over page elements
object_range.each(function()
{
// Check whether page is too long
if (document_current_page_height + $(this).outerHeight(true) > constant_default_page_height)
{
// Create page break
page_break_array.push([$(this), page_number]);
// Adjust variables
page_number++
document_current_page_height = 0;
}
// Increment current page height
document_current_page_height += $(this).outerHeight(true));
});
// Create page breaks (done separately for performance benefit)
for (var i = 0; i < page_break_array.length; i++) { page_break_array[i][0].before("... HTML content ..."); }
I am building a filter for querying our html table (10k+ rows). My initial thought was to first hide all the rows, and then unhide the rows that match the specific query. If the filter is removed, then show all rows.
Is this a more optimal way of writing the hide/show all rows functions?
// optimize this!
this.hideAllRows = function() {
nodes = document.getElementById('table_body').children
for(var i=0, i_max=nodes.length; i<i_max; i++) {
nodes[i].style.display="none"
}
}
// optimize this!
this.showAllRows = function() {
nodes = document.getElementById('table_body').children
for(var i=0, i_max=nodes.length; i<i_max; i++) {
nodes[i].style.display=""
}
}
Thanks!
One solution would be to implement a pagination or "infinite" scroll feature. This would remove the need to render 10k dom elements simultaneously. You could render them in batches, as the user scrolls, or chunk them into pages.
Alternatively, you can try pulling the table out of the dom, hiding rows, then reinserting it. This will prevent unneccesary reflows/paints/etc.
As a rule of thumb using a pure javascript for loop is faster than using jQuery .each() but your basic selection using the .getElementById() and .children property is already well optimized.
However iterating through 10k+ elements in the browser is always going to be slow. Showing and hiding elements is best suited to record sets in the 100s not the 1000s.
Why not make an AJAX request that returns data (presumably from a database) already formated as <tr>...some <td>s here....</tr><tr>...some <td>s here....</tr>?
That way you can let your database do all the heavy lifting when it comes to filtering, they are optimized to do this. It keeps your code is simple, your bandwidth is reduced, and your DOM manipulation is kept to a minimum.
Whenever you want to apply a filter, you can make a $.ajax request.
function filter(criteria){
$.ajax({
url : myDataUrl,
type : "POST",
data : {criteria : criteria}
})
.success(function (data){
$("#table-body").html(data);
});
}
I've got a loop where I insert about 500 <div>s into the DOM via document.getElementById(target).appendChild(docFragmentDiv), and each <div> has probably 20 or 30 descendants. I can insert up to about 100 just fine, but the page temporarily freezes while the DOM is updated if there's something like 200+ inserts.
How can I avoid this choke? Right now I have an interval set up to insert 50 per interval, with 25 ms between intervals. Is there a better way to do this?
You should use innerHTML, and you should use it only once.
For optimal efficiency, you should assemble pieces of HTML in an array of strings using a loop, then write document.getElementById(target).innerHTML = htmlPieces.join()
Using innerHTML like SLaks suggests will work, but it's probably better practice to create a document fragment, append all of your div nodes to that, then append the document fragment to the main document.
var docFrag = document.createDocumentFragment();
for (var i=0; i<500; i++) {
docFrag.appendChild(document.createElement("div"));
}
document.getElementById(target).appendChild(docFrag);
innerHtml() is almost always faster than appendChild().
Have a look at QuirksMode's benchmarking for real numbers.
Edit: Is there any reason why you're adding them in intervals rather than all at once? I think SLaks is right about doing it all at once.
I'm not sure if this would be any faster, but it would be the first thing I tried:
Instead of adding them all to the page, add them all to a container that you haven't yet added to the page, and then add that container to the page.
If that improves performance, then you can modify it to suit your needs.
Be careful that you aren't using appendChild to the actual document until the last instant. In other words, if you're doing:
var div1 = document.createElement("div");
document.body.appendChild(div1);
for (var i=0; i<500; i++) {
var div = document.createElement("div");
div1.appendChild(div);
}
Then you are redrawing the entire page every time you iterate in that loop.
Better, if you're using DOM methods, to reverse this:
var div1 = document.createElement("div");
for (var i=0; i<500; i++) {
var div = document.createElement("div");
div1.appendChild(div);
}
document.body.appendChild(div1);
Now you are drawing your divs to an offscreen buffer, and only making the page redraw once.