How does overflow: hidden; & border-radius on a container cause massive slowdowns to “Paint / Render Layer” within container, only on IE?

I've been having a lot of trouble with bad performance on IE (all versions including IE11) in a HTML/SVG javascript-powered widget, only when the widget is hosted on a certain page.

After identifying that the main cause of the slowdown was paint / render layer redraws, and exhausting the information about these I could get out of IE Developer Tools, I resorted to trial and error turning off ancestor classes one at a time until the performance improved; then, on identifying the class, turning off style rules one at a time.

My entire problem seems to come down to a single overflow: hidden; rule on an ancestor several divs up the tree.

The difference it makes is incredible: with overflow: hidden; up the tree, a simple user interaction (highlighting an SVG path, generating a HTML text label, showing the label and positioning it relative to the SVG path and container) maxes out the processor, reduces the UI frame rate to zero and freezes everything dead for between 1,000 and 4,000 miliseconds per interaction. Without overflow: hidden; on the ancestor, it completes in tens of miliseconds and the frame rate never drops below half (non-IE browsers are the same regardless of overflow: hidden;).

  • Here's the profile with overflow: hidden; on the ancestor, profiling both on and off interactions, filtered to paint events:

    enter image description here

  • Here's the profile without overflow: hidden; on the ancestor, profiling both on and off interactions, filtered to paint events. The only change was ticking or unticking the tick box next to the overflow: hidden; style in the DOM inspector, and it doesn't matter in which order I do the tests:

    enter image description here

I don't want to just override this overflow: hidden; as a sticky plaster and say job done, not understanding how this happens and risking the problem re-occurring with other seemingly trivial CSS changes. I'd much prefer to understand why overflow: hidden; makes such a difference and address that in a robust way which works regardless of the overflow rule applied.

Sadly I can't post a full demo but here's a summary of the relevant part of the DOM structure with comments on the layout-related styles:

  <div class="responsive-grid">

  <!-- ...lots of nested divs that simply inherit styles, I can't change this aspect of the Drupal layout -->

    <div id="panel-5" class="col-12"> <!-- width: 100%; float: left -->
    <!-- this is the first element IE looks at for offsetWidth when doing the changes below -->

    <!-- ...a few more nested divs without layout-changing styles -->

      <div class="panel"> <!-- overflow: hidden; clear: both; border: 1px; -->
      <!-- this is the element where removing the overflow: hidden changes everything -->
      <!-- I'm not sure what clear:both is for here, since no siblings. Seems redundant -->

      <!-- ...a few more nested divs with no style rules, some contain <p>s <h2>s etc... -->

        <div class="container"> <!-- position: relative; -->
          <div class="sub-column col-8"> <!-- width: 66%; display: inline-block -->
            <div class="square"> <!-- width: 100%; padding-bottom: 100%; position: relative -->
              <svg viewbox="0 0 500 500" preserveAspectRatio="XMinYMin meet" ...>
              <!-- svg position: absolute; width:100%; height: 100% -->
                Many paths here

              <div class="label"> <!-- fixed width in pixels; position: absolute -->
                Some text here
              </div>
            </div>
          </div>
          <div class="sub-column col-4"> <!-- width: 33%; display: inline-block -->
             <div class="sidebar">
                Many interactive controls here

              <!-- .square, svg andd .sidebar contain the only elements that are updated -->
            </div>
          </div>
        </div>

        <!-- some more ordinary position: static text containers -->
      </div>
    </div>
  </div>

What could possibly be happening here, and is there any way it can be prevented without removing / forbidding overflow: hidden; on the ancestor element?


I've seen How to avoid the performance cost of overflow:hidden? but both questions and answers appear to be specific to HTML tables and an old Webkit bug since fixed.

They also seem specific to cases where content clipped by the overflow gets needlessly painted; a quirk of my case is that the overflow: hidden; isn't actually clipping much (if anything) on this page (but I can't just remove it because it's part of template affecting hundreds of other pages, where it does have an effect).


Update: The plot thickens. I've managed to replicate the problem with my widget in a simpler HTML structure, and discovered that the problem only occurs if both overflow: hidden; and border-radius (in my case, 3px) are set on the same container. With either one but not the other, the problem disappears.

  • Here's a sample with overflow: hidden; but not border-radius. Maybe a tiny bit slower than above, but the difference is trivial:

    enter image description here

  • Here's a sample with overflow: hidden; and border-radius from the same simplified structure:

    enter image description here

Answers:

Answer

After a lot more testing I think I'm starting to understand what's going on here. This is purely based on observation though, so I'd still be keen for a more authoritative answer if anyone has one.


What causes it?

It seems to happen only if all these are true:

  • The browser is IE (any version)
  • An ancestor element we'll call X contains both overflow: hidden and border-radius (or -ms-border-radius). In my testing, it doesn't happen if different ancestors in the same branch have these styles.
  • There are many complex elements such as SVG paths or %-width divs that are in a DOM branch where they, or an element between them and element X, has position: absolute; or position: relative;

The problem also seems to be more pronounced in proportion to the number of elements affected by position: absolute;/relative and their complexity. In cases where there were SVG paths in a responsively scaling %-width SVG container with % padding-bottom for fixed aspect ratio, for example, the problem was very pronounced; if this branch was given position: static but another branch had %-width divs with a position: absolute; ancestor, then the problem was still observable compared to removing one of overflow: hidden; or border-radius, but was much less severe.


But why?

I don't have any definitive answer, but I've got a plausible theory that seems to fit the facts. Ironically, it'd be a backfiring attempt at performance optimisation by IE.

I noticed that offsetWidth calculations for elements between X and the paths were going all the way to the paths, which made no sense to me and prompted a related question because paths within an SVG container surely can't influence the layout outside the container.

I also noticed while researching this that other browsers - particularly, an older version of Chrome - seemed to have a different problem: elements that should have been hidden were being rendered, causing slowdowns.

Putting these together, I think there's a plausible explanation for what's going on here. If true, ironically, my performance woes were being caused by a backfiring attempt by IE to optimise performance and avoid problems like the above-linked now-fixed Chrome issue.

If this theory is true, something like this would be happening within IE:

  • It sees the overflow: hidden; and concludes that it can improve performance by identifying elements that are wholly outside the element's bounds before doing redraw / reflow / paint etc events on them.
  • It sees the position: absolute; and position: relative; further down the DOM and concludes that these and their children might potentially be wholly outside the container and could potentially be disregarded like this
  • During events such as redraw and reflow, it first cycles through all these elements and checks they're not wholly outside the bounds. This still happens even without border-radius, but in such cases it's trivial and takes miliseconds.
  • ...however, IE notices the border-radius and concludes that the shape of this overflow: hidden element is not a rectangle and therefore a more complex calculation is needed to determine what is out of bounds
  • Presumably, it also concludes that SVG paths need to be compared based on their actual shapes not their simple bounding box co-ordinates, since this is no longer a simple matter of comparing parallel rectangles.
  • Suddenly, the calculation to test if an element can be disregarded during paint / redraw / reflow has become complex. If repeated hundreds of times for hundreds of elements, it massively worsens performance on every triggering event.

How to fix it?

If you can, simply move the overflow: hidden or border-radius to a child element so they're not both on the same element. Job done.

For me, however, I'm making a plugin that needs to be capable of being dropped in anywhere, and won't have any control over the pages its deployed on. I'm not aware of any way I could force IE to turn this behaviour off.

The best approach I can think of, is to assume that the border-radius style is non-essential for aesthetics and that the overflow: hidden; might be essential for structure, and so, if the browser is IE, look up the ancestor tree and remove border-radius from any element that has both it and overflow: hidden;.

My application already uses jQuery, so this test looks something like this:

if( isAnyIE() ) {
  $container.parentsUntil("body").filter(function(){

    var $this = $(this),
      overflow = $this.css('overflow');
    return ( overflow === 'hidden' && hasBorderRadius( $this ) );

  }).addClass( 'remove-border-radius' ); 
}

function hasBorderRadius( $element ){
  function getNum( style ){
    return parseFloat( $element.css( 'border-'+style+'-radius' ) ) || 0;
  }
  var number = 0;

  number += getNum( 'top-left' );
  number += getNum( 'bottom-left' );
  number += getNum( 'top-right' );
  number += getNum( 'bottom-right' );

  $element = null;

  return !!number;
}

function isAnyIE(){
  // isIE(): use conditional comments and classes, see https://stackoverflow.com/a/18615772/568458
  // isIE10: use user agent like navigator.appVersion.indexOf("MSIE 10") !== -1;
  // isIE11: use user agent like !!navigator.userAgent.match(/Trident.*rv[ :]*11\./);

  return isIE11() || isIE10() || isIE();
}

With CSS like:

.remove-border-radius {
  border-radius: 0 0 0 0 !important;
  -ms-border-radius: 0 0 0 0 !important;
}
Answer

The problem was brought to Microsoft's attention in this bug report written by Joppe Kroon in February 2014:

[IE 9] [IE 11] div's with a border-radius and overflow other than visible resize slow when filled with div's with position relative

Redrawing of the page performs very badly when there are a lot of large DIV elements with a combination of position "absolute", "fixed" or "relative", an overflow other than "visible" and a border-radius. This can be clearly seen when resizing the window or scrolling. Individual paint events can take up to 1.5 seconds to complete, causing the page to become unresponsive.

This performance issue shows up in IE 11 and IE 9 but not in IE 10.


The only significant reply from Microsoft was published in April 2016. It does not provide a solution or a workaround:

[...] We have looked at the issue you reported very closely and found it closely resembles similar reported issues. None of our usual resolutions (“BY DESIGN”, “DUPLICATE”, “FIXED”, etc.) exactly match this situation therefore we are marking it as “WON’T FIX” for lack of anything better but what we really mean is we’ll be dealing with it in an aggregated kind of way rather than looking at this exact incident in this exact situation. [...]

All the best, The MS Edge Team

The status of the bug is : Closed as Won't fix

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.