4

Based on this question: How do I know the IntersectionObserver scroll direction?

I was trying to reproduce some layout/reflow cases within the observable callback, but I couldn't, so I tried to simplify the use case and end up asking this question.

I was reading the gist of Paul Irish what-forces-layout.md and my question is very simple.

Having the case of an input without a callback on the body element definitely triggers layout, see the example below:

element.focus() triggers layout

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <input type="text">
  <script type="text/javascript">
    var elementB = document.querySelector('input');

    elementB.focus();
  </script>
</body>
</html>

see chrome performance record

But if wrap the focus event in a click callback, doesn't trigger layout/reflow.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <input type="text">
  <script type="text/javascript">
    var elementB = document.querySelector('input');

    function onClick() {
      elementB.focus();
    }

    document.addEventListener('click', onClick);
  </script>
</body>
</html>

see chrome performance record

So that's my question why isn't triggering layout/reflow?

Jose Paredes
  • 3,882
  • 3
  • 26
  • 50
  • That's the kind of question only the ones who built the dev-tools can answer with a strong affirmation, e.g, it could be that the dev tools don't register short-circuited reflows because the layout didn't change, and thus the reflow had nothing to do. – Kaiido Apr 25 '18 at 09:29
  • @Kaiido, that makes sense, it's also very hard to reproduce this across browsers – Jose Paredes Apr 25 '18 at 09:32

2 Answers2

3

I am not sure If I get your question right but in case 1, when the parser is starts executing the script, the DOMContentLoaded is not fired yet and it is still parsing rest of the document. Meanwhile you call focus on the elemB, you are immediately triggering the layout flow.

In case 2, onClick function is not called at all unless you click the document itself. You can verify this by turning on the "Paint Flashing" on the fiddle you provided. The input will become green only when you click.

Whereas in the first case you see a brief flash of the input at start (thats your call to .focus) and then the whole documentElement (at DOMContentLoaded).

In case two you only have the whole documentElement flash once (on DOMContentLoaded, provided that nothing else trigger reflow/repaint onload event) and then only the input element once per click.

PS:

Now as far as I can see, I have tried your 2 cases on my local machine and interestingly in your first case I see 2 layout activites right after DOMContentLoaded.

However if I comment out the line elementB.focus(); from your case 1 and record again, I see 2 layout activities again.

From my understanding the browser will do 2 layout operations on start, once it starts to parse the body and then once around DOMContentLoaded. And if any synchronous forced layout trashing is done by javascript (by calling any of the methods/properties listed in your link), the browser will try to batch these operations.

To test this behavior, I modified your 1st case as below:

!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <input type="text" style="position:relative;top:0px;">
  <script type="text/javascript">
    var elementB = document.querySelector('input');
    elementB.focus();
  </script>
  <script async="true">
    setTimeout(function(){
        elementB.style.top = parseInt(elementB.style.top) + 5 + "px";
    },500)
  </script>
</body>
</html>

Now what will happen is, you will have a third layout activity right after (~500ms) the load event (async is unnecessary). But if you were to make the setTİmeout 0ms, you would get 2 layout activities again! (the microtask queue behavior might not be guaranteed, in case you see 3 layouts, to force sync layout, remove the async attribute and the setTimeout wrap inside the 2nd script tag). Bottom Line: So the browser batches it, or at least this is what I see from this example.

For your second case, when I record it the way you posted, it is correct that I do not see layout activity (2 layout as before). But what I see is a consistent style recalculation + update layout tree + painting after each event. This makes me think that once the layout tree is updated, if layout trashing is not necessary it is not recalculated. To test that behavior, I changed your second script as below:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <input type="text" style="position:relative;top:0px;">
  <script type="text/javascript">
    var elementB = document.querySelector('input');

    function onClick() {
      elementB.focus();
      elementB.style.top = parseInt(elementB.style.top) + 5 + "px";
    }

    document.addEventListener('click', onClick);
  </script>
</body>
</html>

Here each time you click the document, the input box will move down 5 pixels. If you record during 10 seconds for multiple click events you will see a lot of update layout tree + repaint AND layout trashing as well. This makes me think layout trashing is done after updating layout tree if only necessary.

CONCLUSION (I might be terribly wrong)

  • The browser will try to batch layout activity during parsing of the HTML.
  • element.focus will trigger repaint + update layout tree, but layout trashing is not guaranteed (at least from these examples)
ibrahim tanyalcin
  • 5,643
  • 3
  • 16
  • 22
  • This is not what I meant by saying them. When you are not calling the function reflow won't be triggered, that's all that there is to it. If you do anywhere elem.scrollTop it will trigger reflow but function(){elem.scrollTop ..} will not until it is called. That's why wrapping them does not trigger the reflow. – ibrahim tanyalcin Apr 23 '18 at 17:03
  • the problem is not wrapping, the problem is when I click on the document, `reflow` should be triggered in the second script, but it's not being triggered, so my question is why so? in both cases `elementB.focus()` is being called – Jose Paredes Apr 23 '18 at 17:30
  • If I turn on the paint flashing and click on the document I see the reflow clearly – ibrahim tanyalcin Apr 23 '18 at 17:53
  • `repaint` is not the same as `reflow` what you see in the paint flashing is `repaint` – Jose Paredes Apr 23 '18 at 18:16
  • Yes correct, I will update with a PS, I think you have a false positive. – ibrahim tanyalcin Apr 23 '18 at 19:42
  • `But if you were to make the setTİmeout 0ms, you would get 2 layout activities again!` I have tried this and I still see 3 layouts activities – Jose Paredes Apr 25 '18 at 08:46
  • The behavior of the microtaskq queue might not be guaranteed. In that case to force synchronous layout-trashing remove the setTimeout wrap around the second script and turn of async attribute. You will get 2 layouts again. – ibrahim tanyalcin Apr 25 '18 at 08:59
  • `async`/`defer` attribute has no effect on inline scripts, so it doesn't really matter – Jose Paredes Apr 25 '18 at 09:35
  • yes I have said it in my answer as well. However the main idea behind my answer to your question is reproducible based on these examples. – ibrahim tanyalcin Apr 25 '18 at 10:16
1

That' really hard to tell for sure why the dev tools don't notice the reflow in this case, it might be because since there has been no changes in the layout, the reflow algorithm got short-circuited and the dev-tools omitted it. It could also be something else...

But the reflow happens, at least if it has to.

To test reflow, the best way IMO is to trigger something that needs it, and CSS transitions are such a thing.

var input = document.querySelector('input');
// test our logic without anything that should trigger a reflow 
noreflow.onclick = function() {
  // from 20px in CSS
  test.style.transition = 'none';
  test.style.width = '100px'; // should be ignored
  test.style.transition = 'width 1s linear';
  test.style.width = '20px';
  // should go from 20px to 20px => no transition
};
// test with manually triggered reflow
reflow.onclick = function() {
  // from 20px in CSS
  test.style.transition = 'none';
  test.style.width = '100px';
  input.focus(); // reflow
  // now should be computed as 100px
  test.style.transition = 'width 1s linear';
  test.style.width = '20px';
  // now that should move
}
#test {
  width: 20px;
  height: 20px;
  background: red;
}
<input type="text"><br>
<button id="noreflow">no reflow</button>
<button id="reflow">reflow</button>
<div id="test"></div>
Kaiido
  • 123,334
  • 13
  • 219
  • 285