用户在访问一个Web网站(页面)或应用时,总是希望它的加载速度快,功能流畅。如果过于慢,用户就很有可能失去耐心而离开你的Web网站或应用。作为开发人员,给自己应用提供更快的访问速度,提供很好的用户体验是必备的基础技能,而且Web开发者在开发中也可以做很多事情来改善用户体验。那我们今天就来和大家聊聊,在CSS方面有哪些技巧可以帮助我们来提高Web页面的渲染速度。
一般来说,大多数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)的功能与其他值的不同:
display: none:隐藏元素并破坏其渲染状态。这意味着取消隐藏元素与渲染具有相同内容的新元素一样昂贵
visibility: hidden:隐藏元素并保持其渲染状态。这并不能真正从文档中删除该元素,因为它(及其子树)仍占据页面上的几何空间,并且仍然可以单击。它也可以在需要时随时更新渲染状态,即使隐藏也是如此
content-visibility: hidden:隐藏元素并保留其渲染状态。这意味着该元素隐藏时行为和display: none一样,但再次显示它的成本要低得多
content-visibility属性的扩展阅读:
content-visibility: the new CSS property that boosts your rendering performance
https://web.dev/content-visibility/
More on content-visibility
https://css-tricks.com/more-on-content-visibility/
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的使用并不复杂,它能接受的值有:
auto:默认值,浏览器会根据具体情况,自行进行优化
scroll-position:表示开发者将要改变元素的滚动位置,比如浏览器通常仅渲染可滚动元素“滚动窗口”中的内容。而某些内容超过该窗口(不在浏览器的可视区域内)。如果will-change显式设置了该值,将扩展渲染“滚动窗口”周围的内容,从而顺利地进行更长,更快的滚动(让元素的滚动更流畅)
content:表示开发者将要改变元素的内容,比如浏览器常将大部分不经常改变的元素缓存下来。但如果一个元素的内容不断发生改变,那么产生和维护这个缓存就是在浪费时间。如果will-change显式设置了该值,可以减少浏览器对元素的缓存,或者完全避免缓存。变为从始至终都重新渲染元素。使用该值时需要尽量在文档树最末尾上使用,因为该值会被应用到它所声明元素的子节点,要是在文档树较高的节点上使用的话,可能会对页面性能造成较大的影响
详细的使用,请参阅:
CSS Will Change Module Level 1
https://www.w3.org/TR/css-will-change-1/
Everything You Need to Know About the CSS will-change Property
https://dev.opera.com/articles/css-will-change-property/
CSS Reference:will-change
https://tympanus.net/codrops/css_reference/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可以记作以下几个规则,即 五可做,三不可做:
在样式表中少用will-change
给will-change足够的时间令其发挥该有的作用
使用
如果需要的话,可以JavaScript中使用它(添加和删除)
修改完成后,删除will-change
不要同时声明太多的属性
不要应用在太多元素上
不要把资源浪费在已停止变化的元素上
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元素:
<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的更多内容:
Let’s Take a Deep Dive Into the CSS Contain Property
https://css-tricks.com/lets-take-a-deep-dive-into-the-css-contain-property/
Helping Browsers Optimize With The CSS Contain Property
https://www.smashingmagazine.com/2019/12/browsers-containment-css-contain-property/
CSS contain Property
https://termvader.github.io/css-contain/
在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更多的介绍,可以阅读:
A deep dive into webfonts
https://iamschulz.com/a-deep-dive-into-webfonts/
How to avoid layout shifts caused by web fonts
https://simonhearne.com/2021/layout-shifts-webfonts/
The Best Font Loading Strategies and How to Execute Them
https://css-tricks.com/the-best-font-loading-strategies-and-how-to-execute-them/
A font-display setting for slow connections
https://calendar.perfplanet.com/2020/a-font-display-setting-for-slow-connections/
How to Load Fonts in a Way That Fights FOUT and Makes Lighthouse Happy
https://css-tricks.com/how-to-load-fonts-in-a-way-that-fights-fout-and-makes-lighthouse-happy/
The importance of @font-face source order when used with preload
https://nooshu.github.io/blog/2021/01/23/the-importance-of-font-face-source-order-when-used-with-preload/
The Fastest Google Fonts
https://csswizardry.com/2020/05/the-fastest-google-fonts/
A COMPREHENSIVE GUIDE TO FONT LOADING STRATEGIES
https://www.zachleat.com/web/comprehensive-webfonts/
早前在滚动的特性和改变用户体验的滚动新特性中向大家介绍了几个可以用来改变用户体验的滚动特性,比如滚动捕捉、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
浏览器针对处理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,就会妨碍渲染性能。
/* 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》中阐述过。
在使用CSS变量时,我们总是要注意我们的变量是在哪个范围内定义的,如果改变它,将影响许多子代,从而产生大量的样式重新计算。
结合CSS变量使用calc()是一个很好的方法,可以获得更多的灵活性,限制我们需要定义的变量数量。在不同的浏览器中测试calc()与CSS变量的结合,并没有发现任何大的性能问题。然而在一些浏览器中对一些单位的支持还是有限的,比如deg或ms,所以我们必须记住这一点。
如果我们比较一下在JavaScript中通过内联样式设置变量与setProperty方法的性能标志,浏览器之间有一些明显的差异。在Safari中通过内联样式设置属性的速度非常快,而在Firefox中则非常慢,所以使用setProperty设置变量是首选
有关于这方面的具体细节就不在这阐述了,如果你对这方面感兴趣的话,可以阅读下面这几篇文章:- Performance of CSS Variables
https://lisilinhart.info/posts/css-variables-performance/
Control CSS loading with custom properties
https://jakearchibald.com/2016/css-loading-with-custom-props/
CSS Custom Properties performance in 2018
https://blog.jiayihu.net/css-custom-properties-performance-in-2018/
Improving CSS Custom Properties performance
https://blogs.igalia.com/jfernandez/2020/08/13/improving-css-custom-properties-performance/
可能很多人会说,5G已到来,终端设备性能越来越好,网络环境也越来越强,Web性能已不是问题了,但事实上在Web开发过程中总是难免碰到性能是的问题。而且我们为用户提供更流畅的体验也是我们必备技术之一。时至今日,优化Web性能的方式和手段很多,但在开发时注重每个细节,可以让我们把性能做得更好。正如文章中提到这些。
除了文章提到的这几个点,还有一些其他的方法可以使用CSS来提高网页的性能。当然,文章中提到的一些特性还没有得到所有浏览器支持,比如content-visibility、contain等,但在未来它们肯定能让页面渲染带来更快的渲染。另外,文章中提到的一些技巧并没有深入阐述,比如CSS的引用方式,CSS的阻塞等。如果你对这方面感兴趣的话,可以继续观注后续的相关更新;如果你在这方面更好的建议或经验,欢迎与我们分享。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8