10

I have a library that build UI using Javascript, and because of the dynamic content involved I sometimes want to put content out to the browser, examine how the layout was changed to support this and then do a different logic depending on the result. For example: detect if some text is overflowing and truncate it with an ellipsis.

Usually I implement this by putting out the changes, then using window.setTimeout(0) to wait for the layout to update and invoke the rest of the logic. This is obviously sub-optimal as different browsers may either implement a minimal timeout that is too slow to prevent flicker or faster that uses a lot of CPU.

Ideally I would like to do the DOM changes, then force the layout to update synchronously and run the "fix-up" logic immediately inline. Any ideas?

Guss
  • 30,470
  • 17
  • 104
  • 128
  • I don't understand. Why would setTimeout(0) with a slow minimal timeout cause flicker? – Alohci Aug 06 '11 at 11:02
  • If the minimal timeout is too long (lets say more then 100ms), then the update chain that I do will cause flicker: the first step will do something, then the second step fixes that and the third step does something else and so forth - instead of doing everything at once. – Guss Aug 12 '11 at 10:48

4 Answers4

9

My understanding is that reading any of the CSS properties will force a reflow. You should not need to setTimeout at all.

Excerpt from Rendering: repaint, reflow/relayout, restyle:

But sometimes the script may prevent the browser from optimizing the reflows, and cause it to flush the queue and perform all batched changes. This happens when you request style information, such as

 offsetTop, offsetLeft, offsetWidth, offsetHeight
 scrollTop/Left/Width/Height
 clientTop/Left/Width/Height
 getComputedStyle(), or currentStyle in IE

All of these above are essentially requesting style information about a node, and any time you do it, the browser has to give you the most up-to-date value. In order to do so, it needs to apply all scheduled changes, flush the queue, bite the bullet and do the reflow.

Here's a list of the API calls/properties that will trigger a reflow.

(This answer used to link to a site that 404s now. Here's a link to it in the wayback machine.)

Community
  • 1
  • 1
Sean McMillan
  • 10,058
  • 6
  • 55
  • 65
  • 1
    they may trigger a reflow, but I doubt the actual reflow is done immediately(~synchronously). EDIT: I've seen in you link `The browser will setup a queue of the changes your scripts require and perform them in batches.` – BiAiB Aug 05 '11 at 12:44
  • 1
    What I'm doing is to try to read the layout properties as you describe, but in some browsers (notably IE), this will not work well as the properties have not been yet updated to take into account my changes. – Guss Aug 12 '11 at 10:49
  • True, `clientHeight`, `currentStyle` did not trigger an immediate reflection in IE8 – Niks May 20 '13 at 12:44
  • @PeterMajeed: Thanks; Found a live version. – Sean McMillan Oct 24 '13 at 15:55
  • Live site for `Here's a list of the API calls/properties that will trigger a reflow` https://gist.github.com/paulirish/5d52fb081b3570c81e3a – joseantgv Feb 06 '20 at 16:32
  • Thanks, I've edited the answer with the new link and a wayback for the old one. – Sean McMillan Feb 07 '20 at 16:21
3

We encountered a crazy problem with IE8 (Firefox, Chrome are fine). We use toggleClass('enoMyAddressesHide') on child element.

.enoMyAddressesHide{display:none}

But the parent(s) div container does not refresh/re-layout its height.

setTimeout(), read position, read width and height of element do not help. Finally we can find out a working solution:

jQuery(document).ready(function ($) {
    var RefreshIE8Layout = function () {
        $('.enoAddressBook:first').css('height', 'auto');
        var height = $('.enoAddressBook:first').height();
        $('.enoAddressBook:first').css('height', height);
    };

    $(".enoRowAddressInfo .enoRowAddressInfoArea ul li img.enoMyAddresses").click(function () {
        $(this).parent().find(".enoAllInfoInAddressBox,img.enoMyAddresses").toggleClass('enoMyAddressesHide');

        RefreshIE8Layout(); // fix IE8 bug, not refresh the DOM dimension after using jQuery to manipulate DOM
    });
});

It looks stupid, but it works!

Thach Lockevn
  • 1,438
  • 1
  • 12
  • 15
  • This looks interesting. Do you think resetting the height to `auto` after setting it to something specific will retain the reflow behavior? Because I would really like to have the height still be automatic. – Guss Oct 13 '11 at 12:23
  • @Guss in my test (with IE8 native browser on Win7), what we have to do is **change** the `height CSS` property. You could change to something else (`100%, 50px`, ...), then change back to `auto`. – Thach Lockevn Oct 14 '11 at 05:17
  • Awesome. I'm not sure when I'll get to try that (been busy with other projects lately) but it looks like what I was looking for so I'm going to go ahead and accept your answer. Thanks! – Guss Oct 14 '11 at 11:35
0

Yes, you can!!

Most browsers optimize the reflow process by queuing changes and performing them in batches. Flushing the render tree changes requires you to retrieve some layout information ( offset calculations, getComputedStyle(), and scroll values ).

var el = document.getElementById("my-element");
var top = el.offsetTop;

The above code will force the browser to execute changes in rendering queue in order to return the correct values.

Simple!!

0

I have no idea that this will actually work for your implementation, but:

var myDiv = document.getElementById("myDiv");
myDiv.innerHTML = "".concat(myDiv.innerHTML);

I believe that setting innerHTML forces a DOM reload for a child element. I have no idea what would happen if you scoped this to the whole site. It sounds like you're pretty well hooked into the DOM all over the place though, and this method might force any node references you've already made to become undefined. In other words:

var mySelect = document.getElementById("mySelect");// suppose this lives in myDiv.
var myDiv = document.getElementById("myDiv");
myDiv.innerHTML = "".concat(myDiv.innerHTML);

In the above instance, I believe you'll lose your reference to mySelect. If you've got lots of this going on and you're using fields not getters (i.e., not getting each element you care about using $() or $get or document.getElementById(..) every time you access it) then there's a strong chance this type of flushing could hose you.

You'd also most certainly lose any page state data you're not manually tracking/setting - textboxes with new text from the user, checkboxes newly checked by the user, etc, would reinitialize to their default state using this approach to flushing.

Good luck - happy coding!

B

Brian
  • 2,772
  • 15
  • 12
  • Yea, this is a bust: I'm not about to re-render the entire page as that would break a lot of things I have no control of. – Guss Aug 12 '11 at 10:46