提高Web页面渲染速度的7个技巧

240次阅读  |  发布于3年以前

用户在访问一个Web网站(页面)或应用时,总是希望它的加载速度快,功能流畅。如果过于慢,用户就很有可能失去耐心而离开你的Web网站或应用。作为开发人员,给自己应用提供更快的访问速度,提供很好的用户体验是必备的基础技能,而且Web开发者在开发中也可以做很多事情来改善用户体验。那我们今天就来和大家聊聊,在CSS方面有哪些技巧可以帮助我们来提高Web页面的渲染速度。

内容可见性(content-visibility)

一般来说,大多数Web应用都有复杂的UI元素,而且有的内容会在设备可视区域之外(内容超出了用户浏览器可视区域),比如下图中红色区域就在手机设备屏幕可视区域之外:

在这种场合下,我们可以使用CSS的content-visibility来跳过屏幕外的内容渲染。也就是说,如果你有大量的离屏内容(Off-screen Content),这将会大幅减少页面渲染时间。

这个功能是CSS新增的特性,隶属于 W3C 的 CSS Containment Module Level 2 模块。也是对提高渲染性能影响最大的功能之一。content-visibility可以接受visible、auto和hidden三个属性值,但我们可以在一个元素上使用content-visibility:auto来直接的提升页面的渲染性能。

假设我们有一个像下面的页面,整个页面有个卡片列表,大约有375张,大约在屏幕可视区域能显示12张卡片。正如下图所示,渲染这个页面浏览器用时大约1037ms:

你可以给所有卡片添加content-visibility:

.card {
    content-visibility: auto;
}

所有卡片加入content-visibility样式之后,页面的渲染时间下降到150ms,差不多提高了六倍的渲染性能:

正如你所看到的,content-visibility非常强大,提高页面渲染非常有用。换然话说,有了CSS的content-visibility属性,影响浏览器的渲染过程就变得更加容易。本质上,这个属性 改变了一个元素的可见性,并管理其渲染状态。

content-visibility有点类似于CSS的display和visibility属性,然而,content-visibility的操作方式与这些属性不同。

content-visibility的关键能力是,它允许我们推迟我们选择的HTML元素渲染。默认情况之下,浏览器会渲染DOM树内所有可以被用户查看的元素。用户可以看到视窗可视区域中所有元素,并通过滚动查看页面内其他元素。一次渲染所有的元素(包括视窗可视区域之外不可见的HTML元素)可以让浏览器正确计算页面的尺寸,同时保持整个页面的布局和滚动条的一致性。

如果浏览器不渲染页面内的一些元素,滚动将是一场噩梦,因为无法正确计算页面高度。这是因为,content-visibility会将分配给它的元素的高度(height)视为0,浏览器在渲染之前会将这个元素的高度变为0,从而使我们的页面高度和滚动变得混乱。但如果已经为元素或其子元素显式设置了高度,那这种行为就会被覆盖。如果你的元素中没显式设置高度,并且因为显式设置height可能会带来一定的副作用而没设置,那么我们可以使用contain-intrinsic-size来确保元素的正确渲染,同时也保留延迟渲染的好处。

.card {
    content-visibility: auto;
    contain-intrinsic-size: 200px;
}

这也意味着它将像有一个“固有尺寸”(Intrinsic-size)的单一子元素一样布局,确保你没设置尺寸的div(示例中的.card)仍然占据空间。contain-intrinsic-size作为一个占位符尺寸来替代渲染内容。

虽然contain-intrinsic-size能让元素有一个占位空间,但如果有大量的元素都设置了content-visibility: auto,滚动条仍然会有较小的问题。

content-visibility提供的另外两个值visible和hidden可以让我们实现像元素的显式和隐藏,类似于display的none和非none值的切换:

在这种情况下,content-visibility可以提高频繁显示或隐藏的元素的渲染性能,例如模态框的显示和隐藏。content-visibility可以提供这种性能提升,这要归功于其隐藏值(hidden)的功能与其他值的不同:

content-visibility属性的扩展阅读:

合理使用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提示,它不会对你使用它的元素产生任何样式上的影响。但值得注意的是,如果创建了新的层叠上下文,它可以产生外观效果。

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

<!-- HTML -->
<div class="animate"></div>

/* CSS */
.animate {
    will-change: opacity
}

浏览器渲染上面的代码时,浏览器将为该元素创建一个单独的层。之后,它将该元素的渲染与其他优化一起委托给GPU,即,浏览器会识别will-change属性,并优化未来与不透明相关的变化。这将使动画变得更加流畅,因为GPU加速接管了动画的渲染。> 根据 @Maximillian Laumeister 所做的性能基准,可以看到,他通过这种单行变化获得了超过120FPS的渲染速度,和最初的渲染速度(大约50FPS)相比,提高70FPS左右。

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

详细的使用,请参阅:

虽然说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 属性的示例,在大部分的场景中,你都应该这样做。

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可以记作以下几个规则,即 五可做,三不可做:

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

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

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

在上述这几个值中,size、layout和paint可以单独使用,也可以相互组合使用;另外content和strict是组合值,即content是layout paint的组合,strict是layout paint size的组合。 contain的size、layout和paint提供了不同的方式来影响浏览器渲染计算:

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

<div class="item">
    <div>Lorem ipsum...</div>
</div>

使用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属性有五个不同的值:

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

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);
    }
}

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

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

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

scroll-behavior让滚动更流畅

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

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

scroll-behavior接受两个值:

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

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

html {
    scroll-behavior:smooth;
}

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

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

开启GPU渲染动画

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

减少渲染阻止时间

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

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

<link rel="stylesheet" href="styles.css">

将其分解为多个样式表后:

<!-- style.css contains only the minimal styles needed for the page rendering -->
<link rel="stylesheet" href="styles.css" media="all" />

<!-- Following stylesheets have only the styles necessary for the form factor -->
<link rel="stylesheet" href="sm.css" media="(min-width: 20em)" />
<link rel="stylesheet" href="md.css" media="(min-width: 64em)" />
<link rel="stylesheet" href="lg.css" media="(min-width: 90em)" />
<link rel="stylesheet" href="ex.css" media="(min-width: 120em)" />
<link rel="stylesheet" href="print.css" media="print" />

默认情况下,浏览器假设每个指定的样式表都是阻塞渲染的。通过添加 media属性附加媒体查询,告诉浏览器何时应用样式表。当浏览器看到一个它知道只会用于特定场景的样式表时,它仍会下载样式,但不会阻塞渲染。通过将 CSS 分成多个文件,主要的 阻塞渲染 文件(本例中为 styles.css)的大小变得更小,从而减少了渲染被阻塞的时间。

避免@import包含多个样式表

通过 @import,我们可以在另一个样式表中包含一个样式表。当我们在处理一个大型项目时,使用 @import 可以使代码更加简洁。

关于 @import 的关键事实是,它是一个阻塞调用,因为它必须通过网络请求来获取文件,解析文件,并将其包含在样式表中。如果我们在样式表中嵌套了 @import,就会妨碍渲染性能。

/* style.css */
@import url("windows.css");

/* windows.css */
@import url("componenets.css");

与使用 @import 相比,我们可以通过多个 link 来实现同样的功能,但性能要好得多,因为它允许我们并行加载样式表。

注意动态修改自定义属性方式

CSS自定义属性又名CSS变量,该特性已经是非常成熟的特性了,可以在Web的开发中大胆的使用该特性:

:root { --color: red; }

button {
    color: var(--color);
}

在使用CSS自定义属性时,时常在root(根元素)上注册自定义属性,这种方式注册的自定义属性是个全局的自定义属性(全局变量),可以被所有嵌套的子元素继承。就上例而言,--color属性允许任何button样式将其作为变量使用。

熟悉CSS自定义属性的同学都知道,可以使用style.setProperty来重新设置已注册好的自定义属性的值。但在修改根自定义属性时,需要注意,因为它会影响Web的性能。早在2017年@Lisi Linhart 在《 Performance of CSS Variables》中阐述过。

有关于这方面的具体细节就不在这阐述了,如果你对这方面感兴趣的话,可以阅读下面这几篇文章:- Performance of CSS Variables

小结

可能很多人会说,5G已到来,终端设备性能越来越好,网络环境也越来越强,Web性能已不是问题了,但事实上在Web开发过程中总是难免碰到性能是的问题。而且我们为用户提供更流畅的体验也是我们必备技术之一。时至今日,优化Web性能的方式和手段很多,但在开发时注重每个细节,可以让我们把性能做得更好。正如文章中提到这些。

除了文章提到的这几个点,还有一些其他的方法可以使用CSS来提高网页的性能。当然,文章中提到的一些特性还没有得到所有浏览器支持,比如content-visibility、contain等,但在未来它们肯定能让页面渲染带来更快的渲染。另外,文章中提到的一些技巧并没有深入阐述,比如CSS的引用方式,CSS的阻塞等。如果你对这方面感兴趣的话,可以继续观注后续的相关更新;如果你在这方面更好的建议或经验,欢迎与我们分享。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8