我是一块正直的红色矩形,此刻正躺在空白的网页上。我的程序员主人今天似乎想让我表演才艺——要么优雅地变胖300像素,要么瞬间移动到舞台右侧。可谁能想到,这背后的浏览器舞台竟藏着如此精彩的暗战!
第一章:JS动画的投喂饼干术
我的主人先掏出了JS魔法棒,用requestAnimationFrame对我进行“投喂”:
.box {
width: 100px;
height: 100px;
position: relative;
left: 0;
top: 0;
background-color: red;
transition: left 1s ease-out;
}
transition: left 1s ease-out 监测我们的left改变,一但改变就会缓慢过度显示
<script>
const box = document.querySelector('.box')
let width = 0
function move() {
width += 2 // 每次投喂2像素饼干
box.style.width = width + 'px'
if (width < 300) {
requestAnimationFrame(move) // 嚼完继续要饼干
}
}
move()
</script>
我们来看看效果,我们用js实现了动画效果,让一个盒子的宽度持续增大
这段代码让我体验了“像素级增肥”:
- 递归投喂:
move()自调用形成循环投喂链,相当于递归函数,让函数自己调用自己 - 帧率同步:
requestAnimationFrame让我的增肥节奏与屏幕刷新率完美同步 - 精确控制:每次2像素的精准投喂(但150次的投喂次数...嗝!)
开发者旁白:JS动画就像亲手喂饼干——你能控制每一口的量,但需要一直盯着喂!
我们可以用JS实现动画的效果,我们还可以用什么来实现动画呢?是不是css?我们的css属性:transform opacity等是不是也可以实现动画的效果,当然这是毋庸置疑的
第二章:CSS瞬移的时空魔法
当主人切换到CSS魔法阵时,事情变得魔幻起来:
.box {
transition: left 1s ease-out; /* 安装时空传送门 */
}
.box.active {
left: 300px; /* 设定传送坐标 */
}
但魔法启动时却遭遇了时间悖论:
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
box.classList.add('active') // 这个0ms延迟竟是关键!
}, 0)
})
这里藏着的浏览器秘密:
- DOMContentLoaded:浏览器刚读完剧本(HTML),但演员还没化妆(渲染)
- setTimeout(0) 的魔法:把启动指令塞进“任务队列”,让浏览器先完成渲染
- 事件循环潜规则:渲染任务 > setTimeout 微任务
红盒子吐槽:没有这0ms延迟,我就成了闪现的幽灵!观众根本看不到我的优雅入场!
那么为什么没有这个延迟函数,我们就看不到动画效果呢?这一切与浏览器html和css顺序有关
由于setTimeout是异步任务,浏览器要等所有的同步任务执行完后才会执行异步任务
setTimeout(() => {
console.log('hello world!`');
}, 0)
console.log('你好世界!');
执行结果是怎么样的呢?大家是不是觉得是先hello world!再你好世界!,但当我们执行,结果却大相径庭
我们来看看这张图,详细地解释了同步任务和异步任务的执行流程
大家可以看看我写得这篇文章,你会加深对同步任务,异步任务的理解:当代码开始"画饼":JavaScript异步修仙指南
第三章:浏览器后台的疯狂流水线
我的每一次动作,都让浏览器工厂里爆发996加班: 浏览器解析 HTML 和 CSS 的顺序是这样的:
- HTML 解析 → DOM 构建
- CSS 解析 → CSSOM 构建
- DOM + CSSOM → 构建 Render Tree
- Layout(布局) → Paint(绘制) → Composite(合成)
在这个过程中,样式计算是在渲染之前完成的。
我们DOM树加载完成就会改变width的宽度,此时还没有渲染,也就看不到页面
也就是说,即使浏览器还没有把元素画到屏幕上,它已经知道这个元素应该是什么样子了。
所以在你添加 .active 类的时候:
- 浏览器已经完成了对
.box初始样式的计算(left: 0); - 然后你立即修改为
left: 300px; - 但因为这些操作都在同一个“任务”中完成,浏览器不会触发 transition;
- 它不会播放动画,而是直接应用最终样式(
left: 300px);
所以,并不是浏览器“看不到”初始状态,而是它知道初始状态,也知道最终状态,但是因为它们变化太快,浏览器跳过了过渡动画。
✅ 举个生活中的类比:
想象一下你在看一个动画片:
- 第一帧:画面 A(初始)
- 第二帧:画面 B(目标)
如果这两帧在同一时间点被加载进来,你会看到什么?
👉 你只会看到画面 B,而不会看到画面 A → B 的动画。
同样的道理也适用于浏览器的渲染和 transition 动画。
简单来说我们改变width时,页面还没有完成渲染,浏览器肯定选择最终width值来渲染页面,也就看不到动画效果了
第四章:性能对决擂台赛
当JS动画和CSS动画在后台相遇:
| 维度 | JS动画 | CSS Transition |
|---|---|---|
| 控制精度 | ★★★★★ (像素级操控) | ★★☆☆☆ (预设路径) |
| 性能开销 | ★★☆☆☆ (重排重绘连环炸) | ★★★★★ (GPU专属通道) |
| 代码复杂度 | ★★☆☆☆ (递归/条件控制) | ★★★★★ (声明式爽翻天) |
| 物理模拟能力 | ★★★★★ (复杂物理引擎) | ★★☆☆☆ (基础缓动) |
血泪教训现场:
- JS动画修改
width:触发 布局(layout)→绘制(paint)→合成(composite) 全流程 - CSS过渡修改
left:因开启独立图层,仅触发 合成(composite) 阶段 - 性能差距:约等于手工织布 vs 全自动纺织机
红盒子体检报告:JS动画后我腰酸背痛(150次重排),CSS动画后我神清气爽(GPU按摩)
特别揭秘图层分层部(GPU特区) :
transform和opacity的VIP通道:享受独立图层待遇- GPU加速的秘密:修改时只需重绘该图层(像更换舞台背景板)
- JS动画的悲剧:修改
width触发全局装修(重排+重绘150次!)
车间主任备忘录:JS动画=让工人拆了建建了拆;CSS动画=直接换预制板!
所以我们应该选择何种方式来做动画呢?答案显然易见
终章:红盒子的动画哲学
当浏览器再次刷新,我完成了最后一次表演。回顾今日历程,凝结为三点觉悟:
-
浏览器不是魔术师而是流水线工人:理解渲染管线(DOM→CSSOM→渲染树→布局→分层→绘制)才能避免“无效加班”
-
性能的本质是减少沟通成本:
- JS动画:频繁让CPU和GPU开会协调(重排/重绘)
- CSS动画:预先签订GPU加速协议(transform/opacity)
-
setTimeout(0)不是魔法而是排队策略:它让渲染线程优先拿到麦克风(事件循环机制)
谢幕彩蛋:当主人同时使用JS和CSS修改我的属性时——
“求求你们别同时让我做仰卧起坐和跳广场舞了!”(浏览器哭晕在渲染层)