提高Web页面渲染速度的7个技巧,原理竟然是这

53 阅读12分钟

合理使用will-change

CSS渲染器(CSS Renderer)在渲染CSS样式之前需要一个准备过程,因为有些CSS属性需要CSS渲染器事先做很多准备才能实现渲染。这就很容易导致页面出现卡顿,给用户带来不好的体验。

比如Web上的动效,通常情况之下,Web动画(在动的元素)是和其他元素一起定期渲染的,以往在动画开发时,会使用CSS的3D变换(transform中的translate3d()或translateZ())来开启GPU加速,让动画变得更流畅,但这样做是一种黑魔法,会将元素和它的上下文提到另一个“层”,独立于其他元素被渲染。可这种将元素提取到一个新层,相对来说代价也是昂贵的,这可能会使transform动画延迟几百毫秒。

不过,现在我可以不使用transform这样的Hack手段来开启GPU加速,可以直接使用CSS的will-change属性,该属性可以表明元素将修改特定的属性,让浏览器事先进行必要的优化。也就是说,will-change是一个UA提示,它不会对你使用它的元素产生任何样式上的影响。但值得注意的是,如果创建了新的层叠上下文,它可以产生外观效果。

比如下面这样的一个动画示例:

/* CSS */

.animate {

will-change: opacity

}

浏览器渲染上面的代码时,浏览器将为该元素创建一个单独的层。之后,它将该元素的渲染与其他优化一起委托给GPU,即,浏览器会识别will-change属性,并优化未来与不透明相关的变化。这将使动画变得更加流畅,因为GPU加速接管了动画的渲染。

根据 @Maximillian Laumeister 所做的性能基准,可以看到,他通过这种单行变化获得了超过120FPS的渲染速度,和最初的渲染速度(大约50FPS)相比,提高70FPS左右。

will-change的使用并不复杂,它能接受的值有:

  • auto:默认值,浏览器会根据具体情况,自行进行优化

  • scroll-position:表示开发者将要改变元素的滚动位置,比如浏览器通常仅渲染可滚动元素“滚动窗口”中的内容。而某些内容超过该窗口(不在浏览器的可视区域内)。如果will-change显式设置了该值,将扩展渲染“滚动窗口”周围的内容,从而顺利地进行更长,更快的滚动(让元素的滚动更流畅)

  • content:表示开发者将要改变元素的内容,比如浏览器常将大部分不经常改变的元素缓存下来。但如果一个元素的内容不断发生改变,那么产生和维护这个缓存就是在浪费时间。如果will-change显式设置了该值,可以减少浏览器对元素的缓存,或者完全避免缓存。变为从始至终都重新渲染元素。使用该值时需要尽量在文档树最末尾上使用,因为该值会被应用到它所声明元素的子节点,要是在文档树较高的节点上使用的话,可能会对页面性能造成较大的影响

  • :表示开发者将要改变的元素属性。如果给定的值是缩写,则默认被扩展全,比如,will-change设置的值是padding,那么会补全所有padding的属性,如 will-change: padding-top, padding-right, padding-bottom, padding-left;

详细的使用,请参阅:

虽然说will-change能提高性能,但这个属性应该被认为是最后的手段,它不是为了过早的优化。只有消退你必须处理性能问题时,你才应该使用它。如果你滥用的话,反而会降低Web的性能。比如:

使用will-change表示该元素在未来会发生变化。

因此,如果你试图将will-change和动画同时使用,它将不会给你带来优化。因此,建议在父元素上使用will-change,在子元素上使用动画。

.animate-element-parent {

will-change: opacity;

}

.animate-element {

transition: opacity .2s linear

}

不要使用非动画元素。

当你在一个元素上使用will-change时,浏览器会尝试通过将元素移动到一个新的图层并将转换工作交互GPU来优化它。如果你没有任何要转换的内容,则会导致资源浪费。

除此之外,要用好will-change也不是件易事,MDN在这方面做出了相应的描述:

  • 不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与 will-change 结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。比如 *{will-change: transform, opacity;}

  • 有节制地使用:通常,当元素恢复到初始状态时,浏览器会丢弃掉之前做的优化工作。但是如果直接在样式表中显式声明了 will-change 属性,则表示目标元素可能会经常变化,浏览器会将优化工作保存得比之前更久。所以最佳实践是当元素变化之前和之后通过脚本来切换 will-change 的值

  • 不要过早应用 will-change 优化:如果你的页面在性能方面没什么问题,则不要添加 will-change 属性来榨取一丁点的速度。will-change 的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。它不应该被用来预防性能问题。过度使用 will-change 会导致大量的内存占用,并会导致更复杂的渲染过程,因为浏览器会试图准备可能存在的变化过程。这会导致更严重的性能问题。

  • 给它足够的工作时间:这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。然后浏览器可以选择在变化发生前提前去做一些优化工作。所以给浏览器一点时间去真正做这些优化工作是非常重要的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上 will-change 属性。

最后需要注意的是,建议在完成所有动画后,将元素的will-change删除。下面这个示例展示如何使用脚本正确地应用 will-change 属性的示例,在大部分的场景中,你都应该这样做。

var el = document.getElementById('element');

// 当鼠标移动到该元素上时给该元素设置 will-change 属性

el.addEventListener('mouseenter', hintBrowser);

// 当 CSS 动画结束后清除 will-change 属性

el.addEventListener('animationEnd', removeHint);

function hintBrowser() {

// 填写上那些你知道的,会在 CSS 动画中发生改变的 CSS 属性名们

this.style.willChange = 'transform, opacity';

}

function removeHint() {

this.style.willChange = 'auto';

}

在实际使用will-change可以记作以下几个规则,即 五可做,三不可做:

  • 在样式表中少用will-change

  • 给will-change足够的时间令其发挥该有的作用

  • 使用来针对超特定的变化(如,left, opacity等)

  • 如果需要的话,可以JavaScript中使用它(添加和删除)

  • 修改完成后,删除will-change

  • 不要同时声明太多的属性

  • 不要应用在太多元素上

  • 不要把资源浪费在已停止变化的元素上

让元素及其内容尽可能独立于文档树的其余部分(contain)

W3C的CSS Containment Module Level 2除了提供前面介绍的content-visibility属性之外,还有另一个属性contain。该属性允许我们指定特定的DOM元素和它的子元素,让它们能够独立于整个DOM树结构之外。目的是能够让浏览器有能力只对部分元素进行重绘、重排,而不必每次针对整个页面。即,允许浏览器针对DOM的有限区域而不是整个页面重新计算布局,样式,绘画,大小或它们的任意组合。

在实际使用的时候,我们可以通过contain设置下面五个值中的某一个来规定元素以何种方式独立于文档树:

  • layout :该值表示元素的内部布局不受外部的任何影响,同时该元素以及其内容也不会影响以上级

  • paint :该值表示元素的子级不能在该元素的范围外显示,该元素不会有任何内容溢出(或者即使溢出了,也不会被显示)

  • size :该值表示元素盒子的大小是独立于其内容,也就是说在计算该元素盒子大小的时候是会忽略其子元素

  • content :该值是contain: layout paint的简写

  • strict :该值是contain: layout paint size的简写

在上述这几个值中,size、layout和paint可以单独使用,也可以相互组合使用;另外content和strict是组合值,即content是layout paint的组合,strict是layout paint size的组合。

contain的size、layout和paint提供了不同的方式来影响浏览器渲染计算:

  • size:告诉浏览器,当其内容发生变化时,该容器不应导致页面上的位置移动

  • layout:告诉浏览器,容器的后代不应该导致其容器外元素的布局改变,反之亦然

  • paint:告诉浏览器,容器的内容将永远不会绘制超出容器的尺寸,如果容器是模糊的,那么就根本不会绘制内容

@Manuel Rego Casasnovas提供了一个示例,向大家阐述和演示了contain是如何提高Web页面渲染性能。这个示例中,有10000个像下面这样的DOM元素:

Lorem ipsum...

使用JavaScript的textContent这个API来动态更改div.item > div的内容:

const NUM_ITEMS = 10000;

const NUM_REPETITIONS = 10;

function log(text) {

let log = document.getElementById("log");

log.textContent += text;

}

function changeTargetContent() {

log("Change "targetInner" content...");

// Force layout.

document.body.offsetLeft;

let start = window.performance.now();

let targetInner = document.getElementById("targetInner");

targetInner.textContent = targetInner.textContent == "Hello World!" ? "BYE" : "Hello World!";

// Force layout.

document.body.offsetLeft;

let end = window.performance.now();

let time = window.performance.now() - start;

log(" Time (ms): " + time + "\n");

return time;

}

function setup() {

for (let i = 0; i < NUM_ITEMS; i++) {

let item = document.createElement("div");

item.classList.add("item");

let inner = document.createElement("div");

inner.style.backgroundColor = "#" + Math.random().toString(16).slice(-6);

inner.textContent = "Lorem ipsum...";

item.appendChild(inner);

wrapper.appendChild(item);

}

}

如果不使用contain,即使更改是在单个元素上,浏览器在布局上的渲染也会花费大量的时间,因为它会遍历整个DOM树(在本例中,DOM树很大,因为它有10000个DOM元素):

在本例中,div的大小是固定的,我们在内部div中更改的内容不会溢出它。因此,我们可以将contain: strict应用到项目上,这样当项目内部发生变化时,浏览器就不需要访问其他节点,它可以停止检查该元素上的内容,并避免到外部去。

尽管这个例子中的每一项都很简单,但通过使用contain,Web性能得到很大的改变,从4ms降到了0.04ms,这是一个巨大的差异。想象一下,如果DOM树具有非常复杂的结构和内容,但只修改了页面的一小部分,如果可以将其与页面的其他部分隔离开来,那么将会发生什么情况呢?

有关于contain的更多内容:

使用font-display解决由于字体造成的布局偏移(FOUT)

在Web开发的过程中,难免会使用@font-face技术引用一些特殊字体(系统没有的字体),同时也可能会配合变量字体特性,使用更具个性化的字体。

使用@font-face加载字体策略大概如下图所示:

上图来自于@zachleat的《A COMPREHENSIVE GUIDE TO FONT LOADING STRATEGIES》一文。

Web中使用非系统字体(@font-face规则引入的字体)时,浏览器可能没有及时得到Web字体,就会让它用后备系统字体渲染,然后优化我们的字体。这个时候很容易引起未编排(Unstyled)的文本引起闪烁,整个排版本布局也看上去会偏移一下(FOUT)。

幸运的是,根据@font-face规则,font-display属性定义了浏览器如何加载和显示字体文件,允许文本在字体加载或加载失败时显示回退字体。可以通过依靠折中无样式文本闪现使文本可见替代白屏来提高性能。

CSS的font-display属性有五个不同的值:

  • auto:默认值。典型的浏览器字体加载的行为会发生,也就是使用自定义字体的文本会先被隐藏,直到字体加载结束才会显示。即字体展示策略与浏览器一致,当前,大多数浏览器的默认策略类似block

  • block:给予字体一个较短的阻塞时间(大多数情况下推荐使用 3s)和无限大的交换时间。换言之,如果字体未加载完成,浏览器将首先绘制“隐形”文本;一旦字体加载完成,立即切换字体。为此,浏览器将创建一个匿名字体,其类型与所选字体相似,但所有字形都不含“墨水”。使用特定字体渲染文本之后页面方才可用,只有这种情况下才应该使用 block。

  • swap:使用 swap,则阻塞阶段时间为 0,交换阶段时间无限大。也就是说,如果字体没有完成加载,浏览器会立即绘制文字,一旦字体加载成功,立即切换字体。与 block 类似,如果使用特定字体渲染文本对页面很重要,且使用其他字体渲染仍将显示正确的信息,才应使用 swap。

  • fallback:这个可以说是auto和swap的一种折中方式。需要使用自定义字体渲染的文本会在较短的时间不可见,如果自定义字体还没有加载结束,那么就先加载无样式的文本。一旦自定义字体加载结束,那么文本就会被正确赋予样式。使用 fallback时,阻塞阶段时间将非常小(多数情况下推荐小于 100ms),交换阶段也比较短(多数情况下建议使用 3s)。换言之,如果字体没有加载,则首先会使用后备字体渲染。一旦加载成功,就会切换字体。但如果等待时间过久,则页面将一直使用后备字体。如果希望用户尽快开始阅读,而且不因新字体的载入导致文本样式发生变动而干扰用户体验,fallback 是一个很好的选择。

  • optional:效果和fallback几乎一样,都是先在极短的时间内文本不可见,然后再加载无样式的文本。不过optional选项可以让浏览器自由决定是否使用自定义字体,而这个决定很大程度上取决于浏览器的连接速度。如果速度很慢,那你的自定义字体可能就不会被使用。使用 optional 时,阻塞阶段时间会非常小(多数情况下建议低于 100ms),交换阶段时间为 0。

下面是使用swap值的一个例子:

@font-face {

font-family: "Open Sans Regular";

font-weight: 400;

font-style: normal;

src: url("fonts/OpenSans-Regular-BasicLatin.woff2") format("woff2");

font-display: swap;

}

在这个例子里我们通过只使用WOFF2文件来缩写字体。另外我们使用了swap作为font-display的值,页面的加载情况将如下图所示:

注意,font-display一般放在@font-face规则中使用。

有关于字体加载和font-display更多的介绍,可以阅读:

scroll-behavior让滚动更流畅

早前在滚动的特性和改变用户体验的滚动新特性中向大家介绍了几个可以用来改变用户体验的滚动特性,比如滚动捕捉、overscroll-behavior和scroll-behavior。

scroll-behavior是CSSOM View Module提供的一个新特性,可以轻易的帮助我们实现丝滑般的滚动效果。该属性可以为一个滚动框指定滚动行为,其他任何的滚动,例如那些由于用户行为而产生的滚动,不受这个属性的影响。

scroll-behavior接受两个值:

  • auto :滚动框立即滚动

  • smooth :滚动框通过一个用户代理定义的时间段使用定义的时间函数来实现平稳的滚动,用户代理平台应遵循约定,如果有的话

除此之外,其还有三个全局的值:inherit、initial和unset。

使用起来很简单,只需要这个元素上使用scroll-behavior:smooth。因此,很多时候为了让页面滚动更平滑,建议在html中直接这样设置一个样式:

html {

scroll-behavior:smooth;

}

口说无凭,来看个效果对比,你会有更好的感觉:

有关于scroll-behavior属性更多的介绍可以再花点时间阅读下面这些文章:

  • CSSOM View Module:scroll-behavior

  • CSS-Tricks: scroll-behavior

  • Native Smooth Scroll behavior

  • PAGE SCROLLING IN VANILLA JAVASCRIPT

  • smooth scroll behavior polyfill

开启GPU渲染动画

浏览器针对处理CSS动画和不会很好地触发重排(因此也导致绘)的动画属性进行了优化。为了提高性能,可以将被动画化的节点从主线程移到GPU上。将导致合成的属性包括 3D transforms (transform: translateZ(), rotate3d(),等),animating, transform 和 opacity, position: fixed,will-change,和 filter。一些元素,例如 ,  和 ,也位于各自的图层上。将元素提升为图层(也称为合成)时,动画转换属性将在GPU中完成,从而改善性能,尤其是在移动设备上。

减少渲染阻止时间

今天,许多Web应用必须满足多种形式的需求,包括PC、平板电脑和手机等。为了完成这种响应式的特性,我们必须根据媒体尺寸编写新的样式。当涉及页面渲染时,它无法启动渲染阶段,直到 CSS对象模型(CSSOM)已准备就绪。根据你的Web应用,你可能会有一个大的样式表来满足所有设备的形式因素。

但是,假设我们根据表单因素将其拆分为多个样式表。在这种情况下,我们可以只让主CSS文件阻塞关键路径,并以高优先级下载它,而让其他样式表以低优先级方式下载。

最后

文章到这里就结束了,如果觉得对你有帮助可以点个赞哦

开源分享:docs.qq.com/doc/DSmRnRG…