1

At first, i was trying to figuring how to implement a "line-by-line shows" effect, as far as i know, chrome will reflow the page when there is a dom insert action, so i just think the following code could work:

Click the "Click Me" span tag, there will be some lines of "-.-" span occur one-by-one.

<!DOCTYPE html>
<html lang="en">
  <body>
    <div>
      <span id="clickme">Click Me</span>
    </div>
    <div id="container"></div>
    <script>
      const container = document.getElementById("container");
      const clickme = document.getElementById("clickme");

      clickme.addEventListener("click", () => {
        for (let i = 0; i < 10; ++i) {
          const child = document.createElement("div");

          for (let j = 0; j < 20; ++j) {
            const span = document.createElement("span");
            span.textContent = "-.- ";
            child.appendChild(span);
          }

          container.appendChild(child);
        }
      });
    </script>
  </body>
</html>

Unfortunately, chrome seems to apply all the dom change in one batch, so i used performance recording to check if it really does, and here the result just verified the guess:

enter image description here

I was really confused by this result, so i tried again to add a breakpoint at the for-loop entry to see what happened in this process, and the result is completely different from the normal process:

Here i uploaded two screenshots because the rest ones is just the same render result (line-by-line).

enter image description here

enter image description here

So my question is why breakpoints change the behavior of chrome's rendering process, and is there any way to implement the "line-by-line shows" effect only with JavaScript?

(My original purpose is a little more complicated than "line-by-line", and cannot be solved by CSS transition)

韩沅锡
  • 21
  • 2
  • Use `setTimeout(0, ..)` – mousetail Sep 01 '22 at 07:56
  • "*chrome will reflow the page when there is a dom insert action*" - no, it'll reflow the page only when it needs to, which is when rendering the results of your click handler to the screen. You'll need to introduce your own delay at each step if you want an animation. – Bergi Sep 01 '22 at 08:18
  • @mousetail Thank you for the solution, actually i've tried setTimeout(callback, 0) in my previous practice, and it works well in chrome tabs, but it breaks when used in an iframe tag (sorry about not mentioning this situation). I tried to set some longer delay value in iframe, sometimes 10ms works, while sometimes not, so i set the delay to 20ms now, so far it works as expected, but i'm concerned about the risk T.T, cause i don't know the root cause. Do you have any idea about this iframe problem ^_^? – 韩沅锡 Sep 01 '22 at 09:13
  • @Bergi "only when it needs to" is more cogent, may I ask the specific strategy that chrome uses ? Maybe there is a specifition about this ? – 韩沅锡 Sep 01 '22 at 09:21
  • @韩沅锡 More specifically, "when the code has finished running and there have been any changes to the DOM". Or possibly earlier if required by code. See https://stackoverflow.com/questions/510213/when-does-reflow-happen-in-a-dom-environment – Bergi Sep 01 '22 at 12:10

2 Answers2

2

When Chrome hits a breakpoint it will enter the special "spin the event loop" algorithm (or a variation of it at least).
This algorithm allows to pause the currently active task and let the event loop process other duties, such as updating the rendering. Note though that no other scripts will run while in this state. So you will have CSS animations update, new DOM changes painted to screen, but you won't e.g have scroll events, or requestAnimationFrame callbacks fire.


Also, note that the browser does not trigger a reflow at each DOM insertion. It will try to trigger a reflow only at the last moment when required (i.e right before painting in most cases). Though some Web-APIs can force such a reflow (e.g all the position getters like HTMLElement#offsetTop.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Learned a lot. For the detail "Though some Web-APIs can force such a reflow", actually i've tried to insert code like `log(scrollHeight)` right after `appendChild` which i thought could trigger force render, but result was the same (like first screenshot in the post), the only different thing was that performance-recording showed a longer scripting time. I can distantly understand that chrome calculated all layout info when i called "scrollHeight" , but somehow didn't apply the result on screen, so is there any way to eliminate the gap between layout and render ? – 韩沅锡 Sep 01 '22 at 12:00
  • 2
    @韩沅锡 It may have done [a reflow but not a repaint](https://stackoverflow.com/q/2549296/1048572). No, there's no way to force that. The solution is to do the animations properly, i.e. asynchronously; and if you need to align them with the rendering frame rate then use `requestAnimationFrame`. – Bergi Sep 01 '22 at 12:13
  • @Bergi Thanks again, i think i should learn more about the "reflow/repaint" process, and i'll try `requestAnimationFrame` later in my origin project to check if it works. – 韩沅锡 Sep 02 '22 at 02:28
  • 1
    @韩沅锡 if I may, I invite you to read this answer of mine which I believe is both simple enough and still quite a correct overview of the situation: https://stackoverflow.com/a/47343090/3702797 – Kaiido Sep 02 '22 at 02:45
  • @Kaiido Thanks for inviting, the linked question really covered my situation, and your explain is clear enough. As i commented above, I'll try to do some more reading about browsers' "DOM update / reflow / repaint" stratrgy, may i ask for some related books or links ? ^_^ – 韩沅锡 Sep 02 '22 at 05:51
1

You can use setInterval as shown in your amended code below. This repeatedly interrupts the processing thread, those allowing the browser to refresh the display. When the number of desired lines is reached, you should invoke clearInterval.

<!DOCTYPE html>
<html lang="en">
  <body>
    <div>
      <span id="clickme">Click Me</span>
    </div>
    <div id="container"></div>
    <script>
      const container = document.getElementById("container");
      const clickme = document.getElementById("clickme");

      clickme.addEventListener("click", () => {         
        let lines = 0;
        const intervalID = setInterval(() => {
          const child = document.createElement("div");
          for (let j = 0; j < 20; ++j) {
            const span = document.createElement("span");
            span.textContent = "-.- ";
            child.appendChild(span);
          }
          container.appendChild(child);    
          if (++lines >= 10) {
            clearInterval(intervalID);
          }
        }, 500);               
      });
    </script>
  </body>
</html>
uminder
  • 23,831
  • 5
  • 37
  • 72
  • Thanks for the reply, setInterval can solve the problem indeed, but I was wondering why breakpoints cause a different rendering behavior, is there any clue for this question? – 韩沅锡 Sep 01 '22 at 09:16