优化-CSS
重绘与回流
在页面的生命周期中, 网页生成的时候, 至少会渲染一次. 在用户访问的过程中, 还会不断的触发重绘和回流.
重绘和回流是渲染步骤中的一小步, 但是这两个步骤对于性能的影响比较大.
- 重绘是当前节点需要更改外观而不会影响布局的, 比如改变 color 就称为重绘
- 回流是布局或者集合属性需要改变就称为回流
回流必定重绘, 重绘不一定引发回流. 回流所需的成本比重绘高得多, 改变深层次的节点更可能导致父节点的一些列回流.
重绘和回流实际上和 Event Loop 有关
- 当 EventLoop 执行完 MincroTasks 后, 会判断 documeny 是否需要更新, 因为浏览器时 60HZ 刷新率, 所以每 16ms 才会更新一次.
- 判断是否有 resize/scroll, 有就触发, 所以这两个事件实际上至少 16ms 才会触发一次, 自带节流
- 判断是否触发了 media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行 requestAnimationFrame 回调
- 执行 IntersectionObserver 回到, 该方法用于判断元素是否可见, 可以用于懒加载, 但是兼容性不太好
- 更新界面
- 一帧完成, 如果一帧中有空闲时间, 就会去执行
requestIdleCallback回调
回流(重排)
当DOM的变化影响了元素的几何信息(元素的位置和尺寸的大小), 浏览器需要重新计算元素的几何属性, 将其安排在界面中的正确位置, 这个过程叫做回流.
简单的说就是重新生成布局, 排列元素.
下面的几种机框会发生回流:
- 页面初始渲染,这是开销最大的一次回流
- 添加/删除可见的DOM元素
- 改变元素位置
- 改变元素尺寸,比如边距、填充、边框、宽度和高度等
- 改变元素内容,比如文字数量,图片大小等
- 改变元素字体大小
- 改变浏览器窗口尺寸,比如resize事件发生时
- 激活CSS伪类(例如::hover)
- 设置
style属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow - 查询某些属性或调用某些计算方法:
offsetWidth、offsetHeight等,除此之外,当我们调用getComputedStyle方法,或者IE里的currentStyle时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”。
常见引起重排属性和方法:
width
height
margin
padding
display
border-width
border
position
overflow
font-size
vertical-align
min-height
clientWidth
clientHeight
clientTop
clientLeft
offsetWudth
offsetHeight
offsetTop
offsetLeft
scrollWidth
scrollHeight
scrollTop
scrollLeft
scrollIntoView()
scrollTo()
getComputedStyle()
getBoundingClientRect()
scrollIntoViewIfNeeded()
回流影响的范围
由于浏览器渲染界面是基于流式布局模型的, 所以触发回流会对周围DOM重新排列, 影响的范围有两种:
- 全局范围: 从根节点开始对整个渲染树进行重新布局
- 局部范围: 对渲染树的某个部分或者一个渲染对象进行重新布局. 比如把一个dom的宽高定死, 在dom内部触发回流.
重绘
当一个元素的外观发生变化, 但没有改变布局, 重新吧元素外观绘制出来的过程, 叫做重绘.
常见的引起重绘的属性:
color
border-style
visibility
background
text-decoration
background-image
background-position
background-repeat
outline-color
outline
outline-style
border-radius
outline-width
box-shadow
background-size
优化重绘与回流
减少回流范围
以局部布局的形式组织html结构,尽可能小的影响重排的范围:
- 尽可能在低层级的DOM节点上,而不是像上述全局范围的示例代码一样,如果你要改变p的样式,class就不要加在div上,通过父元素去影响子元素不好。
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。不得已使用table的场合,可以设置table-layout:auto;或者是table-layout:fixed这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
减少回流次数
- 样式集中改变
不要频繁的操作样式,对于一个静态页面来说,明智且可维护的做法是更改类名而不是修改样式,对于动态改变的样式来说,相较每次微小修改都直接触及元素,更好的办法是统一在cssText变量中编辑。虽然现在大部分现代浏览器都会有Flush队列进行渲染队列优化,但是有些老版本的浏览器比如IE6的效率依然低下。
// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// 当top和left的值是动态计算而成时...
// better
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
// better
el.className += " className";
- 分离读写操作
DOM 的多个读操作(或多个写操作),应该放在一起。不要两个读操作之间,加入一个写操作。
// bad 强制刷新 触发四次重排+重绘
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';
div.style.right = div.offsetRight + 1 + 'px';
div.style.bottom = div.offsetBottom + 1 + 'px';
// good 缓存布局信息 相当于读写分离 触发一次重排+重绘
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
var curRight = div.offsetRight;
var curBottom = div.offsetBottom;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
div.style.right = curRight + 1 + 'px';
div.style.bottom = curBottom + 1 + 'px';
原来的操作会导致四次重排,读写分离之后实际上只触发了一次重排,这都得益于浏览器的渲染队列机制:
当我们修改了元素的几何属性,导致浏览器触发重排或重绘时。它会把该操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行这些操作。
- 将DOM离线
- 使用
display:none: 一旦我们给元素设置 display:none 时(只有一次重排重绘),元素便不会再存在在渲染树中,相当于将其从页面上“拿掉”,我们之后的操作将不会触发重排和重绘,添加足够多的变更后,通过 display属性显示(另一次重排重绘)。通过这种方式即使大量变更也只触发两次重排。另外,visibility : hidden 的元素只对重绘有影响,不影响重排。 - 通过
DocumentFragment创建一个 dom 碎片,在它上面批量操作 dom,操作完成之后,再添加到文档中,这样只会触发一次重排 - 复制节点,在副本上工作,然后替换它!
- 使用absolute或者fixed脱离文档流
使用绝对定位会使的该元素单独成为渲染树中 body 的一个子元素,重排开销比较小,不会对其它节点造成太多影响。当你在这些节点上放置这个元素时,一些其它在这个区域内的节点可能需要重绘,但是不需要重排。
- 优化动画
- 可以把动画效果应用到position属性为
absolute或fixed的元素上,这样对其他元素影响较小。 - 启用GPU加速: GPU 加速通常包括以下几个部分:Canvas2D,布局合成, CSS3转换(transitions),CSS3 3D变换(transforms),WebGL和视频(video)。
- 其他
- 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
for (let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop);
}
- 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
- 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
- CSS 选择符从右往左匹配查找,避免 DOM 深度过深
- 将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于 video 标签,浏览器会自动将该节点变为图层
- 使用
translate替代top
<div class="test"></div>
<style>
.test {
position: absolute;
top: 10px;
width: 100px;
height: 100px;
background: red;
}
</style>
<script>
setTimeout(() => {
// 引起回流
document.querySelector('.test').style.top = '100px';
}, 1000);
</script>
压缩和最小化css
这部分借助webpack的插件可以很好的实现, 比如使用Terser, 或者使用webpack自带的工具.
删除未使用的CSS
可以通过UnusedCSS或者PurifyCSS, 帮助查询不必要的CSS样式, 但是必须要配合仔细的视觉回归测试.
或者使用CSS-in-JS: 每个组件内渲染的样式都是只需要CSS. 在CSS-in-JS中可以将其内敛到页面中, 或者将其提取到外部CSS文件中.
优先考虑关键的CSS
关键CSS是一种CSS优化技术, 它提取并内嵌CSS以获得页面以上的内容. 在HTML文档的head中内联提取的样式.
但是这些内容需要保持在14kb以下.
为了确定关键的CSS并不完全的准确, 因此你需要对折叠位置进行假设. 这对于高度动态的网站是非常困难的. 我们可以通过Critical, CriticalCSS和Penthouse等工具进行自动化处理
异步加载CSS
除了关键CSS之外的部分, 可以异步加载. 实现的方式是将link media属性设置为print.
<link rel ="stylesheet" href="non-critical.css" media = "print" onload="this.media='all'">
另一种方法是使用<link rel="preload">
避免在CSS文件中使用@import
使用@import会阻塞渲染流程, 而使用link是并行下载的.
contain 属性
contain CSS属性告诉浏览器, 该元素机器子元素是独立于文档树的.浏览器会优化页面独立部分的渲染
contain属性在包含许多独立小组件的页面上非常的有用. 可以使用它来防止每个小组件的更改影响边框外的副作用.
使用CSS优化字体加载
- 避免在加载字体时出现不可见的文件
字体通常是需要一段时间来加载大文件. 一些浏览器会隐藏文本, 直到字体加载完毕(导致不可见文本的闪烁, FOIT)来处理这个问题. 在优化速度时, 你会希望避免不可见文本的闪烁, 并使用系统字体立即向人们展示内容.
可以使用font-display来控制字体的显示顺序
- 使用可变字体以减少文件大小
可变字体使字体的许多不同变化能够被整合到一个文件中, 而不是为每一种宽度, 重量或者样式都有一个单独的字体文件.
内容可见性(content-visibility)
一般来说, web应用中有些内容会在设备的可视区域之外.
我们可以使用CSS的content-visibility来跳过屏幕外的内容渲染. 也就是说, 如果你有大量的离屏内容(Off-screen content), 这会大幅减少页面渲染的时间.
这个是CSS新增的特性.
content-visibility有点类似于CSS的display和visibility, 但是其实现方式又与之不同.
content-visibility的关键能力, 它允许我们推迟我们选择的HTML元素渲染. 默认的情况下, 浏览器会渲染DOM树内所有可以被用户查看的元素. 用户可以看到视窗可视区域的所有元素, 并通过滚动查看页面内的其他元素. 一次渲染所有的元素, 可以让浏览器正确计算页面尺寸, 同时保持整个页面的布局和滚动条的一致性.
如果浏览器不渲染一些元素, 滚动的计算将变得非常复杂.
content-visibility会将分配给它的元素的高度视为0, 浏览器在渲染之前会将这个元素的高度变为0, 从而使我们的页面高度和滚动变得混乱. 但如果已经为元素或者其子元素显示的设置了高度, 这个行为就会被覆盖. 如果没有显示的设置高度, 并且因为显示设置height可能会带来一定的副作用而没有设置, 那么可以使用contain-intrinsic-size来确保元素的正确渲染, 同时保留延迟渲染的好处:
.card {
content-visibility: auto;
contain-intrinsic-size: 200px;
}
这会启用一个占位符尺寸来代替渲染的内容.
content-visibility的另一个能力, 是可以通过visibile和hidden实现元素的显示和隐藏能力, 类似display但是能提高渲染的性能. 因为他的渲染原理是不一样的.
display: none, 隐藏元素并破话渲染状态. 其隐藏和渲染具有一样的性能消耗