CSS3 动画卡顿性能优化
- 明确以下几个概念:单线程,主线程和合成线程,浏览器是单线程运行的(注意,是执行,并不是说浏览器只有1个线程,而是运行时,runing),但实际上浏览器的2个重要的执行线程,这 2 个线程协同工作来渲染一个网页:主线程和合成线程。
- 主线程负责:运行 JavaScript;计算 HTML 元素的 CSS 样式;页面的布局;将元素绘制到一个或多个位图中;将这些位图交给合成线程。
合成线程负责:通过 GPU 将位图绘制到屏幕上;通知主线程更新页面中可见或即将变成可见的部分的位图;计算出页面中哪部分是可见的;计算出当你在滚动页面时哪部分是即将变成可见的;当你滚动页面时将相应位置的元素移动到可视区域。
那么为什么会造成动画卡顿呢?
原因就是主线程和合成线程的调度不合理。
- 在使用height,width,margin,padding作为transition的值时,会造成浏览器主线程的工作量较重,例如从margin-left:-20px渲染到margin-left:0,主线程需要计算样式margin-left:-19px,margin-left:-18px,一直到margin-left:0,而且每一次主线程计算样式后,合成进程都需要绘制到GPU然后再渲染到屏幕上,前后总共进行20次主线程渲染,20次合成线程渲染,20+20次,总计40次计算。
- 主线程的渲染流程,可以参考浏览器渲染网页的流程:
使用 HTML 创建文档对象模型(DOM)
使用 CSS 创建 CSS 对象模型(CSSOM)
基于 DOM 和 CSSOM 执行脚本(Scripts)
合并 DOM 和 CSSOM 形成渲染树(Render Tree)
使用渲染树布局(Layout)所有元素
渲染(Paint)所有元素
也就是说,主线程每次都需要执行Scripts,Render Tree ,Layout和Paint这四个阶段的计算。 - 而如果使用transform的话,例如tranform:translate(-20px,0)到transform:translate(0,0),主线程只需要进行一次tranform:translate(-20px,0)到transform:translate(0,0),然后合成线程去一次将-20px转换到0px,这样的话,总计1+20计算。
- 假设我们要一个元素的 height 从 100 px 变成 200 px,就像这样:
div {
height: 100px;
transition: height 1s linear;
}
div:hover {
height: 200px;
}
可能会比较耗时,容易出现卡顿现象
- 而使用 transform:scale 实现,减少主线程的计算次数,提高动画性能
div {
transform: scale(0.5);
transition: transform 1s linear;
}
div:hover {
transform: scale(1.0);
}
其次还可以通过开启硬件加速的方式优化动画,开启3d加速
webkit-transform: translate3d(0,0,0);
-moz-transform: translate3d(0,0,0);
-ms-transform: translate3d(0,0,0);
-o-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
总结解决 CSS3 动画卡顿方案
- 尽量使用 transform 当成动画熟悉,避免使用 height,width,margin,padding 等;
- 要求较高时,可以开启浏览器开启 GPU 硬件加速
源链接:https://www.jb51.net/article/147736.htm
抓抓乐抽奖代码实现(移动端)
- 前端动画展示的一种抽奖形式
- 有上下两排奖品左右移动
- 点击抽奖按钮上方夹子下落,奖品停止移动,随机抓取移动的奖品上升
- 夹子到达初始位置,奖品返回初始位置并继续移动
- template
<template>
<div class="shopping-grab" :style="{'background-image': `url(${styleConfig.grab_bg})`}" >
<!-- 动画部分 -->
<div class="grab-box">
<!-- 钩子部分 -->
<div class="hook-box">
<!-- 绳子 -->
<div class="rope" :class="{ropetop:isDown && layers == 1,ropeBtm:isDown && layers == 2}" :style="{'background-image': `url(${styleConfig.grab_rope})`}"></div>
<!-- 钩子主体 -->
<div class="hook" :style="{'background-image': `url(${styleConfig.grab_body})`}" ref="hookRef">
<!-- 钩手左 -->
<div class="hookLeft" :class="{downLeft:isDown}" :style="{'background-image': `url(${styleConfig.grab_hand_left})`}"></div>
<!-- 钩手右 -->
<div class="hookRight" :class="{downRight:isDown}" :style="{'background-image': `url(${styleConfig.grab_head_right})`}"></div>
</div>
</div>
<!-- 盲盒部分 -->
<div class="blind-box">
<!-- 上排盲盒 -->
<div class="blind-top" :class="{'blind-top-stop': isStop}" ref="blindTop">
<div class="blind-item" v-for="(item,index) in blindBoxTop" :key="index" :style="{'left': item.left + 'px'}">
<img :src="item.url" class="blind-img" alt="">
</div>
</div>
<!-- 下排盲盒 -->
<div class="blind-btm" id="blind-btm" :class="{'blind-top-stop': isStop}" ref="blindBtm">
<div class="blind-item" v-for="(item,index) in blindBoxBtm" :key="index" :style="{'left': item.left + 'px'}">
<img :src="item.url" class="blind-img" alt="">
</div>
</div>
</div>
</div>
<!-- 抓抓乐按钮 -->
<div class="grab-btn" :style="{'background-image': `url(${styleConfig.grab_btn})`}" @click="grabFn" v-log="'商店-抓抓乐抽奖按钮'">
<!-- 抓抓乐按钮文案 -->
<div class="grab-btn-text" :style="{'color': styleConfig.btn_text_color}">{{ styleConfig.btn_text }}</div>
</div>
</div>
</template>
- script
<script>
export default {
// 导出了btnClick抽奖按钮点击 and prizePopupFn 中奖弹窗
props: {
// 盲盒数组
blindArray: {
type: Array,
default: ()=>{
return []
}
},
// 是否调用抽奖动画
isAnimation: {
type: Boolean,
default: false
},
// 抓抓乐样式
styleConfig: {
type: Object,
default: ()=>{
return {
grab_bg: '', // 背景图
grab_rope: '', // 绳子图
grab_body: '', // 钩子主体
grab_hand_left: '', // 钩子左
grab_head_right: '', // 钩子右
grab_btn: '', // 按钮图片
btn_text_color: '', // 按钮文案颜色
btn_text: '', // 按钮文案
}
}
}
},
watch: {
isAnimation(newVal) {
if (newVal) {
this.grabLottrey()
}
},
},
created () {
// 适配
const iphone6ItemWidth = 85;
const curScreenWidth = window.screen.width;
const curItemWidth = curScreenWidth / 375 * iphone6ItemWidth;
let blind1 = {
url: this.blindArray[0],
left: 0,
}
let blind2 = {
url: this.blindArray[1],
left: curItemWidth,
}
let blind3 = {
url: this.blindArray[2],
left: curItemWidth * 2,
}
let blind4 = {
url: this.blindArray[3],
left: curItemWidth * 3,
}
let blind5 = {
url: this.blindArray[4],
left: curItemWidth * 4,
}
let blind6 = {
url: this.blindArray[0],
left: curItemWidth * 5,
}
let blind7 = {
url: this.blindArray[1],
left: curItemWidth * 6,
}
let blind8 = {
url: this.blindArray[2],
left: curItemWidth * 7,
}
let blind9 = {
url: this.blindArray[3],
left: curItemWidth * 8,
}
let blind10 = {
url: this.blindArray[4],
left: curItemWidth * 9,
}
this.blindBoxTop.push(blind1,blind2,blind3,blind4,blind5,blind6,blind7,blind8,blind9,blind10) // 上层盲盒
this.blindBoxBtm = JSON.parse(JSON.stringify(this.blindBoxTop)) // 下排盲盒
},
data () {
return {
blindBoxTop: [], // 上排盲盒
blindBoxBtm: [], // 下排盲盒
isDown: 0, // 绳子拉长
layers: 1, // 落下层数
isStop: 0, // 是否暂停盲盒移动
timeid: null,
isAim: null
}
},
methods: {
// 点击抽盲盒
grabFn() {
this.$emit('btnClick') // 导出按钮点击事件
},
// 钩子动画
hookFn() {
this.isDown = 1 // 钩子是否下落
let cdnTop = this.$refs.blindTop.childNodes
let cdnBtm = this.$refs.blindBtm.childNodes
if (this.layers == 1) {
this.timeid = setTimeout(()=>{
for (let i = 0; i < cdnTop.length; i++) {
console.log(cdnBtm[i].getBoundingClientRect().left)
if (cdnTop[i].getBoundingClientRect().left >= 100 && cdnTop[i].getBoundingClientRect().left < 220) {
let sltbox = this.$refs.hookRef.appendChild(cdnTop[i])
setTimeout(()=>{
this.initial() // 初始化盲盒移动
this.$refs.blindTop.appendChild(sltbox) // 盲盒复位
},2500)
clearTimeout(this.timeid)
return
}
}
},2000)
} else if (this.layers == 2) {
this.timeid = setTimeout(()=>{
for (let i = 0; i < cdnBtm.length; i++) {
console.log(cdnBtm[i].getBoundingClientRect().left)
if (cdnBtm[i].getBoundingClientRect().left >= 100 && cdnBtm[i].getBoundingClientRect().left < 220) {
let sltbox = this.$refs.hookRef.appendChild(cdnBtm[i])
setTimeout(()=>{
this.initial() // 初始化动画
this.$refs.blindBtm.appendChild(sltbox) // 盲盒复位
},3500)
clearTimeout(this.timeid)
return
}
}
},3000)
}
},
// 盲盒动画
blindFn() {
this.layers = Math.ceil(Math.random()*2);
this.hookFn() // 调用钩子动画
if (this.layers == 1) {
setTimeout(()=>{ // 如果是第一层点击后延时停止盲盒移动
this.isStop = 1
},2000)
} else if (this.layers == 2) {
setTimeout(()=>{ // 如果是第二层点击后延时停止盲盒移动
this.isStop = 1
},3000)
}
},
// 盲盒抽奖
grabLottrey() {
this.blindFn() // 调用盲盒动画
// 判断下落的层数控制弹中奖弹窗的延时时间
if (this.layers == 1) {
setTimeout(()=>{
this.$emit('prizePopupFn')
},3000)
} else if (this.layers == 2) {
setTimeout(()=>{
this.$emit('prizePopupFn')
},5000)
}
},
// 初始化动画
initial() {
this.isStop = 0 // 盲盒移动
this.isDown = 0 // 初始化下落类名
},
}
}
</script>
- style
<style lang="scss" scoped>
@import '../css/mixin.scss';
// 抓抓乐
.shopping-grab {
height: 1068px;
width: 100%;
// background-color: pink;
margin-bottom: 20px;
background-size: 100% 100%;
background-repeat: no-repeat;
padding-top: 66px;
position: relative;
box-sizing: border-box;
.grab-box {
height: 750px;
width: 606px;
// background-color: pink;
overflow: hidden;
margin: 0 auto 10px;
padding-top: 280px;
position: relative;
// 钩子
.hook-box {
z-index: 99;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
// 被抓起来的盒子样式
.blind-item {
margin: 0 auto;
width: 150px;
height: 236px;
overflow: hidden;
.blind-img {
height: 136px;
width: 150px;
margin: 90px auto 0;
}
}
// 绳子 infinite
.ropetop {
animation: animationTop 4s linear;
}
.ropeBtm {
animation: animationBtm 6s linear;
}
.rope {
width: 16px;
height: 50px;
background-size: 100% 100%;
background-repeat: repeat;
margin: 0 auto 0;
transform: translate3d(0, 0, 0);
// animation: animationBtm 7s linear;
}
@keyframes animationTop {
0% {
height: 50px;
}
50% {
height: 210px;
}
100% {
height: 50px;
}
}
@keyframes animationBtm {
0% {
height: 50px;
}
50% {
height: 470px;
}
100% {
height: 50px;
}
}
// 钩子部分
.hook {
width: 170px;
height: 124px;
background-size: 100%;
background-repeat: no-repeat;
margin: 0 auto 0;
position: relative;
.hookLeft {
position: absolute;
left: 0;
top: 36%;
height: 100px;
width: 50px;
background-size: 100%;
background-repeat: no-repeat;
}
.downLeft {
transform: rotate(22deg);
transform-origin: 10px 10px;
transition: 1s;
}
.downRight {
transform: rotate(-22deg);
transform-origin: 40px 10px;
transition: 1s;
}
.hookRight {
position: absolute;
right: 0;
top: 36%;
height: 100px;
width: 50px;
background-size: 100%;
background-repeat: no-repeat;
}
}
}
// 盲盒
.blind-box {
height: 412px;
width: 100%;
position: relative;
.blind-top {
animation: rolling1 7s linear infinite;
position: absolute;
height: 140px;
// display: flex;
width: 1700px;
// transform: translate3d(0, 0, 0);
/* Other transform properties here */
@keyframes rolling1 {
from {
transform: translate3d(0,0,0);
}
to {
transform: translate3d(-50%,0,0);
}
}
.blind-item {
position: absolute;
top: 0;
// margin-right: 20px;
width: 150px;
height: 136px;
.blind-img {
height: 136px;
width: 150px;
margin: 0 auto;
}
}
.blind-bmbox {
width: 1700px;
height: 140px;
}
}
.blind-btm {
animation: rolling2 7s linear infinite;
position: absolute;
top: 284px;
height: 140px;
// display: flex;
width: 1700px;
@keyframes rolling2 {
from {
transform: translate3d(-50%,0,0);
}
to {
transform: translate3d(0,0,0);
}
}
.blind-item {
// margin-right: 20px;
position: absolute;
top: 0;
width: 150px;
height: 136px;
.blind-img {
height: 136px;
width: 150px;
margin: 0 auto;
}
}
.blind-bmbox {
width: 1700px;
height: 140px;
}
}
.blind-top-stop {
animation-play-state:paused;
}
}
}
.grab-btn {
position: absolute;
bottom: 76px;
left: 50%;
transform: translateX(-50%);
width: 420px;
height: 160px;
background-size: 100%;
// background-color: #fff;
background-repeat: no-repeat;
margin: 0 auto 10px;
.grab-btn-text {
font-size: 22px;
text-align: center;
line-height: 170px;
}
}
}
</style>