在Chrome/Mac上强制DOM重绘/刷新

255

偶尔,Chrome会不正确地渲染完全有效的HTML/CSS或者根本不渲染。通过DOM检查器深入了解,通常足以让它认识到错误并进行正确重绘,因此可以证明标记是正确的。在我正在处理的项目中,这种情况经常发生(而且是可预测的),因此我已经编写了代码,在特定情况下强制执行重绘。

这在大多数浏览器/操作系统组合中都起作用:

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

就是说,修改一些未使用的CSS属性,然后请求一些强制重绘的信息,最后再恢复原来的属性。但是不幸的是,Chrome团队为Mac版本找到了一种无需重绘即可获取offsetHeight的方法,这使得这个本来很有用的技巧失效了。

目前,我在Chrome/Mac上找到的替代方案非常丑陋:

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

实际上,强制使元素跳动一下,然后放松一会儿并将其跳回。更糟糕的是,如果把超时时间降低到500毫秒以下(以使其不那么明显),它经常不能产生预期的效果,因为浏览器在恢复到原始状态之前无法重新绘制。

有人愿意提供一个更好的重绘/刷新hack版本吗(最好基于上面的第一个示例),可以在Chrome/Mac上运行?


2
请查看下面关于不透明度0.99的答案 - 这里是最佳答案 - 但很难找到,因为它在页面深处。 - Grouchal
1
这在 Linux 上也会发生 :-( - schlingel
2
你可以用requestAnimationFrame替换timeout,这样你就能以16毫秒的延迟实现相同的效果,而不是1000毫秒。 - trusktr
3
在此处提交一个Chromium问题以解决无效的渲染问题。尽管这个问题已经存在了4年,但似乎还没有人这样做过。 - Bardi Harborow
这个问题至少应该有一个jQuery标签。 - Makan
显示剩余3条评论
25个回答

196

不确定您想要实现什么,但这是我过去成功使用的一种方法,可以强制浏览器重新绘制,也许它对您有用。

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

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

如果这个简单的重绘不起作用,您可以尝试这个。它会在元素中插入一个空文本节点,以确保重绘。
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
}

编辑:回应Šime Vidas的问题,我们正在实现的是一种强制重排。您可以从大师本人http://paulirish.com/2011/dom-html5-css3-performance/了解更多信息。


3
据我所知,无法仅重绘视口的一部分。浏览器总是会重新绘制整个网页。 - Šime Vidas
41
在Chrome中,我需要用.hide.show(0)来正确地工作,而不是使用$('#parentOfElementToBeRedrawn').hide().show()。 - l.poellabauer
1
@l.poellabauer 我也是 - 这没有意义...但还是谢谢。你怎么找到的? :) - JohnIdol
7
FYI,我工作的相关领域是无限滚动列表。隐藏/显示整个UL有点行不通,所以我尝试了一下,并发现如果您在任何元素上执行.hide().show(0)(我选择了页面页脚),它应该可以刷新页面。 - treeface
1
可以仅重绘视口的一部分。虽然没有相应的API,但浏览器可以选择这样做。视口首先以瓦片形式绘制。而复合层则是独立绘制的。 - morewry
显示剩余5条评论

72

以上所有的答案都对我没用。但我注意到调整大小窗口会导致重新绘制,这对我有帮助:

$(window).trigger('resize');

12
提示:这将重新绘制您的整个窗口,这会消耗大量性能,可能不是 OP 想要的。 - hereandnow78
7
调整窗口大小总是会触发重新布局,但 $(window).trigger('resize') 代码不会,它只抛出“resize”事件,如果已分配相关处理程序,则会触发该事件。 - guari
2
你帮我节省了一整个星期的时间!我的问题是在设置<body style="zoom:67%">之后,页面缩放到了我期望的级别,但页面被冻结并无法滚动!你的答案立即解决了这个问题。 - Katelynn ruan

47
我们最近遇到了这个问题,并发现将受影响的元素提升为一个具有translateZ复合层可以解决问题,而无需额外的javascript。
.willnotrender { 
   transform: translateZ(0); 
}

由于这些绘画问题主要出现在Webkit/Blink中,而且这个修复方案主要针对Webkit/Blink,因此在某些情况下更可取。特别是因为许多JavaScript解决方案不仅会导致重新绘制,还会导致回流和重绘

1
当 canvas 不断动画时,移除了 HTML 元素却仍在画布上绘制时,对我而言这很有效。 - Knaģis
这解决了我在 neon-animated-pages 中遇到的布局问题。谢谢 :+1: - ZeunO8
1
这个方案可行。Safari会让位于设置为display none的div中的元素变得可见而且无法被选择。调整窗口大小可以使它们消失。添加这个transform能够使“残留物”如预期一样消失了。 - Jeff Mattson
1
太好了,谢谢!它起作用了。现在是2020年,而这个问题的解决方案是从2015年1月份的。你有什么想法为什么这个问题还没有被修复? - Kai
z-index: 0; 看起来也能正常工作,在我的使用情况下比这个答案中的 transform 引起的 z 层级混乱要少。 - derpedy-doo
修复我在移动Safari中旋转的SVG元素无法渲染的问题,2021年。 - James M

35

这个解决方案没有超时限制!真正 强制 重绘!适用于Android和iOS。

var forceRedraw = function(element){
  var disp = element.style.display;
  element.style.display = 'none';
  var trick = element.offsetHeight;
  element.style.display = disp;
};

2
这在我的Linux上的Chrome和FF上都不起作用。然而,仅仅访问element.offsetHeight(而不修改display属性)确实有效。就像浏览器在元素不可见时会跳过offsetHeight计算一样。 - Grodriguez
这个解决方案完美地解决了我在“overwolf”中使用的基于Chrome的浏览器遇到的一个bug。我一直在尝试弄清楚如何解决这个问题,而你为我节省了很多功夫。 - nacitar sevaht

13

这对我有用。点这里赞。

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

$(el).redraw();

1
但它留下了一些“遗物”,如“display: block”等,有时可能会破坏页面结构。 - Adam Pietrasiak
这基本上应该与 $().hide().show(0) 相同。 - Mahn
@Mahn 你试过了吗?我猜 .hide() 是非阻塞的,因此它会跳过浏览器中的重新渲染运行,也就是该方法的整个重点。(如果我没记错的话,那时我测试过。) - zupa
2
@zupa,已在Chrome 38上测试并可正常工作。诀窍在于show()show(0)不同,后者通过在超时中使用jQuery动画方法来指定持续时间,而不是立即触发,这给了渲染的时间,就像hide(0, function() { $(this).show(); })一样。 - Mahn
@Mahn 好的,你发现得很好。如果它可以跨平台使用并且不是最近的jQuery功能,我肯定支持你的解决方案。由于我没有时间测试它,我会将其添加为“可能有效”。如果你认为上述内容适用,欢迎编辑答案。 - zupa
@Mahn 让我详细说明一下。定义 .redraw() jQuery 方法有助于降低认知负荷。直接使用 .hide().show(0) 将需要更多的文档说明,因此它只涉及 .redraw() 方法的实现细节。 - zupa

10
在setTimeout的时间为0时将一个元素隐藏然后再显示它,可以强制重新绘制该元素。
$('#page').hide();
setTimeout(function() {
    $('#page').show();
}, 0);

1
这个方法适用于Chrome浏览器中SVG滤镜有时不会重绘的bug,https://bugs.chromium.org/p/chromium/issues/detail?id=231560。超时设置非常重要。 - Jason D
这是在尝试强制Chrome重新计算myElement.getBoundingClientRect()值时对我有效的方法,这些值被缓存,可能是为了优化/性能目的。我只需为将新元素添加到DOM并在具有先前(缓存)值的旧元素从DOM中删除后获取新值的函数添加零毫秒超时即可。 - Chunky Chunk

7
这对我来说似乎很管用。而且,它真的完全看不出来。
$(el).css("opacity", .99);
setTimeout(function(){
   $(el).css("opacity", 1);
},20);

@LonnieBest 你知道是否还有其他因素导致了这个问题吗?比如说你的DOM大小。这个解决方案在应用程序的某个部分对我有效,但在另一个部分却无效,所以我怀疑还有其他因素在起作用。 - Uche Ozoemena
@UcheOzoemena 如果我没记错的话,那个项目比正常情况下有更多的DOM元素(主要是具有许多行的表格)。如果这个技巧对你不起作用,考虑将 setTimeout(function(){},0); 放入其中,以便稍后进行调整。 - Lonnie Best
@LonnieBest 谢谢,是的,我已经尝试过使用 setTimeout(() => {}, 0) ,但没有成功 :( - Uche Ozoemena
1
@UcheOzoemena 假设上面的技巧在setTimeout函数的主体中,您可以尝试将第二个参数增加到远大于零的值,以查看是否最终可以使技巧起作用。当您进行异步编程时,它变得复杂; 您可能必须创建一个承诺,告诉您何时完成其他所有操作,然后再执行技巧。很有可能,在正确的时间之前,您已经使用了这个技巧。 - Lonnie Best
@LonnieBest 哦,相信我,我看到了这个问题的复杂性!无论如何,是的,我已经有了这种承诺机制,但用于另一个问题。你还记得在那个项目中是否有其他策略来管理这样的问题吗?我担心如果需要更长的超时时间,我可能不会更接近根本原因。 - Uche Ozoemena
显示剩余3条评论

6

2020年:更轻、更强

以前的解决方案对我来说已经不再奏效了。

我猜测浏览器通过检测越来越多的“无用”变化来优化绘制过程。

这个解决方案让浏览器绘制一个克隆来替换原始元素。它可以工作,而且可能更加可持续:

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

已在Chrome 80 / Edge 80 / Firefox 75上进行了测试


4
如果您的元素上挂载了事件监听器,这并不是一个好主意。在替换元素后,您将不得不重新添加它们到该元素。 - karlingen
2021年在Chrome中循环执行时会出现问题。 - AntonOfTheWoods
1
如果您有事件监听器,这将无法正常工作。事件将不再起作用。 - Bassam Seydo

4

调用window.getComputedStyle()应该强制重排


3
对我来说没有用,但那可能是因为我需要 offsetHeight 属性,它不是 CSS。 - Sebas

3
今天在OSX El Capitan和Chrome v51中遇到了这个挑战。该页面在Safari中运行正常。我尝试了这个页面上的几乎所有建议,但都没有起作用,而且都有副作用...最终,我实现了下面的代码-超级简单-没有副作用(在Safari中仍然像以前一样工作)。
解决方案:根据需要在有问题的元素上切换类。每次切换都会强制重新绘制。(为方便起见,我使用了jQuery,但纯JavaScript也应该没有问题...)
jQuery类切换
$('.slide.force').toggleClass('force-redraw');

CSS类别(Class)
.force-redraw::before { content: "" }

就是这样了...

注意:您必须在“完整页面”下运行下面的代码片段才能看到效果。

$(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>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接