淘宝联盟 做网站,关于怎样把网站建设好的一些建议,浙江建设信息港首页介绍,机械网站建设营销前言
最近在做复图标库功能时#xff0c;感觉这个功能在使用上有些“生硬”。如随机删除一个图标#xff0c;后面的元素在视觉上是“瞬间移动”过来补位的。想着做个小优化#xff0c;简单加个动画效果吧。 看起来确实“花里胡哨”了#xff0c;实现也很简单#xff0c; …前言
最近在做复图标库功能时感觉这个功能在使用上有些“生硬”。如随机删除一个图标后面的元素在视觉上是“瞬间移动”过来补位的。想着做个小优化简单加个动画效果吧。 看起来确实“花里胡哨”了实现也很简单
ultransition-group appear tagul!--图片循环--/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代码执行。一般的纯前端阻塞都是来自JSJS线程的运行本身就会阻塞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 classbutton修改第一个元素位置/button
ul classlistli classlist-item active元素1/lili classlist-item元素2/lili classlist-item元素3/lili classlist-item元素4/lili classlist-item元素5/lili classlist-item元素6/li
/ulFirst获取初始位置 // 元素
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可以更简单实现一个动画
参考文档
让你的网页更丝滑
性能优化之关于像素管道及优化
前端动画必知必会