12

I am having a problem with high CLS (Content Layout Shift) while using Bootstrap (4.5) grid for two column layout with column order change.

CLS is a Core Web Vital metric. Basically Google sees a problem when webpage's parts are moving when the page is loading. Supposedly this metric is to affect SEO.

On high resolutions my layout consists of two columns. Main content on the right and sidebar on the left. On lower resolutions sidebar content is pushed down below main content. So HTML looks like this:

<div class="container-fluid">
    <div class="row">
        <div class="col-lg-8 order-lg-2">
        </div>
        <div class="col-lg-4 order-lg-1">
        </div> 
    </div> 
</div>

The problem is that for brief moment while the page renders on desktops, the main content appears on the left side, then milliseconds later it shifts to the right place on the right. With simple pages (with simple DOM or no external resources) the shift is not detectable. 

I've prepared an example of such page. (The source code is on github). To measure CLS I am using Lighthouse in Chrome. In my case when I refresh the page I can see columns moving and Lighthouse informs me of CLS value of 0.326. The result might depend on rendering resources so you might get something different. But it seems Google Page Insight gives similar result:

enter image description here

Anyway, is there a way to avoid such shift while the page renders? 

szymond
  • 1,311
  • 2
  • 19
  • 41
  • 1
    I don't have an answer for why this happens (it appears to be something to do with when compositor actions occur arranging the layers, it must be a "late action" if you switch the order around) but the solution is to serve the HTML in the correct order and not rely on CSS to change the order around. If that is a feasible option for you then I would do that and it will correct the issue, but obviously this may not work if the order is correct on other screen sizes. Interestingly if I use CPU slowdown this doesn't occur, it appears to be a multi-threading issue at a best guess. – GrahamTheDev Sep 13 '20 at 10:47
  • Well, the idea is to have main content provided first for single column layout (for mobile devices). So simply changing ordering makes new problems. – szymond Sep 13 '20 at 12:05
  • yes that is what I suspected, I am intrigued does the same problem occur in a "real world" scenario where you have images, more CSS, JavaScript etc. on the page as the extra computation time may remedy the issue. If the problem still occurs the only thing you could do is have a white overlay display until page load, which would be a hack but would at least solve the CLS issue. – GrahamTheDev Sep 13 '20 at 12:08
  • Also I wonder if the same issue would occur if it changed from flex to grid model for layout....interesting problem like I said as CSS is inlined and there are no fonts loaded (the things I would normally look for with CLS issues) – GrahamTheDev Sep 13 '20 at 12:11
  • The problem occured on real website. I've striped it so nobody says it's something else fault. Actually stripping resources made problem less detectable. You mention making overlay, but I think you can't easily detect and remove it in the right moment. – szymond Sep 13 '20 at 13:56
  • A simple fix is to change the placement of the two main div's e.g. `
    ` i think this is not visible any more as seen in this example fiddle https://jsfiddle.net/h8c419jv/.
    –  Sep 20 '20 at 11:21

2 Answers2

9

It seems the problem is more Chrome related then flexbox or bootstrap. It turned out that the problem is caused by premature renderation. Chrome's parser "yields" (so it triggers rendering): 

  • after reading 65536 bytes of data, 
  • after encountering <script> tag after reading 50 "tokens" (which I think are basically html tags).

The example I provided shows the first case (but actually my real website experiences CLS because the second one). Both of those cases have "bugs" related to them submitted: 1130290 and 1041006

So the answer to the problem is hoping that the "bugs" will get resolved. In the meantime depending on actual cause you can limit file size or remove <script> tags.     

szymond
  • 1,311
  • 2
  • 19
  • 41
  • 1
    The problem was resolved for me by moving all of my page's various ` – joepetrakovich Feb 03 '21 at 19:26
  • This just got me. (I'm not using Bootstrap). Pagespeed Insights was reporting some egregious Cumulative Layout Shift on a seemingly innocuous part of the page. Chrome Dev Tools Performance tab called out the same thing on the same DOM element. But there was nothing that looked liked it should cause CLS much less such a nasty one. Turns out I had a tiny little inline script tag in that area (all it did was subscribe to some page event). I moved it to the bottom of the page instead of smack in the middle of it and now no more CLS problems are reported. – BowserKingKoopa Mar 22 '21 at 18:40
  • Chromium is changing parsing behavior to perform better on cases like this. The new algorithm is too large to fit in this margin, but essentially it will increase the 65k chunks to a much larger value, and will process more tokens per chunk before yielding. It will also process more tokens than before after inline scripts. See http://crbug.com/1041006 for additional details. – Mason Freed Nov 03 '21 at 19:24
0

Short Answer

Minimise the HTML and the problem seems to go away.

Longer Answer

I did a bit of digging, this isn't a complete "this is exactly what happens" answer but I got enough of an idea to come up with the above solution and to roughly explain my reasoning. I am hoping someone can expand upon the gaps in my knowledge.

So what made me come up with the above solution?

After profiling the page load I notice that there were 2 HTML parse tasks being created.

One dealt with lines 1-770 of the HTML and the other dealt with lines 771 to the end of the document.

Because of this the page appears to render the first 770 lines and then recalculates the layout on the second HTML parse task because you have swapped the order (and the .col-lg-4 column is in the second parsing pass HTML).

You won't see this on a "normal" page as if the page is rendered in DOM order layouts will be correct anyway and the second HTML parsing pass will just add more detail to the layout.

As it appeared to be pretty consistent where it cut the page off I removed all line breaks and white space. My theory being that whatever algorithm is deciding where to split the HTML up is using line number as part of that calculation.

By reducing the effective lines to 15 or so I was hoping to make that algorithm only parse the HTML in one pass.

It actually still does it in 2 but the last pass is the closing </html> tag only so doesn't matter. The result of this is when the parsed HTML is combined with the CSSOM it can calculate the layout correctly.

a bit of a hack but it should work up to certain page depths.

please note - if I doubled the DOM node count this workaround did not work again. If I changed the length of each list item (i.e. put lorem ipsum in) but didn't change the structure it did not make a difference. So it appears to be some combination of number of DOM elements and line number that decides when the HTML parser should stop its first pass.

A possible solution

Go back to old layout models. If you use float:left and float:right it should work. I think this specific issue is a combination of page complexity (number of DOM nodes) and using flexbox.

With flexbox being slightly slower than old layout models and sometimes having to use multiple passes (old layout models are single pass) I would imagine this issue would not persist with the above recommendation.

Where I found out about multiple layout passes in certain scenarios

GrahamTheDev
  • 22,724
  • 2
  • 32
  • 64
  • 1
    Just to clarify, the parser doesn't consider line numbers at all. It mainly uses token **counts** plus an overall maximum number of bytes to determine when to yield and perform rendering. – Mason Freed Nov 03 '21 at 19:26
  • @MasonFreed do you happen to have any resources that explain this in more detail as it is something I would like to understand better. Thanks. – GrahamTheDev Nov 04 '21 at 07:33
  • Unfortunately, no, I don't have a good reference for this, other than the code itself. You can find that here: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/parser/html_document_parser.cc;l=1015;drc=d961ab1ddcfee53ec3b41115218a78b35e2bcb17 – Mason Freed Nov 18 '21 at 18:59