Flip动画
前言
最近在做复图标库功能时,感觉这个功能在使用上有些“生硬”。如随机删除一个图标,后面的元素在视觉上是“瞬间移动”过来补位的。想着做个小优化,简单加个动画效果吧。
看起来确实“花里胡哨”了,实现也很简单,
<ul><transition-group appear tag="ul"><!--图片循环--></transition-group>
</ul>
为什么简单的设置几个样式规则,元素就可以平滑的移动到对应的位置?如果我们手写这个功能,应该如何考量和设计?
插播
先简单回顾几个基本知识点(更细节的内容这里不展开讨论)。
浏览器的渲染流程
虽然不同的浏览器内核在渲染流程上稍有不同,但大体上是一致的,主要步骤如下:
- DOM树构建:解析HTML,生成DOM树
- CSSOM树构建:解析CSS,生成CSSOM树
- 渲染树构建:将DOM树和CSSOM树结合,生成渲染树(Render Tree)
- 布局:根据渲染树,计算元素的的几何信息(位置,大小)
- 绘制:根据布局信息,把每一个图层转换为像素,渲染在屏幕上
简单示意图如下
由图可知:
- CSS解析不会阻塞DOM解析,但会阻塞DOM渲染
- JavaScript会阻塞DOM解析,进而阻塞DOM渲染
- 浏览器碰到script标签(没有defer/async属性时)就会触发页面渲染。如果前面的CSS资源尚未加载完毕,浏览器会等待它加载完毕后再执行脚本
FPS
简单来说,FPS是浏览器的每秒的渲染帧数,大多数设备的刷新率是60FPS,一般来说FPS越低页面就会越卡顿。
像素管道
标准上每一帧约为16.7ms。但浏览器需要花费时间将新帧绘制到屏幕上,大约只有10ms执行代码。如果无法满足这个要求,帧率会降低,出现卡顿。
先看一张经典图:
- JavaScript(代码执行)。一般的纯前端阻塞都是来自JS,JS线程的运行本身就会阻塞UI线程(除WebWorker外)。所以执行长时间的同步代码会占用每帧的渲染时间。
- Style(样式计算)。利用CSS匹配器计算元素的最终样式。
- Layout(布局)。当样式规则应用后,浏览器开始计算元素在屏幕上显示的大小及位置,这个过程中一个元素的变动可能会影响到另一个元素,从而引起回流。
- Paint(绘制)。绘制就是简单的像素填充,包括文本、颜色、图片、边框、阴影等可视部分。因为网页样式是层级结构,所以绘制操作会在每一层进行。
- Composite(合成)。合成操作会按照正确的层级顺序绘制到屏幕上,以保证渲染的正确性。层级错误的话会导致样式错乱,如底层的元素显示到上层等。
上述过程为理论标准过程,但实际上并非每一帧都会完整执行这五个步骤,不管我们通过JS或者css动画去完成一些动作,本质上都与【回流】(重排)和【重绘】两个概念相关。所以通常对于指定帧有3种运行方式。
-
修改元素的"layout"属性(几何属性,如宽、高、位置等),浏览器会自动重排页面,受到影响的元素都需要重新绘制,且最终绘制的元素需要进行合成。重排经过了管道的每一步,对性能影响比较大。
-
修改元素的"paint only"属性(外观属性,,如颜色、阴影等),不影响页面布局,此时浏览器会跳过布局,但仍执行绘制。
-
修改元素的一个不需要布局和重绘的属性(如透明度、transform变形等),浏览器只执行合成,性能较好。
由上可知,JavaScript、Style和Composite三个阶段是无法避开的。而执行的阶段越少,耗时就越短,每秒渲染的帧数就越高。
简单优化
布局的过程实际上就是回流的过程,这一步几乎会对整个页面重新计算排版,性能开销较大。而绘制是像素填充的过程,是管道中运行时间最长的任务。所以针对JS动画,通常我们可以采用如下优化方法
- 使用requestAnimationFrame来代替定时器
- 大量计算任务可以使用Web Worker执行
- 更改DOM时使用微任务
- 降低CSS选择器的复杂度
- 避免强制同步布局
- 合理设计z-index层数
- 频繁修改属性的元素,可使其脱离文档流
- 渲染层提升为合成层,利用GPU加速绘制
下面简单介绍其中的两点(后面会用到)
requestAnimationFrame
上面提到了,每一帧必须保证JS运行时间小于10ms,才能给样式计算、布局、绘制留出充足的时间。那么,是否我们满足了这个条件,且保证每一帧耗时都在16.7ms之内,就能保证不丢帧呢?
其实不必然,这取决于JS执行方式,如使用定时器(setTimeout/setInterval)来实现。因为定时器无法保证回调函数的真正执行时机,它可能在某一帧的开始、中间、结束时执行,有可能导致丢帧。
使用requestAnimationFrame,会使浏览器在下一次重绘之前调用传入的动画函数,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。
如下图所示,我们分别使用setTimeout和requestAnimationFrame把元素平移800像素,差异还是很明显的。
避免强制同步布局(FSL)
在上面的浏览器的渲染流程中提到,浏览器中的页面的渲染过程可以分为计算布局和绘制两个阶段。布局是指浏览器根据DOM树、CSS样式和其他因素来确定每个元素在页面中的大小和位置等相关属性。绘制是指将计算好的布局信息转化为可视元素显示在屏幕上。
通常情况下,浏览器会对布局操作进行优化,例如使用异步方式进行布局(也称为增量布局或延迟布局)。这意味着当对DOM进行修改时,浏览器不会立即触发布局,而是等待一段时间,将多个连续的布局操作合并在一起进行。这样可以提高性能和响应速度。
然而,有些情况下,我们需要在修改DOM后立即获得最新的布局信息。这时我们可以使用强制同步布局的方法来实现。强制同步布局的方式往往是通过触发获取某些属性值的操作,例如读取元素的位置、大小、滚动等属性,或者通过访问offsetTop
、offsetWidth
、offsetHeight
属性来实现。这样会迫使浏览器立即执行布局阶段,以确保获取的属性值是最新的。
当然,单个FSL影响不大,如果触发了布局抖动,会导致严重的性能问题。举个简单的例子,批量修改元素宽度
// 把子元素的宽设置成与外部容器一样
const container = document.querySelector('.container');
const items = document.querySelectorAll('.item');
// 遍历所有子节点,重新设置width
for (var i = 0; i < items.length; i++) {const width = container.offsetWidth;items[i].style.width = width + 'px';
}
实际上,每次更改样式,都会导致刚刚执行的布局失效。因为更改了新的样式,下一次读取宽度时,浏览器需要重新布局,直到循环结束,循环期间的布局实际上都是“无效”的。
我们可以在谷歌性能在线测试网站(https://googlechrome.github.io/devtools-samples/jank/)进行测试。观察性能面板,可以看到给出了警告提示:强制回流可能是性能瓶颈。定位到相关代码可以看到正是获取元素位置信息造成的。
FLIP
进入正题。严格来说,FLIP并不是特定的代码实现或者框架,而是一种思路。FLIP技术以一种高效的方式来动态的改变DOM元素的位置和尺寸,而无需关注布局是如何计算或渲染的。在改变的过程中赋予一定的动效,从而达到动画的目的。
##核心思想
FLIP由四个单词组成:First, Last, Invert, Play。
First:
元素的初始状态。
Last
元素的最终状态。
Invert
反转。计算初始状态和最终状态的属性差异,如宽高、位置、透明度等,设置对应的规则进行反转,使其看起来还在初始状态(这点比较绕,下面有具体示例介绍)。
Play
执行。移除对应规则,使其平滑变化到最终状态。
具体实现
下面来看一个简单示例:把第一个元素移动到最后一个位置(原生写法)。
按照FLIP的设计原则,我们来看一下如何实现。
<!--html部分-->
<button class="button">修改第一个元素位置</button>
<ul class="list"><li class="list-item active">元素1</li><li class="list-item">元素2</li><li class="list-item">元素3</li><li class="list-item">元素4</li><li class="list-item">元素5</li><li class="list-item">元素6</li>
</ul>
-
First:获取初始位置
// 元素 const btn = document.querySelector('button'); const list = document.querySelector('.list'); const firstItem = document.querySelector('.list-item:first-child'); // 获取位置方法(这个例子只有上下平移,只记录top值即可) function getLocation() {const rect = firstItem.getBoundingClientRect();return rect.top; } // First:获取初始位置 const start = getLocation(); console.log('first:', start);
-
Last:获取最终位置
// 移动元素 btn.onclick = () => {list.insertBefore(firstItem, null);// Last:获取最终位置const end = getLocation();console.log('last:', end) }
效果如下:获取到了起始位置和最终位置
到了这里,大家可能会有疑问,这都变化完了,拿到的两个位置信息有什么用?别急,下面才是重点。
-
Invert:规则反转
这里大家可以暂停一下,思考一个问题,当我获取到最终位置的时候,看到的是变化前的页面还是变化后的页面?
其实看到的是变化前的页面,这个现象才是核心所在。这里大家可能会有疑问,明明看到元素动了啊,为什么还是变化前的页面?我们可以来验证一下:
// 移动元素 btn.onclick = () => {list.insertBefore(firstItem, null);// Last:获取最终位置const end = getLocation();console.log('last:', end)// 模拟执行js代码const start = Date.now();while (Date.now() - start < 2000) {console.log('模拟执行js代码')} }
可以看到当我们获取元素最终位置的时候,显示的还是未变化前的页面。为什么会这样?这就是我们上面提到的当获取元素布局信息的时候,会触发强制同步布局,浏览器立即执行布局,但还没有到绘制阶段。
接下来计算偏移值,设置变化规则即可。注意这里我们要用开始状态减去最终状态,做一个反转,使其看起来还在原来位置(因为动画是从开始位置到最终位置的)
// Invert:反转 const dis = start - end; firstItem.style.transform = `translateY(${dis}px)`; console.log('invert:', dis)
可以看到,DOM结构已经发生变化,元素也反转回到了初始位置。
-
Play:执行
这里我们只需要设置一下transition效果,并移除掉transform即可(这样元素就会回到它现在真实的位置)。这里我们使用requestAnimationFrame来实现。
// play回调 function raf(callback) {requestAnimationFrame(() => {requestAnimationFrame(callback);}) } // 移动元素 btn.onclick = () => {list.insertBefore(firstItem, null);// Last:获取最终位置const end = getLocation();console.log('last:', end)// Invert:反转const dis = start - end;firstItem.style.transform = `translateY(${dis}px)`;console.log('invert:', dis)// Play:执行raf(() => {firstItem.style.transition = 'transform 1s';firstItem.style.removeProperty('transform');console.log('play')}) }
到这里我们就实现了一个很简单的FLIP动画。当然这个例子只是对一个元素做了效果,我们同样可以对其它元素做相应的操作。
简单封装
以Vue为例,做如下封装,快速实现图片库的随机插入、删除、乱序等动画效果。
// 记录位置
recordPosition(nodes) {return nodes.reduce((prev, node) => {const rect = node.getBoundingClientRect();const { left, top } = rect;if (node.attributes.card.value) {prev[node.attributes.card.value] = { left, top, node };}return prev;}, []);
},
// 设置动画
async scheduleAnimation(update) {// 获取子节点(nodes:参与动画的节点)const prev = Array.from(nodes);// 记录子节点初始位置const prevRectMap = this.recordPosition(prev);// 执行数据变化:如增删改等操作update()await this.$nextTick();// 记录子节点现在位置const currentRectMap = this.recordPosition(prev);// 遍历对比Object.keys(prevRectMap).forEach((node) => {const currentRect = currentRectMap[node];const prevRect = prevRectMap[node];// 计算反转值const invert = {left: prevRect.left - currentRect.left,top: prevRect.top - currentRect.top,};// 设置动画const keyframes = [{transform: `translate(${invert.left}px, ${invert.top}px)`,},{ transform: "translate(0, 0)" },];const options = {duration: 300,easing: "linear",};// 执行动画currentRect.node?.animate(keyframes, options);})
}
// 调用
this.scheduleAnimation(()=>{// TODO... 对元素增删改
})
图片库的相关实现
列表的相关实现
transtion-group
当然在Vue中已经内置了相关功能,查看源码(src/platforms/web/runtime/components/transition-group.ts。可以看到在初始render时记录原始位置,在updated中记录最新位置,并计算偏移参数,设置动画效果,执行完成之后,移除相关属性。
React可以参考react-transition-group或者react-flip-toolkit等插件。
为什么要用FLIP
对于明确知道元素的起止状态的动画,如从坐标(0,0)移动到(100,100),或透明度从0变化到1等,直接设置相应的规则即可。而对于一些无法明确起止状态的动画,使用FLIP就简单多了,避免了我们手动计算维护元素的状态。
后记
- 我们可能在写一些过渡效果的时候,无意中用到了FLIP动画,但更需要了解相关原理
- 合理使用动效让平台的操作更加平滑(就如同使用loading让用户感知网站确实在响应)
- 要确保多个连续的FLIP动画之间互不影响,或者说要预留出一定的时间给到相关的计算过程
- 实现动画当然有多种方式,结合项目实际选择合适的技术(如通过纯CSS实现瀑布流而非JS的形式)
- 使用Web Animations API可以更简单实现一个动画
参考文档
让你的网页更丝滑
性能优化之关于像素管道及优化
前端动画必知必会
相关文章:

Flip动画
前言 最近在做复图标库功能时,感觉这个功能在使用上有些“生硬”。如随机删除一个图标,后面的元素在视觉上是“瞬间移动”过来补位的。想着做个小优化,简单加个动画效果吧。 看起来确实“花里胡哨”了,实现也很简单, …...

Java通过RAG构建专属知识问答机器人_超详细
RAG:融合检索与生成的文本精准生成技术 检索增强生成(RAG)是一种技术,它通过结合检索模型和生成模型来提高文本生成的准确性。具体来说,RAG首先利用检索模型从私有或专有的数据源中搜索相关信息,然后将这些…...

2.1 使用点对点信道的数据链路层
欢迎大家订阅【计算机网络】学习专栏,开启你的计算机网络学习之旅! 文章目录 前言1 通信信道类型2 数据链路3 帧4 透明传输5 差错检测 前言 在计算机网络通信中,数据链路层起着关键作用。它为直接相连的网络设备之间提供可靠的数据传输服务。…...

台式机来电自启动设置
在前司时,由于有些工作需要用到台式机,且一到节假日或者突然停电等情况,电脑每次都需要自己手动开机,后来研究了一下,发现可以在BIOS里面更改设置,从而变成关机的情况下,只要来电就能自动开机&a…...

【最新华为OD机试E卷-支持在线评测】考勤信息(100分)多语言题解-(Python/C/JavaScript/Java/Cpp)
🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 💻 ACM金牌🏅️团队 | 大厂实习经历 | 多年算法竞赛经历 ✨ 本系列打算持续跟新华为OD-E/D卷的多语言AC题解 🧩 大部分包含 Python / C / Javascript / Java / Cpp 多语言代码 👏 感谢大家的订阅➕ 和 喜欢�…...

netdata保姆级面板介绍
netdata保姆级面板介绍 基本介绍部署流程下载安装指令选择设置KSM为什么要启用 KSM?如何启用 KSM?验证 KSM 是否启用注意事项 检查端口启动状态 netdata和grafana的区别NetdataGrafananetdata各指标介绍总览system overview栏仪表盘1. CPU2. Load3. Disk…...

苹果最新论文:LLM只是复杂的模式匹配 而不是真正的逻辑推理
大语言模型真的可以推理吗?LLM 都是“参数匹配大师”?苹果研究员质疑 LLM 推理能力,称其“不堪一击”!苹果的研究员 Mehrdad Farajtabar 等人最近发表了一篇论文,对大型语言模型 (LLM) 的推理能…...

Python知识点:基于Python工具,如何使用Scikit-Image进行图像处理与分析
开篇,先说一个好消息,截止到2025年1月1日前,翻到文末找到我,赠送定制版的开题报告和任务书,先到先得!过期不候! 基于Python的Scikit-Image图像处理与分析指南 在Python的科学计算生态系统中&am…...

MongoDB初学者入门教学:与MySQL的对比理解
🏝️ 博主介绍 大家好,我是一个搬砖的农民工,很高兴认识大家 😊 ~ 👨🎓 个人介绍:本人是一名后端Java开发工程师,坐标北京 ~ 🎉 感谢关注 📖 一起学习 &…...

Oracle AI Vector Search
Oracle AI Vector Search 是 Oracle Database 23ai 中引入的一项新技术,它允许用户在数据库中直接存储和高效查询向量数据。这项技术旨在简化应用程序的开发,并且支持不同维度和格式的向量。以下是 Oracle AI Vector Search 的一些关键特性和优势&#x…...

基于SpringBoot的健身会员管理系统实战分享
在这个充满活力的时代,我们自豪地呈现一款专为健身爱好者和专业人士设计的会员管理系统——一个集创新、效率与便捷于一体的解决方案。我们的系统基于强大的RuoYi-Vue框架构建,采用最新的Spring Boot和Vue3技术,确保了系统的高性能和用户友好…...

Elasticsearch高级搜索技术-结构化数据搜索
目录 结构化数据的存储 示例映射 使用range查询 查询示例 运算符 更多示例 日期查询 示例 结构化数据搜索是Elasticsearch另一个强大的功能,允许用户对具有明确类型的数据(如数字、日期和布尔值)进行精确的过滤和查询。这种类型的搜索通常涉及…...

ffmpeg面向对象——类所属的方法探索
ffmpeg是面向对象的思想写的代码,自然符合oopc的实现套路。这个也是oopc的通用法则。 1.类所属方法oopc的实现形式 ffmpeg抽象出某一类,然后某一类的方法如何调用?你说这还不简单: 对象.对象方法() 或者 对象指针-&g…...

TensorRT-LLM七日谈 Day3
今天主要是结合理论进一步熟悉TensorRT-LLM的内容 从下面的分享可以看出,TensorRT-LLM是在TensorRT的基础上进行了进一步封装,提供拼batch,量化等推理加速实现方式。 下面的图片更好的展示了TensorRT-LLM的流程,包含权重转换&…...

如何使用Pandas库处理大型数据集?
如何使用Pandas库处理大型数据集? 处理大型数据集是数据分析中的一个挑战,尤其是在资源有限的情况下。Pandas是Python中非常流行的数据处理库,但它在处理非常大的数据集时可能会遇到内存限制的问题。因此,我们需要一些策略来提高Pandas处理大型数据集的效率。以下是使用Pa…...

XHR 创建对象
XHR 创建对象 XMLHttpRequest(XHR)是现代Web开发中不可或缺的技术之一。它允许Web开发者通过JavaScript发送网络请求,以在不重新加载整个页面的情况下更新网页的某部分。XHR为开发者提供了一种在客户端和服务器之间传输数据的有效方式,是AJAX(Asynchronous JavaScript an…...

# 在执行 rpm 卸载软件使用 nodeps 参数时,报错 error: package nodeps is not installed 分析
在执行 rpm 卸载软件使用 nodeps 参数时,报错 error: package nodeps is not installed 分析 一、问题描述: 在执行 rpm 卸载软件使用 nodeps 参数时,报错 error: package nodeps is not installed 如下图: 二、报错分析&…...

C++的类和动态内存分配(深拷贝与浅拷贝)并实现自己的string类
首先,我们先写一个并不完美的类: #include<iostream> #include<cstring> using namespace std;class Mystring{private:char *p;int len;static int num;friend ostream& operator<<(ostream& os, const Mystring& c);pu…...

通过观测云 DataKit Extension 接入 AWS Lambda 最佳实践
前言 AWS Lambda 是一项计算服务,使用时无需预配置或管理服务器即可运行代码。AWS Lambda 只在需要时执行代码并自动缩放。借助 AWS Lambda,几乎可以为任何类型的应用程序或后端服务运行代码,而且无需执行任何管理。 Lambda Layer 是一个包…...

MySQL-三范式 视图
文章目录 三范式三范式简介第一范式第二范式第三范式 表设计一对一一对多多对多最终的设计 视图 三范式 三范式简介 所谓三范式, 其实是表设计的三大原则, 目的都是为了节省空间, 但是三范式是必须要遵守的吗? 答案是否定的(但是第一范式必须遵守) 因为有时候严格遵守三范式…...

多线程(三):线程等待获取线程引用线程休眠线程状态
目录 1、等待一个线程:join 1.1 join() 1.2 join(long millis)——"超时时间" 1.3 join(long millis,int nanos) 2、获取当前线程的引用:currentThread 3、休眠当前进程:sleep 3.1 实际休眠时间 3.2 sleep的特殊…...

Hi3244 应用指导
Hi3244 是一款DIP8封装高性能、多模式工作的原边控制功率开关。Hi3244内高精度的恒流、恒压控制机制结合完备的保护功能,使其适用于小功率离线式电源应用中。在恒压输出模式中,Hi3244 采用多模式工作方式,即调幅控制(AM࿰…...

【LeetCode热题100】哈希
1.两数之和 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。 你可以按任意顺序返回答…...

Java的四种循环语句
背景: Java 中主要有四种循环语句:for 循环、while 循环、do-while 循环 和 foreach 循环(也称为增强型 for 循环)。下面我将分别介绍这四种循环语句,并给出相应的实例。 for循环: 1. for 循环for 循环是…...

Qt杂记目录
Qt 杂记目录 QMenu 1.menu转string Qt 窗口阴影边框...

项目开发--基于docker实现模型容器化服务
背景 1、docker-compose build 和 docker-compose up -d分别是什么作用? 2、如何进入新构建的容器当中 3、模型保存的方法区别 4、如何让docker容器启动的时候能使用cuda进行模型推理加速 5、如何实现容器的迭代 解决方案 问题1 docker-compose build 和 docker…...

C语言 | Leetcode C语言题解之第477题汉明距离总和
题目: 题解: int totalHammingDistance(int* nums, int numsSize) {int ans 0;for (int i 0; i < 30; i) {int c 0;for (int j 0; j < numsSize; j) {c (nums[j] >> i) & 1;}ans c * (numsSize - c);}return ans; }...

Bug剖析
Bug剖析 • 所有的Bug报告有以下的基本要求: • 标题。要简略。 • 指派。谁来处理这个问题。 • 重现步骤。问题再次出现的相关步骤。 • 优先级别。问题的紧迫性与重要性。 • 严重程度。问题所产生的后果。 • 解决方案。怎么解决问题。 其他很多方面对修复问题…...

HI3516DV500 相机部分架构初探
Hi3516DV500 是一颗面向视觉行业推出的高清智能 Soc。该芯片最高支持 2 路 sensor 输入,支持最高 5M30fps 的 ISP 图像处理能力,支持 2F WDR、多级降噪、六轴防 抖、多光谱融合等多种传统图像增强和处理算法,支持通过 AI 算法对输入图像进行实…...

训练yolo系列出现问题mAP, R, P等为零
1. 问题 40系列显卡训练yolo系列出现问题,loss正常,但mAP,R,P等为零。 环境:ultralytics版本为8.3.9,cuda11.8, torch2.4。 40系列显卡网上说可以使用cuda低于11.7的,自己测试了下…...