拯救动画卡顿之FLIP

news2024/12/23 2:52:14

前置知识

什么是FPS

FPS是浏览器的每秒的渲染帧数,也就是浏览器切换画面的次数,大多数设备的刷新率都是60FPS,一般来说FPS越低页面就会越卡顿。

什么是像素管道?

像素管道是浏览器单个帧的渲染流水线,如果其中有某些环节执行过程过长就会导致卡顿

  • JavaScript。通常来说,阻塞的发起都是来自于 JS ,这不是说不用 JS,而是要正确的使用 JS 。首先,JS 线程的运行本身就是阻塞 UI 线程的(暂不考虑 Web Worker)。从纯粹的数学角度而言,每帧的预算约为 16.7 毫秒(1000 毫秒 / 60 帧 = 16.66 毫秒/帧)。但因为浏览器需要花费时间将新帧绘制到屏幕上,只有 ~10 毫秒来执行 JS 代码,过长时间的同步执行 JS 代码肯定会导致超过 10ms 这个阈值,其次,频繁执行一些代码也会过长的占用每帧渲染的时间。此外,用 JS 去获取一些样式还会导致强制同步布局
  • 样式计算(Style)。此过程是根据匹配选择器(例如 .headline.nav > .nav__item)计算出哪些元素应用哪些 CSS 规则的过程,这个过程不仅包括计算层叠样式表中的权重来确定样式,也包括内联的样式,来计算每个元素的最终样式。
  • 布局(Layout)。在知道对一个元素应用哪些规则之后,浏览器即可开始计算该元素要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,一般来说如果修改了某个元素的大小或者位置,则需要检查其他所有元素并重排(re-flow)整个页面。
  • 绘制(Paint)。绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的,绘制包括两个步骤: 1) 创建绘图调用的列表, 2) 填充像素,后者也被称作栅格化。
  • 合成(Composite)。由于页面的各部分可能被绘制到多个层上,因此它们需要按正确顺序绘制到屏幕上,才能正确地渲染页面。尤其对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。

上述的五个阶段并不是一定都会执行到的,这五个阶段中涉及到了老生常谈的两个概念:重排跟重绘,虽然初次渲染布局跟绘制必不可少,但是后期我们可以控制避免通过这两个管道: 以下是当我们修改不同的样式属性时,会触发的几种帧流程:

从上图中能看到JS阶段以及Style 和 Composite阶段 是不可避免的,因为需要 JS 来引发样式的改变,Style 来计算更改后最终的样式,Composite 来合成各个层最终进行显示,能跳过的步骤只有布局跟绘制,我们知道,执行的阶段越少,耗时就越少,每秒的渲染帧数就会越高,那么能不能直接跳过这两个步骤直接到合成呢? 答案是肯定的,如下的属性只会触发合成阶段: transform、opacity、pointer-events、perspective (透视效果)、curosr、orphans设置当元素内部发生分页时必须在页面底部保留的最少行数(用于打印或打印预览)、widows(设置当元素内部发生分页时必须在页面顶部保留的最少行数(用于打印或打印预览))。

FLIP

综上所述,当我们写动画的时候如果用height margin padding left等会触发重排的属性,相较于只用transform或者opacity会带来更多的性能开销,一旦这个计算时长超过1个动画帧(一般是60帧每秒,也就是说超过16.7ms), 那么这帧动画将不会绘制,产生页面卡顿。 FLIP技术,就是一种让动画只利用到transform或者opacity的技巧,FLIP是 First, Last, Invert, Play的简称。

概念

First

对应动画的Start阶段,用 element.getBoundingClientRect()记录初始位置。

Last

对应动画的End阶段,先执行触发layout变动的代码,同样的用element.getBoundingClientRect()记录元素的终止位置。

Invert

现在元素处于End位置,利用 transform 做一个逆运算,让添加了 transform 的元素回归到初始位置。

Play

真正需要执行动画时,将 transform 置为 None

上述阶段可能有人会困惑,getBoundingClientRect不是也会触发重排吗?但是需要注意的是我们的重心是在动画阶段,要保障的是动画阶段的流畅,更何况用户在网页上进行交互时,比如click,touch,从交互结束到感知到程序的相应大约需要100ms的生理反应时间。我们在用户交互后要做100ms内准备好动画就好了,这些动画准备计算就是getBoundingClientRect(或getComputedStyle)等的计算。

实践

现在实现一个简单的从左到右的循环滚动动画

const playAnimate = (el) => {if (!el) return;// 记录初始位置,对应 FLIP的FIRSTconst pos = tagWrapperRef.current?.getBoundingClientRect();const { left: initLeft } = pos; // 设置元素样式为动画结束时的目标位置 tagWrapperRef.current.classList.add('scroll-to-end'); // 记录终止位置 对应FLIP的LASTconst endPos = tagWrapperRef.current?.getBoundingClientRect();const { left: endLeft } = endPos;// 计算初始位置跟终止位置的偏差const deltaLeft = initLeft - endLeft;tagWrapperRef.current.animate([// 动画开始时,利用 `transform` 做一个逆运算,让添加了 `transform` 的元素回归到初始位置。{transform: `translate(${deltaLeft}px,0)`,},{transform: 'none',},],{duration: totalTime * 1000,easing: 'linear',iterations: Infinity,});
 } 
.scroll-to-end{left:100%} 

采用FLIP的形式成功的避开了动画过程中更改left。 我们注意到:我们可以在动画开始前预先用API计算元素在动画终止时候的位置,只要知道了终态跟初始状态,就能迅速计算出要达到终态该怎么移动,由此避开一些复杂的计算,由此我们也可以得出: 除了用来做性能优化之外,FLIP也能用于简化某些场景下动画的实现过程,如以下几个场景:

  • 视图之间的过渡
  • 图片展开和收缩效果
  • 项目删除和添加时填充空白区域的效果
  • 网格项的重新排序 举个例子,如第三个场景,如若添加或者删除的卡片的大小是未知的,如果用常规的方式移动其他卡片将变得困难,而如果使用FLIP,问题将迎刃而解
// Last
if (updateType === 0) { 
// 增加卡片 
newListData = this.state.listData.slice(0, activeIndex).concat({ index: cardIndex++ 
}, this.state.listData.slice(activeIndex)) 
} else { 
// 删除卡片 
newListData = this.state.listData.filter((value, index) => index !== activeIndex)
}

// Invert

// 0 增加 1 删除
const updateIndex = updateType === 0 ? 1 : 0
activeList.forEach((item, index) => {rect = item.getBoundingClientRect()invertArr[index + updateIndex][0] = invertArr[index + updateIndex][0] - rect.left invertArr[index + updateIndex][1] = invertArr[index + updateIndex][1] - rect.top
 ... 

使用FLIP形式需要注意什么

  • FLIP中的前三个阶段也就是前期的准备工作需要在绘制这个步骤之前,也就是时间需要尽可能的控制在之前提到的100ms以内,否则的话 渲染的过程中可能出现闪烁,但是我们怎么才能抓住绘制前这个时机呢?本人用React比较多,以React为例:答案是useLayoutEffect,它接受一个回调函数,这个函数会在dom更新后、重绘之前同步的执行

比如,我们实现一个点击方块互换位置的动画(详见codepen.io/jlkiri/pen/…)

const Flipper = () => {const [ids, setIds] = React.useState(["square-1", "square-2"]);const rects = React.useRef(new Map()).current;const swap = ([a, b]) => [b, a];React.useEffect(() => {const squares = document.querySelectorAll(".square");// Cache position and size once on initial renderfor (const square of squares) {rects.set(square.id, square.getBoundingClientRect());}}, []);React.useLayoutEffect(() => {const squares = document.querySelectorAll(".square");for (const square of squares) {// Get previous size and position from cacheconst cachedRect = rects.get(square.id);if (cachedRect) {const nextRect = square.getBoundingClientRect();// Invertconst translateX = cachedRect.x - nextRect.x;// Cache the next size and positionrects.set(square.id, nextRect);// Playsquare.animate([{ transform: `translateX(${translateX}px)` },{ transform: `translateX(0px)` }],1000);}}}, ids);return (<div className="container">{ids.map((id, i) => {return (<div id={id} onClick={() => setIds(swap(ids))} className={`square`}>{id}</div>);})}</div>);
};

ReactDOM.render(<Flipper />, document.querySelector("#root")); 

以上可以用如下的流程图概括:

  • 避免多次触发重排 可能会有的人“读”“写”一起进行。

错误的做法: 循环遍历元素并使用 getBoundingClientRect 读取它们的位置,然后立即使用 animate 为它们设置动画。

正确的做法: 批量的读取与写入

最后

Invert这一步骤可能有些许麻烦,今年5月份作者提供了插件 Flip Plugin

最后

为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/95863.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue数据双向绑定

5.Vue数据双向绑定 5.1.什么是双向数据绑定 Vue.js 是一个 MVVM 框架&#xff0c;即数据双向绑定&#xff0c;即当数据发生变化的时候&#xff0c;视图也就发生变化&#xff0c;当视图发生变化的时候&#xff0c;数据也会跟着同步变化。这也算是 Vue.js 的精髓之处了。 值得…

[ MessAuto ]: 短信验证码自动填充,理论支持所有浏览器或 APP, Only For Mac

MessAuto 开源地址&#xff1a;https://github.com/LeeeSe/MessAuto MessAuto 是一款 macOS 平台 自动提取 短信验证码并 粘贴回车 的软件&#xff0c;百分百由Rust开发&#xff0c;适用于任何APP。 特点&#xff1a; 轻量&#xff1a;程序占用存储 1.8 M&#xff0c;占用内…

NLP学习笔记(三) GRU基本介绍

大家好&#xff0c;我是半虹&#xff0c;这篇文章来讲门控循环单元 (Gated Recurrent Unit, GRU) 文章行文思路如下&#xff1a; 首先通过长短期记忆网络引出为什么需要门控循环单元然后介绍门控循环单元的核心思想与运作方式最后通过简洁的代码深入理解门控循环单元的运作方…

奇舞周刊 476 期:代码在内存中的 “形状”

记得点击文章末尾的“ 阅读原文 ”查看哟~下面先一起看下本期周刊 摘要 吧~奇舞推荐■ ■ ■代码在内存中的 “形状”众所周知&#xff0c;js 的基本数据类型有 number、string、boolean、null、undefined 等。那么问题来了 typeof null 和 typeof undefined 分别是什么呢&…

[附源码]Node.js计算机毕业设计果蔬预约种植管理系统Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

[内网渗透]—NTLM网络认证及NTLM-Relay攻击

NTML网络认证 Windows认证分为本地认证和网络认证,当我们开机登录用户账户时,就需要将lsass.exe进程转换的明文密码hash与 sam文件进行比对,这种方式即为——本地认证 而当我们访问同一局域网的一台主机上的SMB共享时,需要提供凭证通过验证才能访问,这个过程就会设计win…

【C++】list 的模拟实现

​&#x1f320; 作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《吃透西嘎嘎》 &#x1f387; 座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录&#x1f449;前言&…

Halcon条码和二维码质量评级

现在各行各业的人们都使用条码/二维码从生产阶段到销售点全程追踪他们 的产品。那么怎么验证生产出来的具有可读性&#xff0c;码的质量等级如何呢&#xff1f; 其实ISO行业标准已经给出了如何评估码的质量等级的标准&#xff0c;以下三种主要验证标准用于确定一维条码、二维码…

毕业设计 - 基于Java EE平台项目管理系统的设计与实现【源码+论文】

文章目录前言一、项目设计1. 模块设计2. 实现效果二、部分源码项目工程前言 今天学长向大家分享一个 java web项目: 基于Java EE平台项目管理系统的设计与实现 一、项目设计 1. 模块设计 从管理员角度看: 用户登入系统后&#xff0c;可以修改管理员的密码。同时具有以下功能…

最全的SpringMVC教程,终于让我找到了

1. 为啥要学 SpringMVC&#xff1f; 1.1 SpringMVC 简介 在学习 SpringMVC 之前我们先看看在使用 Servlet 的时候我们是如何处理用户请求的&#xff1a; 配置web.xml <?xml version"1.0" encoding"UTF-8"?> <web-app xmlns"http://xmln…

[附源码]Python计算机毕业设计国际美容会所管理系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

Jetpack Compose中的动画

Jetpack Compose中没有沿用Android原有的View动画和属性动画&#xff0c;而是新创建了一套全新的动画系统API&#xff0c;这是理所当然的&#xff0c;因为旧的动画系统主要是基于View体系的&#xff0c;而Compose中需要针对的是Composable可组合函数进行处理&#xff0c;那么势…

他文献查到凌晨两点,我用Python十分钟搞定!

大家好&#xff0c;我是爱学习的王饱饱。 对于应届毕业生来说&#xff0c;今年一定是难熬的一年。本来找工作、写论文就已经是两座大山了&#xff0c;还要面临论文无指导的额外压力。 这让我想到了去年毕业的表弟&#xff0c;当时他为了完成论文&#xff0c;摔烂了三个鼠标。…

Jsp服装商城包安装调试

(https://img-blog.csdnimg.cn/78351365dac24f6185cb69ee3a804ba1.png)jsp mysql新季服装商城 功能&#xff1a;前台会员中心后台 前台&#xff1a; 1.服装展示 图文列表 新闻列表 详情 2.注册登录 管理登陆 3.加入购物车 会员中心&#xff1a; 1.个人账户 查看 修改个人…

一个新的React项目我们该如何配置

最近组长让我负责一个新的项目&#xff0c;项目的技术栈是React typescript redux and design&#xff0c;一个工龄1年的小白菜只能先去github找开源项目看看他们做了哪些配置&#xff0c;然后去百度这些配置改如何安装。下面就是我记录一个新的React项目配置的过程。 安装…

知识图谱有哪些应用领域?

知识图谱通常应用于自然语言处理和人工智能领域&#xff0c;常用于提高机器学习模型的准确性和效率。它还可以用于数据挖掘、信息检索、问答系统和语义搜索等领域。近年来知识图谱在电子商务、金融、公安、医疗等行业逐步开始落地&#xff0c;在这些行业的渗透、深入中&#xf…

部门还有谁在? 已经没几个人了~

正文大家好&#xff0c;我是bug菌&#xff5e;终于熬过了阳性的第三天&#xff0c;症状相对没之前那么痛苦了&#xff0c;打算要家里面的兄弟帮忙处理点事情&#xff0c;一个电话打过去&#xff0c;没想到整个部门都没几个人了&#xff0c;病毒的毒性是减弱了&#xff0c;这传染…

Linux进程概念(一)

Linux进程概念冯诺依曼体系结构操作系统操作系统是什么操作系统与硬件的关系操作系统如何管理硬件数据操作系统与软件的关系操作系统的安全操作系统的服务系统调用和库函数概念进程的基本概念什么是进程如何查看进程进程常见的调用冯诺依曼体系结构 常见的计算机&#xff08;台…

牛客题霸sql入门篇之条件查询(三)

牛客题霸sql入门篇之条件查询(三) 3 高级操作符 3.1 高级操作符练习(1) 3.1.1 题目内容 3.1.2 示例代码 SELECT device_id,gender,age,university,gpa FROM user_profile WHERE gendermale AND gpa>3.53.1.3 运行结果 3.1.4 考察知识点 AND关键字表示会筛选出符合左右两…

java DDD领域分层架构设计思想

1为什么要分层 高内聚&#xff1a;分层的设计可以简化系统设计&#xff0c;让不同的层专注做某一模块的事低耦合&#xff1a;层与层之间通过接口或API来交互&#xff0c;依赖方不用知道被依赖方的细节复用&#xff1a;分层之后可以做到很高的复用扩展性&#xff1a;分层架构可…