#一、渲染流程与性能优化
| 主题 | 核心知识点 | 概要 |
|---|---|---|
| 1.从URL到完整页面的流程 | • 从输入URL到页面呈现的全过程• 关键渲染路径 | 高度概括整个流程(网络请求、构建DOM/CSSOM、渲染、布局、绘制),建立宏观认知了解关键渲染路径的几大阶段。 |
| 2.构建DOM与CSSOM | • DOM树与CSSOM树的构建过程• CSSOM的渲染阻塞特性 | 解释为何DOM是渐进式的,而CSSOM是阻塞性的,为后续的优化打下基础。 |
| 3.JS的加载与阻塞 | • <script> 对解析的影响• async vs defer | 讨论JavaScript在渲染流程中的角色。解释默认脚本如何阻塞DOM构建引出async和defer作为优化手段。 |
| 4.渲染树、布局与绘制 | • 什么是渲染树 (Render Tree)• 什么是布局(Layout/Reflow)与绘制(Paint) | 将DOM和CSSOM结合,生成渲染树。详细解释布局(计算位置大小)和绘制(填充像素)这两个核心步骤。 |
| 5.高性能的秘密:图层与合成 | • 图层(Layer)与合成(Compositing)• transform 高性能的秘密 | 了解渲染的最后一步:合成。解释为何transform和opacity能跳过布局和绘制,实现GPU加速(现代前端动画优化的关键) |
| 6.性能优化实战:减少回流与重绘 | • 常见触发回流/重绘的操作• 批量修改DOM、读写分离 | 聚焦于如何通过具体的编码技巧(如DocumentFragment、批量修改样式)来避免不必要的渲染开销。 |
| 7.性能优化实战:合成层的妙用 | • will-change 的使用• 合成层的利与弊 | 深入讲解如何通过will-change等CSS属性手动或自动创建合成层,并讨论过度使用可能带来的问题。 |
| 8.度量与分析:核心Web指标 | • LCP, INP, CLS • 使用 DevTools 进行性能分析 | 用数据说话。介绍核心Web指标,并实战演示如何使用Lighthouse和Performance面板来发现、度量和验证渲染性能问题。 |
1.从URL到完整页面的流程
一、 从输入URL到接收数据
当你在浏览器地址栏输入一个URL并按下回车键时,一系列复杂的事件在幕后悄然发生:
URL解析 : 浏览器首先会解析你输入的URL,识别出其中的协议(如
https)、域名(如www.example.com)、路径等信息。DNS查询 : 浏览器会检查本地缓存,看是否已经有该域名对应的IP地址。如果没有,它会向DNS服务器发送请求,将域名“翻译”成服务器的IP地址。这就像通过一个地址簿查找一个人的电话号码。
建立TCP连接 : 获得IP地址后,浏览器会与目标服务器建立一个TCP连接。这是一个三次握手的过程,确保双方都准备好进行数据传输。
对于https协议,还需要进行TLS/SSL握手,以建立一个加密的安全通道。发送HTTP请求 : 连接建立后,浏览器会向服务器发送一个HTTP请求。这个请求包含了请求方法(通常是GET)、要访问的资源路径、以及一些请求头(Headers),用以告知服务器浏览器的类型、可接受的数据格式等信息。
服务器处理请求并返回响应 : 服务器接收到请求后,会进行处理(例如,从数据库查询数据,渲染HTML模板等),然后向浏览器返回一个HTTP响应。响应中包含了状态码(如
200 OK表示成功)、响应头和响应体(Response Body),响应体通常就是我们请求的HTML文档。
二、 关键渲染路径 (Critical Rendering Path)
当浏览器接收到服务器返回的HTML文档后,渲染引擎就开始工作了。这个将HTML、CSS和JavaScript转换成屏幕上像素的过程,被称为“关键渲染路径”。
- 构建DOM树:
浏览器从上到下解析HTML文档,将各种HTML标签(如
<html>,<body>,<div>)转换成一个树形结构,这就是文档对象模型(Document Object Model, DOM)。DOM是HTML文档在内存中的对象表示,它包含了文档的内容和结构。
- 构建CSSOM树 (Building the CSSOM Tree):
在解析HTML的过程中,如果遇到CSS(无论是通过
<link>标签引用的外部CSS文件,还是<style>标签内的内联样式),浏览器会开始解析CSS。它会将CSS规则转换成一个同样是树形结构的CSS对象模型(CSS Object Model, CSSOM)。
CSSOM包含了页面元素的样式信息。
- 构建渲染树 (Render Tree Construction):
DOM树和CSSOM树构建完成后,浏览器会将它们结合起来,创建一个渲染树(Render Tree)。
渲染树只包含需要显示在页面上的节点。例如,像
<head>这样本身不可见的标签,或者通过display: none;隐藏的元素,都不会出现在渲染树中。渲染树中的每个节点都包含了其在页面上的可见内容和样式信息。
- 布局 (Layout / Reflow):
有了渲染树,浏览器就可以计算出每个节点在屏幕上的确切位置和大小。这个过程称为布局(Layout),有时也叫回流(Reflow)。
浏览器从渲染树的根节点开始遍历,确定每个元素的几何信息(位置、尺寸)。
- 绘制 (Painting / Rasterizing):
布局阶段完成后,浏览器知道了每个元素应该在屏幕的哪个位置、画多大。接下来就是绘制(Painting)阶段。
浏览器会将渲染树中的每个节点转换成屏幕上的实际像素。这个过程涉及到将文本、颜色、图像、边框、阴影等所有可见部分绘制出来。
- 合成 (Compositing):
为了提高效率,浏览器可能会将页面的不同部分绘制在不同的图层(Layers)上。
合成(Compositing)步骤就是将这些图层按照正确的顺序合并在一起,最终显示在屏幕上。这对于处理复杂的动画和滚动效果尤其重要,因为它可以避免对整个页面进行重新绘制。
2.构建DOM与CSSOM
一、 DOM树的构建过程 (Incremental)
当浏览器从服务器接收到HTML文档的字节数据后,它会立即开始处理,这个过程是渐进式的,意味着浏览器无需等待整个文档加载完毕就可以开始解析和渲染页面。
构建流程如下:
字节 (Bytes) → 字符 (Characters): 浏览器根据文件指定的编码(例如
UTF-8)将原始的字节数据转换为字符。字符 (Characters) → 令牌 (Tokens): 浏览器将字符串形式的字符转换为W3C HTML5标准所规定的各种令牌(Token),例如
<html>、<body>等。每个令牌都具有特殊的含义和一组属性。这个过程被称为“词法分析”或“令牌化”。令牌 (Tokens) → 节点 (Nodes): 经过词法分析后,令牌会被转换成定义了其属性和规则的“对象”(即节点)。
节点 (Nodes) → DOM树 (DOM Tree): 由于HTML中的元素存在嵌套关系,这些节点之间会根据这种关系链接成一个树形数据结构,这就是文档对象模型(DOM)。
关键特性:渐进式构建
DOM的构建过程是自上而下、循序渐进的。浏览器每接收到一部分HTML,就会解析并生成对应的DOM节点,并将其添加到DOM树中。这使得浏览器可以在接收到全部HTML之前,就开始渲染页面的已就绪部分,这也是为什么我们有时会看到网页内容从上到下一点点加载显示出来的原因。
二、 CSSOM树的构建过程 (Blocking)
与构建DOM类似,当浏览器遇到CSS代码(无论是外部CSS文件、style标签还是内联样式)时,也会进行类似的处理。
构建流程如下:
字节 (Bytes) → 字符 (Characters): 将CSS文件字节转换为字符。
字符 (Characters) → 令牌 (Tokens): 将字符转换为CSS令牌。
令牌 (Tokens) → 节点 (Nodes): 将令牌转换为包含样式信息的CSS节点。
节点 (Nodes) → CSSOM树 (CSSOM Tree): 将CSS节点聚合成一个树形结构,即CSS对象模型(CSSOM)。
关键特性:渲染阻塞
与DOM不同,CSSOM的构建是渲染阻塞 (Render-Blocking)的。这意味着在CSSOM树完全构建完成之前,浏览器不会进行后续的渲染树构建、布局和绘制工作。
三、 为何DOM是渐进式的,而CSSOM是阻塞性的?
理解这个差异的核心在于样式的继承和覆盖规则。
对于DOM:一个父节点的结构并不会被其后的兄弟节点或子节点所改变。解析到
<div>时,浏览器就可以确定这是一个div元素,无需关心它后面会出现什么内容。因此,它可以一块一块地构建和显示。对于CSSOM:CSS的规则是层叠的。一个后面定义的样式规则可能会覆盖或修改前面定义的规则。
举个例子:
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="box">这是一个盒子</div>
</body>/* style.css */
body {
font-size: 16px;
}
.box {
color: blue;
}
/* ... 文件后面可能还有很多其他规则 ... */
div {
color: red; /* 这个规则会覆盖 .box 的颜色 */
}在这个例子中,.box 元素的颜色最初被设为蓝色,但随后又被 div 选择器覆盖为红色。
如果浏览器不等style.css文件完全下载和解析完毕就开始渲染页面,它可能会先用蓝色渲染.box,等解析到文件末尾时发现颜色应该是红色,于是不得不重新绘制这个元素。这种行为会导致页面内容的**“样式闪烁” (Flash of Unstyled Content, FOUC)**,用户体验极差。
为了避免这种情况,浏览器选择了一种更稳妥的策略:等待所有CSS都加载和解析完毕,构建出完整的、最终的CSSOM树之后,再用这个最终的样式信息去渲染整个页面。 这就是CSS之所以会“阻塞渲染”的原因。
小结与优化启示
DOM是增量的:HTML的解析和DOM构建可以流式进行,无需等待整个文档。
CSSOM是阻塞的:浏览器必须拥有完整的CSSOM才能进入下一渲染阶段,以确保元素样式的正确性。
这个基础知识直接引出了前端优化的一个核心原则:尽快、尽早地加载CSS,并减少CSS文件的大小,以缩短CSSOM的构建时间,从而缩短渲染被阻塞的时间,让用户能更快地看到页面的首次绘制。
3.JS的加载与阻塞
一、 JavaScript在渲染流程中的角色
JavaScript为网页带来了交互性,但它也是一把双刃剑。浏览器在解析HTML构建DOM的过程中,一旦遇到 <script> 标签,就会面临一个抉择。因为JavaScript有能力改变DOM的结构(例如使用 document.write() 或其他DOM操作API),所以浏览器必须谨慎行事。
二、 默认 <script> 标签的阻塞行为
当HTML解析器遇到一个普通的 <script> 标签(没有 async 或 defer 属性)时,会发生以下情况:
暂停DOM构建: HTML解析器会立即停止解析页面的其余部分。
下载脚本: 浏览器会发出请求,下载该脚本文件(如果是外部脚本)。
执行脚本: 脚本下载完成后,JavaScript引擎会立即执行它。
恢复DOM构建: 脚本执行完毕后,HTML解析器才会继续解析剩余的HTML文档。 这个过程被称为“解析器阻塞” (Parser Blocking)。如果脚本下载耗时很长,或者执行时间过久,整个页面的渲染都会被“卡住”,导致用户长时间看到一个白屏。这就是为什么我们通常建议将
<script>标签放在</body>标签之前的原因之一:确保浏览器能够先解析和渲染完整个页面的主要内容。
三、 优化手段:async 和 defer
为了解决脚本阻塞带来的性能问题,HTML5为 <script> 标签引入了两个布尔属性:async 和 defer。它们都告诉浏览器可以异步(在后台)下载脚本,而无需暂停DOM构建。
async(Asynchronous)
<script async src="path/to/script.js"></script>行为:
脚本的下载过程与HTML解析并行进行(异步下载)。
脚本下载完成后,HTML解析器会立即暂停,并执行该脚本。
执行完毕后,恢复HTML解析。
执行顺序: 多个带
async的脚本,它们的执行顺序是不确定的。哪个脚本先下载完,哪个就先执行。适用场景: 适用于那些不依赖DOM、也不依赖其他脚本的独立脚本。例如,网站分析、广告脚本等。
- defer (Deferred)
<script defer src="path/to/script.js"></script>行为:
脚本的下载过程与HTML解析并行进行(异步下载)。
脚本下载完成后,并不会立即执行。它会等待整个HTML文档解析完毕(即
</html>标签被解析后),然后在 DOMContentLoaded 事件触发之前执行。
执行顺序: 多个带 defer 的脚本,它们的执行顺序会按照它们在HTML中出现的顺序来依次执行。
适用场景: 适用于那些需要操作DOM,或者脚本之间有依赖关系的场景。这是目前最推荐的脚本加载优化方案。
四、 对比总结
| 属性 | 解析器阻塞 | 脚本下载 | 脚本执行时机 | 执行顺序 |
|---|---|---|---|---|
| (无) | 是 | 同步 | 下载后立即执行 | 按照在HTML中的顺序 |
| async | 否 (仅在执行时阻塞) | 异步 | 下载后立即执行 | 不保证顺序 |
| defer | 否 | 异步 | HTML解析完成后,DOMContentLoaded之前 | 按照在HTML中的顺序 |
通过合理使用 async 和 defer,你可以显著减少JavaScript对关键渲染路径的阻塞,从而加快页面加载速度,提升用户体验。
4.渲染树、布局与绘制
一、 什么是渲染树 (Render Tree)?
在浏览器成功构建了DOM树(代表文档结构)和CSSOM树(代表文档样式)之后,它需要将这两者结合起来,才能知道最终要“画”什么东西在屏幕上。这个结合的产物就是渲染树 (Render Tree)。
目的:渲染树是页面所有可见内容的结构化表示。它的任务是确定哪些节点需要被渲染,以及它们应用了哪些样式。
构建过程:
浏览器从DOM树的根节点开始遍历。
对于每个遍历到的节点,它会去CSSOM树中查找匹配的样式规则并应用。
最终,它会为每个可见节点生成一个渲染树上的节点。
与DOM树的区别:渲染树和DOM树不是一一对应的。
不可见元素被忽略:渲染树不包含任何在视觉上不可见的元素。例如:
<head>、<script>、<meta>等本身不产生视觉输出的标签。通过CSS设置了 display: none; 的节点(及其所有后代节点)。
注意:通过 visibility: hidden; 或 opacity: 0; 隐藏的元素会出现在渲染树中,因为它们仍然占据着页面空间,只是不可见而已。
简单来说,DOM树是关于“内容和结构”,而渲染树是关于“要画什么以及如何画”。
二、 什么是布局 (Layout / Reflow)?
一旦渲染树构建完成,浏览器就知道需要渲染哪些节点以及它们的样式,但还不知道它们在屏幕上的确切位置和大小。布局 (Layout)步骤就是为了计算这些几何信息。
目的:计算出渲染树中每个节点在设备视口(viewport)内的精确位置和尺寸。这个过程也常被称为回流 (Reflow)。
工作流程:
浏览器从渲染树的根节点开始,进行一次遍历。
它将所有元素的大小和位置信息输出为一个“盒模型”(Box Model),这个模型精确地捕捉了每个元素在页面上的位置(x, y坐标)和尺寸(宽度, 高度)。
相对单位(如 %, em, rem, vw)会被计算成屏幕上的绝对像素值
回流 (Reflow):布局是一个从头到尾的完整过程。但是,当页面上某个元素的几何属性(如宽度、高度、边距、边框)发生变化时,可能会影响到其他元素的位置。浏览器需要重新计算受影响部分的布局,这个重新计算的过程就叫做“回流”。回流是一个非常耗费性能的操作,因为一个微小的改动也可能导致整个页面的重新布局。
可以把布局想象成画一张建筑蓝图:虽然你知道需要一扇门和一扇窗户(渲染树),但你需要通过布局来确定门和窗户在墙上的确切尺寸和位置。
三、 什么是绘制 (Paint)?
在布局阶段确定了所有可见元素的确切几何信息后,浏览器终于可以把它们"画"到屏幕上了。这个过程就是绘制 (Paint)。
目的:将渲染树中的每个节点转换为屏幕上的实际像素。
工作流程:
布局阶段结束后,浏览器得到了所有元素的精确“蓝图”。
绘制阶段会遍历渲染树,调用渲染器的绘制函数,将元素的背景、颜色、文字、边框、阴影等所有视觉效果填充到屏幕的对应区域。
图层与合成 (Layers & Compositing):为了提高效率,浏览器并不会把所有东西都画在一个巨大的画布上。它会智能地将页面内容提升到不同的图层 (Layer)上。
当某个元素发生变化时(例如一个CSS动画),如果它位于独立的图层上,浏览器就只需要重绘这一个图层,而不需要重绘整个页面。
最后,浏览器会将所有这些独立的图层按照正确的顺序合成 (Composite)在一起,形成最终的屏幕画面。
可以把绘制想象成装修工人根据建筑蓝图(布局),用油漆(填充像素)把墙壁刷上颜色、把窗框描上边。
5.高性能的秘密:图层与合成
一、 渲染流程的回顾
在我们深入探讨图层之前,让我们再次回顾一下简化的渲染流程:
JavaScript → Style → Layout (布局) → Paint (绘制) → Composite (合成)
大多数CSS属性的更改(例如 width, height, left, top)都会触发 Layout 和 Paint,这被称为 回流(Reflow) 和 重绘(Repaint),是浏览器中非常耗费计算资源的操作。如果动画的每一帧都经历这个过程,就很容易导致卡顿。
二、 图层 (Layer)
为了优化这个过程,浏览器引入了图层 (Layer)的概念。你可以把网页想象成一个Photoshop或Sketch文件,它不是一个单一的平面,而是由多个堆叠在一起的图层组成的。
默认情况: 在默认情况下,页面上的所有元素都位于同一个默认图层中。
提升为独立图层: 在某些特定情况下,浏览器会为某个元素创建一个新的、独立的图层。这个过程被称为“层提升”(Layer Promotion)。
常见的触发层提升的CSS属性和HTML元素包括:
3D 或透视变换属性:transform: translate3d(x,y,z), rotate3d(...), perspective(...) 等。
使用了 will-change 属性(这是一个专门用于性能优化的属性,可以提前告知浏览器该元素将要发生变化)。
<video>和<canvas>元素。具有 position: fixed 的元素。
一些复杂的动画,例如 opacity 和 transform 的动画。 当一个元素被提升到独立的图层后,它就拥有了自己的“位图”(Bitmap),可以独立于页面的其他部分进行变换和绘制。
三、 合成 (Compositing)
有了分离的图层后,渲染流程的最后一步就是合成 (Compositing)。
合成是指浏览器将所有独立的图层按照正确的顺序和位置组合、叠加,最终生成一个完整的屏幕图像的过程。
关键在于,这个合成工作可以被 GPU (图形处理器) 高效地执行。GPU专门为图形计算而设计,处理位图的移动、缩放、旋转和透明度变化等操作速度极快。
四、 transform 和 opacity 高性能的秘密
现在,我们可以揭示 transform 和 opacity 实现高性能动画的秘密了:
跳过布局和绘制: 当你为一个被提升到独立图层的元素应用 transform (移动、缩放、旋转) 或 opacity (改变透明度) 动画时,浏览器能够识别出这个变化不会影响到其他元素的布局 (Layout),因此可以完全跳过 Layout 和 Paint这两个最耗时的阶段。
GPU 加速: 浏览器只需将这个已经绘制好的图层(作为一个位图)发送给 GPU。然后,在动画的每一帧,CPU都不需要做任何事情,只需通知 GPU 去执行这个图层的变换(例如,将其向右移动几个像素)或改变其透明度即可。
这个过程的渲染路径被大大缩短:
JavaScript → Style → Composite
由于整个过程由高度专业化的 GPU 直接处理,动画可以非常流畅,轻松达到 60fps (每秒60帧),用户感知不到任何卡顿。这也就是我们常说的“GPU加速”。
五、结论与最佳实践
优先使用 transform 和 opacity: 在实现位移动画、缩放、旋转或淡入淡出效果时,应始终优先使用 transform 和 opacity 属性,而不是去更改 top, left, margin 或 width, height。
合理利用 will-change: 如果你知道一个元素即将进行动画,可以使用 will-change: transform, opacity; 来提前“暗示”浏览器,让其为该元素创建独立图层,做好优化准备。
避免滥用图层: 虽然图层可以提升性能,但创建过多的图层会占用大量内存和GPU资源,反而可能导致性能下降。因此,应有节制地使用,避免不必要的层提升。
6.性能优化实战:减少回流与重绘
一、 理解回流 (Reflow) 与重绘 (Repaint)
为了优化,我们首先要精确理解这两个概念:
回流 (Reflow): 当元素的尺寸、结构或位置发生改变时,浏览器需要重新计算元素的几何属性(即重新进行“布局”阶段),这个过程称为“回流”。回流会影响其周围甚至整个页面的其他节点,是开销非常大的一种操作。回流必然导致重绘。
重绘 (Repaint): 当元素的视觉样式(如
color,background-color)发生改变,但其几何属性没有变化时,浏览器会跳过布局阶段,直接为该元素重新绘制(即重新进行“绘制”阶段),这个过程称为“重绘”。相比回流,重绘的开销较小。
优化的核心思想:避免不必要的回流,将多次回流合并为一次。
二、 常见触发回流与重绘的操作
了解触发时机是避免它们的第一步。
常见触发回流 (Reflow) 的操作:
页面首次渲染:这是不可避免的回流。
DOM节点操作:增加、删除或修改DOM节点。
元素几何属性变更:
修改width,height,padding,margin,border等。元素位置变更:
position,top,left等。字体变更:修改
font-size,font-family等。窗口尺寸变更:
resize事件触发。获取特定的布局属性:这是最容易被忽略的一点!当你用JavaScript读取以下属性时,浏览器为了返回精确的值,会强制立即执行一次回流:
offsetTop,offsetLeft,offsetWidth,offsetHeightscrollTop,scrollLeft,scrollWidth,scrollHeightclientTop,clientLeft,clientWidth,clientHeightgetComputedStyle()
仅触发重绘 (Repaint) 的操作:
- 修改
color,background-color,visibility,outline,box-shadow等不影响布局的样式。
三、 优化实战:具体的编码技巧
1. 批量修改DOM
问题:当需要向DOM中添加多个元素时,如果直接在循环中逐个appendChild,每次操作都会触发一次回流。
// 反例:会触发多次回流
const container = document.getElementById('container');
for (let i = 0; i < 100; i++) {
const el = document.createElement('p');
el.textContent = i;
container.appendChild(el); // 每次循环都可能触发回流
}优化技巧:
- 使用
DocumentFragment:DocumentFragment是一个存在于内存中的轻量级DOM容器,对它的所有操作都不会触发回流。我们可以先把所有新节点添加到Fragment中,最后再将Fragment一次性添加到真实DOM中。
// 优例:只触发一次回流
const container = document.getElementById('container');
const fragment = document.createDocumentFragment(); // 创建文档片段
for (let i = 0; i < 100; i++) {
const el = document.createElement('p');
el.textContent = i;
fragment.appendChild(el); // 在内存中操作,不触发回流
}
container.appendChild(fragment); // 一次性插入,触发单次回流- 隐藏元素后操作:将需要多次操作的元素先
display: none,完成所有操作后,再恢复显示。这会产生两次回流(隐藏和显示),但远优于N次。
const container = document.getElementById('container');
container.style.display = 'none'; // 回流 1
// ... 在此进行多次DOM操作 ...
container.style.display = 'block'; // 回流 22. 批量修改样式与读写分离
问题:在循环中交替读取元素的布局属性和设置样式,会导致“布局抖动”(Layout Thrashing)。浏览器被迫在每次循环中都强制回流。
// 反例:布局抖动
const elements = document.querySelectorAll('.box');
for (let i = 0; i < elements.length; i++) {
// 读取 offsetWidth (读操作),强制浏览器回流
const width = elements[i].offsetWidth;
// 设置 style.width (写操作),再次导致布局变更
elements[i].style.width = width + 10 + 'px';
}优化技巧:读写分离 (Read/Write Separation)
将读操作和写操作完全分开,先批量读取所有需要的值并缓存,然后批量进行样式更新。
// 优例:读写分离
const elements = document.querySelectorAll('.box');
const widths = [];
// 读操作
for (let i = 0; i < elements.length; i++) {
widths.push(elements[i].offsetWidth);
}
// 写操作
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = widths[i] + 10 + 'px';
}优化技巧:使用 classList
对于复杂的样式变更,推荐将样式组合定义在一个CSS类中,然后通过JavaScript切换class。浏览器可以更高效地处理class的变更。
/* style.css */
.large-box {
width: 200px;
height: 200px;
border: 2px solid blue;
}/* style.css */
.large-box {
width: 200px;
height: 200px;
border: 2px solid blue;
}通过以上方法,我们可以有效减少渲染引擎的工作量,让网页动画和交互变得更加流畅。
7.性能优化实战:合成层的妙用
核心知识点
will-change 的使用
合成层的利与弊
一、 如何创建合成层?
浏览器在渲染过程中,可能会自动为某些元素创建合成层,这被称为隐式合成。但作为开发者,我们有时需要更精确地控制,这就是显式合成。
- 隐式合成 (Implicit Compositing)
在上一节我们提到,当一个元素满足特定条件时,浏览器会“智能地”将其提升到一个独立的合成层。常见触发条件包括:
3D变换:
transform: translate3d(0, 0, 0);,transform: rotate3d(...)等。position: fixed;拥有
opacity或transform动画的元素。<video>,<canvas>,<iframe>元素。使用了
filter属性的元素。
在过去,开发者会使用一些“hack”手段来强制创建合成层,最著名的就是 transform: translateZ(0); 或 transform: translate3d(0, 0, 0);。虽然这能起作用,但其语义不清晰,且不够规范。
- 显式合成:
will-change
为了解决上述 hack 的问题,CSS 引入了 will-change 属性。这是一个专门为性能优化设计的“提示性”属性。
will-change 的作用是提前告知浏览器:“嘿,这个元素的某个属性即将发生变化,请你提前做好准备和优化!”
如何使用?
它的值是你期望改变的 CSS 属性名。
/* 告诉浏览器,这个元素的 transform 属性即将改变 */
.animating-element {
will-change: transform;
}
/* 也可以指定多个属性 */
.another-element {
will-change: transform, opacity;
}当浏览器看到 will-change: transform; 时,它通常会提前将该元素提升到一个独立的合成层,这样当 transform 真的发生变化时,就可以直接进入高效的“合成”阶段,跳过布局和绘制。
二、 合成层的利与弊
使用合成层就像一把双刃剑,用得好能大幅提升性能,用得不好则会适得其反。
合成层的好处
极致流畅的动画: 这是最大的好处。将动画元素提升到合成层后,其
transform和opacity的变化由 GPU 直接处理,可以轻松实现 60fps 的丝滑动画,避免了主线程的计算压力,从而避免了页面卡顿。隔离影响范围: 当一个元素在自己的合成层上运动时,它不会引发整个页面的重绘(Repaint)或回流(Reflow)。这就像它在一个独立的画板上作画,不会影响到背景画板,极大地减少了不必要的渲染开销。
合成层的“弊”
内存消耗剧增: 每创建一个新的合成层,就意味着需要分配新的内存(RAM)和显存(VRAM)来存储这个图层的位图信息。如果页面上存在大量合成层,内存占用会急剧上升,尤其是在移动端和低端设备上,可能会导致页面响应变慢甚至崩溃。
初始化开销: 将一个元素提升到合成层并非“免费”的操作。浏览器需要为其分配内存、生成纹理、并将其上传到 GPU。这个过程本身需要时间和计算资源,有时可能会导致页面初次渲染时出现微小的延迟或闪烁。
潜在的渲染问题: 在某些情况下,强制提升图层可能会导致一些视觉上的小问题,例如字体渲染可能会变得有些模糊,因为文本被当作位图纹理进行了处理。
三、 最佳实践与总结
不要过早优化,更不要滥用:不要给页面上所有可能动的元素都加上
will-change。只有当一个复杂的动画确实出现了性能问题时,才考虑使用它进行优化。用完就“扔”:
will-change的最佳实践是“按需使用”。通过 JavaScript 在动画即将开始时添加该属性,在动画结束后移除它。
const element = document.querySelector('.my-element');
// 动画开始前
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});
// 动画结束后
element.addEventListener('animationend', () => {
element.style.willChange = 'auto'; // 'auto' 是默认值
});- 借助开发者工具:使用 Chrome 开发者工具的
Layers(图层) 面板。这是一个强大的工具,你可以实时查看页面上的合成层情况,诊断哪些元素被提升了,以及是否存在不必要的层,从而帮助你做出正确的优化决策。
结论:合成层是实现高性能前端动画的关键技术。通过理解其工作原理,并明智、审慎地使用 will-change,你可以在保证性能的同时,避免其带来的副作用,创造出既华丽又流畅的用户体验。
8.度量与分析:核心Web指标
核心知识点
LCP, INP, CLS
使用 DevTools 进行性能分析
一、 核心Web指标 (Core Web Vitals)
核心Web指标是Google提出的一组以用户为中心的指标,用于量化网页的关键体验维度。关注这些指标并进行优化,能显著提升用户满意度。
- LCP (Largest Contentful Paint) - 最大内容绘制
度量对象: 加载性能。它测量的是视口内可见的最大图像或文本块完成渲染的时间点,相对于页面首次开始加载的时间。
目标: 提供良好的用户体验,LCP应在 2.5秒 内发生。
意义: 一个快速的LCP能让用户感觉到页面的主要内容很快就加载出来了,认为页面是有用的。
- INP (Interaction to Next Paint) - 下次绘制的交互
度量对象: 交互响应性。它测量的是从用户发起交互(如点击、轻触或键入)到浏览器绘制下一帧之间所经过的时间。INP衡量的是整个页面生命周期内的所有交互,并报告最慢的那一次(或接近最慢的)。
目标: 提供良好的用户体验,INP应低于 200毫秒。
意义: 一个低的INP意味着页面对用户的操作响应迅速,感觉流畅不卡顿。(注意:INP已于2024年3月取代FID成为核心Web指标)
- CLS (Cumulative Layout Shift) - 累积布局偏移
度量对象: 视觉稳定性。它测量的是页面在整个生命周期内,所有意外发生的布局偏移的累积分数。
目标: 提供良好的用户体验,CLS分数应低于 0.1。
意义: 一个低的CLS分数意味着页面元素稳定,不会因为图片、广告或字体加载而突然跳动,避免用户误点或阅读困难。
二、 使用 DevTools 进行性能分析
Chrome DevTools是发现和解决性能问题的强大盟友。我们主要使用两个面板:Lighthouse和Performance。
- Lighthouse:自动化审计与报告
Lighthouse 是一个自动化的网站审计工具,它能快速生成一份关于性能、可访问性、SEO等方面的综合报告,非常适合作为性能分析的起点。
如何使用:
打开Chrome DevTools (F12或Ctrl/Cmd+Shift+I)。
切换到 Lighthouse 面板。
在 Categories 中勾选 Performance。
选择 Device (通常建议先分析 Mobile,因为移动端网络和性能限制更多)。
点击 Analyze page load 按钮。
如何解读报告:
总分: Lighthouse会给出一个0-100的性能总分,让你对网站性能有一个直观的印象。
核心指标: 报告会清晰地列出 LCP, CLS, TBT (Total Blocking Time, 可作为INP的代理指标)等指标的测量值,并用绿、橙、红三色标示其表现好坏。
优化建议 (Opportunities): 这是报告最有价值的部分。Lighthouse会给出具体的优化建议,例如“减少未使用的JavaScript”、“启用文本压缩”、“为图片提供适当的尺寸”等,并预估这些优化能节省的加载时间。
Lighthouse的角色: 发现和初步度量问题。它告诉你“哪里”慢了,并给出宏观的优化方向。
- Performance:深入分析与验证 当你通过Lighthouse发现了问题后,就需要使用Performance面板来深入分析问题的根本原因。它能记录下页面加载或交互过程中的所有事件,并以时间线的形式呈现,让你洞察浏览器工作的每一个细节。
如何使用:
切换到 Performance 面板。
点击左上角的 录制按钮 (Record) 或按下 Ctrl/Cmd + E。为了分析加载性能,你可以点击 Start profiling and reload page 按钮。
让页面完成加载,或者执行你想要分析的交互操作(比如点击一个卡顿的按钮)。
点击 Stop 停止录制。
如何分析火焰图 (Flame Chart):
时间线概览: 顶部是时间线,包含CPU活动、帧率(FPS)等。出现红色长条通常意味着发生了长时间运行的任务。
Timings & Experience:
在 Timings 泳道中,你可以清晰地看到 LCP 标记,准确定位最大内容绘制的发生时刻。
在 Experience 泳道中,DevTools会标记出每一次 Layout Shift。点击它可以查看详情,找出导致布局偏移的具体元素,帮助你修复CLS问题。
主线程 (Main): 这是分析的重点。
长任务 (Long Tasks): 任何执行时间超过50ms的任务都会被标记为长任务(右上角有红色三角)。这些任务会阻塞主线程,导致页面无法响应用户输入,是造成INP差的主要原因。你需要关注这些黄色的“Scripting”块,看是哪些JavaScript代码执行时间过长。
火焰图颜色: 黄色代表脚本执行(Scripting),紫色代表渲染和布局(Rendering & Layout),绿色代表绘制(Painting)。通过分析各个部分所占的时间,可以判断性能瓶颈是在JS执行阶段还是在渲染阶段。
Performance面板的角色: 深入分析和验证问题。它帮助你定位造成LCP延迟、INP过高或CLS发生的具体代码和事件,从而进行精确优化。
