233

Every once in a while, Chrome will render perfectly valid HTML/CSS incorrectly or not at all. Digging in through the DOM inspector is often enough to get it to realize the error of its ways and redraw correctly, so it's provably the case that the markup is good. This happens frequently (and predictably) enough in a project I'm working on that I've put code in place to force a redraw in certain circumstances.

This works in most browser/os combinations:

    el.style.cssText += ';-webkit-transform:rotateZ(0deg)'
    el.offsetHeight
    el.style.cssText += ';-webkit-transform:none'

As in, tweak some unused CSS property, then ask for some information that forces a redraw, then untweak the property. Unfortunately, the bright team behind Chrome for the Mac seem to have found a way to get that offsetHeight without redrawing. Thus killing an otherwise useful hack.

Thus far, the best I've come up with to get the same effect on Chrome/Mac is this piece of ugliness:

    $(el).css("border", "solid 1px transparent");
    setTimeout(function()
    {
        $(el).css("border", "solid 0px transparent");
    }, 1000);

As in, actually force the element to jump a bit, then chill a second and jump it back. Making it worse, if you drop that timeout below 500ms (to where it would be less noticeable), it often won't have the desired effect, since the browser won't get around to redrawing before it goes back to its original state.

Anybody care to offer a better version of this redraw/refresh hack (preferably based on the first example above) that works on Chrome/Mac?

Minko Gechev
  • 23,174
  • 7
  • 57
  • 66
Jason Kester
  • 5,713
  • 9
  • 33
  • 40
  • I ran into the same problem a few minutes ago. I change the element for a div (it was a span) and now the browser redraws the changes correctly. I know this is a little bit old but this can help some bros out there I think. – Andrés Torres Jun 25 '15 at 19:32
  • 2
    Please see the answer below relating to opacity 0.99 - the best answer here - but not easy to find as it is so deep on the page. – Grouchal Jun 29 '15 at 09:44
  • 1
    This happens on Linux too :-( – schlingel Sep 09 '15 at 08:32
  • 2
    You can replace the timeout with a requestAnimationFrame, in which case you'll achieve the same thing but with a lag of 16ms instead of 1000ms. – trusktr Jul 05 '16 at 17:45
  • 3
    Please [file a Chromium issue](http://crbug.com/new) for the invalid rendering. It appears that no-one has done so, despite this being 4 years ago. – Bardi Harborow Oct 20 '16 at 12:48
  • This question should at least have a jQuery tag. – Makan Tayebi Jun 05 '17 at 08:27
  • I do not have this issue in Chromium 65, but I do have this issue with Chrome 64, so this issue might already be fixed in latest Chrome – conradkleinespel Apr 09 '18 at 10:18
  • hello guys, our project is using somthing like below to force redraw: ``` disp = coreArea.style.display; coreArea.style.display = 'none'; coreArea.offsetHeight; coreArea.style.display = disp; ``` this code is in a event handler. when I run it in chrome on ios, I got an issue. The event behind the event include the force redraw logic never trigger. Does any one know why? – Y.L Jan 09 '20 at 10:28

24 Answers24

186

Not sure exactly what you're trying to achieve but this is a method I have used in the past with success to force the browser to redraw, maybe it will work for you.

// in jquery
$('#parentOfElementToBeRedrawn').hide().show(0);

// in plain js
document.getElementById('parentOfElementToBeRedrawn').style.display = 'none';
document.getElementById('parentOfElementToBeRedrawn').style.display = 'block';

If this simple redraw doesn't work you can try this one. It inserts an empty text node into the element which guarantees a redraw.

var forceRedraw = function(element){

    if (!element) { return; }

    var n = document.createTextNode(' ');
    var disp = element.style.display;  // don't worry about previous display style

    element.appendChild(n);
    element.style.display = 'none';

    setTimeout(function(){
        element.style.display = disp;
        n.parentNode.removeChild(n);
    },20); // you can play with this timeout to make it as short as possible
}

EDIT: In response to Šime Vidas what we are achieving here would be a forced reflow. You can find out more from the master himself http://paulirish.com/2011/dom-html5-css3-performance/

Juank
  • 5,632
  • 1
  • 24
  • 27
  • 3
    Afaik, it is not possible to redraw only a part of the viewport. The browser always redraws the entire web-page. – Šime Vidas Jan 12 '12 at 19:13
  • No, neither hiding/showing it nor removing it/replacing it forces a redraw on Chrome/Mac. And, of course, on other browsers there is a much less intrusive way to achieve the redraw. – Jason Kester Jan 12 '12 at 19:57
  • 39
    instead of $('#parentOfElementToBeRedrawn').hide().show(); I needed .hide.show(0) to work properly in Chrome – l.poellabauer Jul 25 '12 at 08:28
  • 1
    @l.poellabauer same here - this makes no sense... but thanks. How the heck did you find out??? :) – JohnIdol Aug 01 '12 at 11:49
  • 6
    FYI, the relevant area I was working on was an infinite scroll list. It was a bit unworkable to hide/show the entire UL, so I played around and found that if you do `.hide().show(0)` on *any* element (I chose the page footer) it should refresh the page. – treeface Feb 21 '13 at 22:05
  • This worked for me in chrome. Seeing weird drag and drop artifacts occasionally. This should be the accepted answer. – mtyson Feb 14 '14 at 17:18
  • 1
    It is possible to redraw only part of the viewport. There is no API to do so, but the browser can choose to do so. The viewport is painted in tiles to begin with. And composite layers are painted independently. – morewry Jan 15 '15 at 20:02
  • `$(this).parent().hide().show(0)` worked for me, as I wasn't able to hard-code the particular parent element. – kbro Dec 12 '15 at 05:30
  • Setting a element to `display="block"` regardless of it's original display type is probably no good idea. @aviomaksim answer provides a better vanilla JavaScript solution. – Rico Ocepek Aug 03 '16 at 17:35
  • In the process of using this method, I discovered a bug in Google Chrome--if a system dialogue (e.g. alert, prompt) is up right after the forced repaint, it won't show up and the renderer gets stuck. The conditions it happened under are very specific so I won't claim that this will always happen, but in my case it made prototyping my software a pain. – Braden Best Jul 04 '20 at 22:40
64

None of the above answers worked for me. I did notice that resizing my window did cause a redraw. So this did it for me:

$(window).trigger('resize');
Vincent Sels
  • 2,362
  • 1
  • 21
  • 29
  • 12
    hint: this will redraw your complete window, which is performance costly and probably not what OP wants – hereandnow78 Oct 30 '14 at 08:18
  • 7
    Resizing the window always triggers a reflow but the code `$(window).trigger('resize')` doesn't, it only throws the 'resize' event that triggers the related handler if it is assigned. – guari Jul 15 '15 at 19:20
  • 1
    You save me a whole week! My issue is after setting , the page zoomed to my expected level, but the page is freezed and can not scroll!. Your answer solved this issue immediately – user1143669 Jun 08 '16 at 12:28
36

We recently encountered this and discovered that promoting the affected element to a composite layer with translateZ fixed the issue without needing extra javascript.

.willnotrender { 
   transform: translateZ(0); 
}

As these painting issues show up mostly in Webkit/Blink, and this fix mostly targets Webkit/Blink, it's preferable in some cases. Especially since many JavaScript solutions cause a reflow and repaint, not just a repaint.

morewry
  • 3,940
  • 2
  • 30
  • 35
  • 1
    this worked fine for me when the issue was a removed HTML element being drawn on a canvas even though canvas was continuously animated. – Knaģis Feb 12 '16 at 15:29
  • This fixed a layout issue I was having with `neon-animated-pages`. Thank you :+1: – coffeesaga Feb 19 '17 at 02:24
  • 1
    This worked. Safari was leaving elements that where in a div set to display none, visible and not select-able. Resizing the window made them disappear. Adding this transform caused the "artifacts" to disappear as expected. – Jeff Mattson Sep 09 '19 at 17:57
  • 1
    This is great thanks! It worked. It's 2020, and this hack is from January '15. Any idea why this issue has not been fixed? – Kai Sep 25 '20 at 23:09
  • `z-index: 0;`, seems to work as well, which causes less z layering disruption (in my use case) than the `transform` in this answer did. – electrovir Mar 15 '21 at 23:22
27

This solution without timeouts! Real force redraw! For Android and iOS.

var forceRedraw = function(element){
  var disp = element.style.display;
  element.style.display = 'none';
  var trick = element.offsetHeight;
  element.style.display = disp;
};
aviomaksim
  • 2,289
  • 20
  • 14
  • 1
    This does not work for me on Chrome nor FF on Linux. However simply accessing element.offsetHeight (without modifying the display property) **does** work. It's as is the browser would skip the offsetHeight calculation when the element is not visible. – Grodriguez Oct 22 '14 at 10:09
  • This solution works perfectly for working around a bug I encountered in the chrome-based browser used in "overwolf". I was trying to figure out how to do this and you saved me the effort. – nacitar sevaht Jan 30 '15 at 05:21
13

This works for me. Kudos go here.

jQuery.fn.redraw = function() {
    return this.hide(0, function() {
        $(this).show();
    });
};

$(el).redraw();
zupa
  • 11,127
  • 5
  • 34
  • 38
  • 1
    But it left some 'remains' like 'display: block' etc that sometimes can destroy page structure. – Adam Pietrasiak Jul 21 '14 at 09:14
  • This should essentially be the same as `$().hide().show(0)` – Mahn Nov 16 '14 at 14:06
  • @Mahn did you try it? In my guess .hide() is non-blocking thus it would skip the re-rendering run in the browser aka the whole point of the method. (And if I remember correctly, I tested it back then.) – zupa Nov 17 '14 at 09:45
  • 2
    @zupa it's tested and working on Chrome 38. The trick is that `show()` is different from `show(0)` in that by specifying a duration in the latter, it's triggered by jQuery animation methods in a timeout rather that immediately which gives time to render in the same way that `hide(0, function() { $(this).show(); })` does. – Mahn Nov 17 '14 at 11:58
  • @Mahn okay nice catch then. I'm definitely for your solution if it works cross platform and isn't a recent jQuery feature. As I don't have the time to test it, I'll add it as 'may work'. You are welcome to edit the answer if you think the above apply. – zupa Nov 17 '14 at 17:06
  • @Mahn let me elaborate. Defining the .redraw() jQuery method is useful for lowering the cognitive load. Using .hide().show(0) directly would require more documentation, so it's rather only about the implementation details of the .redraw() method. – zupa Nov 17 '14 at 17:11
9

Hiding an element and then showing it again within a setTimeout of 0 will force a redraw.

$('#page').hide();
setTimeout(function() {
    $('#page').show();
}, 0);
Brady Emerson
  • 4,539
  • 1
  • 12
  • 14
  • 1
    This one worked for a Chrome bug where SVG filters sometimes don't repaint, https://bugs.chromium.org/p/chromium/issues/detail?id=231560. The timeout was important. – Jason D May 02 '17 at 03:43
  • This is the one that worked for me while trying to force Chrome to recalculate values for `myElement.getBoundingClientRect()`, which were being cached, likely for optimization / performance purposes. I simply added a zero millisecond timeout for the function that adds the new element to the DOM and gets the new values after the old element with the previous (cached) values was removed from the DOM. – Chunky Chunk Jul 17 '17 at 19:18
6

This seems to do the trick for me. Plus, it really doesn't show at all.

$(el).css("opacity", .99);
setTimeout(function(){
   $(el).css("opacity", 1);
},20);
Xavier Follet
  • 152
  • 2
  • 5
  • It worked for me in Chromium Version 60.0.3112.113 regarding this bug: [Issue 727076](https://bugs.chromium.org/p/chromium/issues/detail?id=727076#c19). Efficient and subtle; many thanks. – Lonnie Best Sep 08 '17 at 13:41
  • @wiktus239 Maybe 20 millis is too fast and the browser doesn't have time to change to .99, before time to change back to 1.0? — This opacity trick works for me anyway, to make contents in an iframe visible in iPhone iOS 12 Safari, and I toggle every 1 000 millis. That's also what's in the linked issue 727076 in the comment above (1000 millis). – KajMagnus Dec 31 '18 at 05:48
4

call window.getComputedStyle() should force a reflow

Sebas
  • 19,640
  • 9
  • 48
  • 103
qlqllu
  • 211
  • 2
  • 9
  • 2
    didn't work for me, but that's probably because I needed the `offsetHeight` property which is not css. – Sebas Jun 07 '15 at 21:38
3

I ran into this challenge today in OSX El Capitan with Chrome v51. The page in question worked fine in Safari. I tried nearly every suggestion on this page - none worked right - all had side-effects... I ended up implementing the code below - super simple - no side-effects (still works as before in Safari).

Solution: Toggle a class on the problematic element as needed. Each toggle will force a redraw. (I used jQuery for convenience, but vanilla JavaScript should be no problem...)

jQuery Class Toggle

$('.slide.force').toggleClass('force-redraw');

CSS Class

.force-redraw::before { content: "" }

And that's it...

NOTE: You have to run the snippet below "Full Page" in order to see the effect.

$(window).resize(function() {
  $('.slide.force').toggleClass('force-redraw');
});
.force-redraw::before {
  content: "";
}
html,
body {
  height: 100%;
  width: 100%;
  overflow: hidden;
}
.slide-container {
  width: 100%;
  height: 100%;
  overflow-x: scroll;
  overflow-y: hidden;
  white-space: nowrap;
  padding-left: 10%;
  padding-right: 5%;
}
.slide {
  position: relative;
  display: inline-block;
  height: 30%;
  border: 1px solid green;
}
.slide-sizer {
  height: 160%;
  pointer-events: none;
  //border: 1px solid red;

}
.slide-contents {
  position: absolute;
  top: 10%;
  left: 10%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<p>
  This sample code is a simple style-based solution to maintain aspect ratio of an element based on a dynamic height.  As you increase and decrease the window height, the elements should follow and the width should follow in turn to maintain the aspect ratio.  You will notice that in Chrome on OSX (at least), the "Not Forced" element does not maintain a proper ratio.
</p>
<div class="slide-container">
  <div class="slide">
    <img class="slide-sizer" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">
    <div class="slide-contents">
      Not Forced
    </div>
  </div>
  <div class="slide force">
    <img class="slide-sizer" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">
    <div class="slide-contents">
      Forced
    </div>
  </div>
</div>
Ben Feely
  • 101
  • 1
  • 3
2

If you want to preserve the styles declared into your stylesheets, better to use something like:

jQuery.fn.redraw = function() {
    this.css('display', 'none'); 
    var temp = this[0].offsetHeight;
    this.css('display', '');
    temp = this[0].offsetHeight;
};

$('.layer-to-repaint').redraw();

NB: with latest webkit/blink, storing the value of offsetHeight is mandatory in order to trigger the repaint, otherwise the VM (for optimizations purposes) will probably skip that instruction.

Update: added the second offsetHeight reading, it is necessary to prevent browser from queueing/caching a following CSS property/class change with the restore of the display value (this way a CSS transition that can follow should be rendered)

guari
  • 3,496
  • 3
  • 26
  • 23
2

2020: Lighter and stronger

The previous solutions don't work anymore for me.

I guess browsers optimize the drawing process by detecting more and more "useless" changes.

This solution makes the browser draw a clone to replace the original element. It works and is probably more sustainable:

const element = document.querySelector('selector');
if (element ) {
  const clone = element.cloneNode(true);
  element.replaceWith(clone);
}

tested on Chrome 80 / Edge 80 / Firefox 75

J.P. Duvet
  • 460
  • 4
  • 12
1

CSS only. This works for situations where a child element is removed or added. In these situations, borders and rounded corners can leave artifacts.

el:after { content: " "; }
el:before { content: " "; }
rm.rf.etc
  • 684
  • 1
  • 6
  • 17
1
function resizeWindow(){
    var evt = document.createEvent('UIEvents');
    evt.initUIEvent('resize', true, false,window,0);
    window.dispatchEvent(evt); 
}

call this function after 500 milliseconds.

Shobhit
  • 157
  • 2
  • 5
1

It helped me

domNodeToRerender.style.opacity = 0.99;
setTimeout(() => { domNodeToRerender.style.opacity = '' }, 0);
1

October 2020, the problem still persists with Chrome 85.

morewry's solution of using transform:translateZ(0) works, actually, any transformation works, including translate(0,0) and scale(1), but if you must update the element again, then the trick is to toggle the transformation, and the best way is to directly remove it, after one frame, using requestAnimationFrame (setTimeout should always be avoided because it will be slower so it can cause glitches).

So, the update one element:

    function refresh_element(node) {
        // you can use scale(1) or translate(0, 0), etc
        node.style.setProperty('transform', 'translateZ(0)');
        // this will remove the property 1 frame later
        requestAnimationFrame(() => {
            node.style.removeProperty('transform');
        });
    }
  • To clarify: the transform solution I posted at the time I originally posted it did need translateZ and didn't need to be toggled. The fix was based on the fact that, at the time, only 3d transformations were reliably promoted composite layers. Composite layers could be repainted independently and were thus prioritized for painting: therefore unlikely to be skipped in optimizations. 5 years later... I bet there are subtle but significant changes. – morewry Oct 17 '20 at 15:59
  • This worked for me. Thank you for posting. – rickz Jan 13 '21 at 19:53
0

An approach that worked for me on IE (I couldn't use the display technique because there was an input that must not loose focus)

It works if you have 0 margin (changing the padding works as well)

if(div.style.marginLeft == '0px'){
    div.style.marginLeft = '';
    div.style.marginRight = '0px';
} else {
    div.style.marginLeft = '0px';
    div.style.marginRight = '';
}
Rogel Garcia
  • 1,835
  • 14
  • 16
0

Sample Html:

<section id="parent">
  <article class="child"></article>
  <article class="child"></article>
</section>

Js:

  jQuery.fn.redraw = function() {
        return this.hide(0,function() {$(this).show(100);});
        // hide immediately and show with 100ms duration

    };

call function:

$('article.child').redraw(); //<==bad idea

$('#parent').redraw();
Mahdi Rostami
  • 260
  • 4
  • 21
  • also works with `.fadeIn(1)` instead of `.show(300)` for me - not pushing the element around. So i guess, it is the triggering of `display:none`and back to `block` – BananaAcid Mar 10 '15 at 01:07
0

My fix for IE10 + IE11. Basically what happens is that you add a DIV within an wrapping-element that has to be recalculated. Then just remove it and voila; works like a charm :)

    _initForceBrowserRepaint: function() {
        $('#wrapper').append('<div style="width=100%" id="dummydiv"></div>');
        $('#dummydiv').width(function() { return $(this).width() - 1; }).width(function() { return $(this).width() + 1; });
        $('#dummydiv').remove();
    },
Narayan
  • 17
  • 5
0

Most answers require the use of an asynchroneous timeout, which causes an annoying blink.

But I came up with this one, which works smoothly because it is synchroneous:

var p = el.parentNode,
    s = el.nextSibling;
p.removeChild(el);
p.insertBefore(el, s);
Oriol
  • 225,583
  • 46
  • 371
  • 457
0

This is my solution that worked for disappearing content...

<script type = 'text/javascript'>
    var trash_div;

    setInterval(function()
    {
        if (document.body)
        {
            if (!trash_div)
                trash_div = document.createElement('div');

            document.body.appendChild(trash_div);
            document.body.removeChild(trash_div);
        }
    }, 1000 / 25); //25 fps...
</script>
Kosmo零
  • 3,605
  • 5
  • 38
  • 74
0

I ran into a similar issue and this simple line of JS helped to fix it:

document.getElementsByTagName('body')[0].focus();

In my case it was a bug with a Chrome extension not redrawing the page after changing its CSS from within the extension.

Rotareti
  • 31,464
  • 14
  • 87
  • 93
0

I wanted to return all the states to the previous state (without reloading) including the elements added by jquery. The above implementation not gonna works. and I did as follows.

// Set initial HTML description
var defaultHTML;
function DefaultSave() {
  defaultHTML = document.body.innerHTML;
}
// Restore HTML description to initial state
function HTMLRestore() {
  document.body.innerHTML = defaultHTML;
}



DefaultSave()
<input type="button" value="Restore" onclick="HTMLRestore()">
Ryosuke Hujisawa
  • 1,763
  • 11
  • 14
0

I had a react component list which when scrolled, then opened another page, then when returning back the list was not rendered on Safari iOS until page was scrolled. So this is the fix.

    componentDidMount() {
        setTimeout(() => {
            window.scrollBy(0, 0);
        }, 300);
    }
luky
  • 1,787
  • 1
  • 17
  • 26
0

Below css works for me on IE 11 and Edge, no JS needed. scaleY(1) does the trick here. Seems the simplest solution.

.box {
    max-height: 360px;
    transition: all 0.3s ease-out;
    transform: scaleY(1);
}
.box.collapse {
    max-height: 0;
}
Edmond Wang
  • 1,476
  • 11
  • 26