红盒子的动画历险记:JS vs CSS 的浏览器舞台剧

155 阅读6分钟

我是一块正直的红色矩形,此刻正躺在空白的网页上。我的程序员主人今天似乎想让我表演才艺——要么优雅地变胖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实现了动画效果,让一个盒子的宽度持续增大

B1E595BDE5B620135411_converted.gif 这段代码让我体验了“像素级增肥”:

  1. 递归投喂move() 自调用形成循环投喂链,相当于递归函数,让函数自己调用自己
  2. 帧率同步requestAnimationFrame 让我的增肥节奏与屏幕刷新率完美同步
  3. 精确控制:每次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!你好世界!,但当我们执行,结果却大相径庭

image.png

我们来看看这张图,详细地解释了同步任务和异步任务的执行流程

image.png

大家可以看看我写得这篇文章,你会加深对同步任务,异步任务的理解:当代码开始"画饼":JavaScript异步修仙指南

第三章:浏览器后台的疯狂流水线

我的每一次动作,都让浏览器工厂里爆发996加班: 浏览器解析 HTML 和 CSS 的顺序是这样的:

  1. HTML 解析 → DOM 构建
  2. CSS 解析 → CSSOM 构建
  3. DOM + CSSOM → 构建 Render Tree
  4. Layout(布局) → Paint(绘制) → Composite(合成)

在这个过程中,样式计算是在渲染之前完成的

ae7aa55c42430c2139c5db346e112a57.png

image.png

我们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特区)

  • transformopacity的VIP通道:享受独立图层待遇
  • GPU加速的秘密:修改时只需重绘该图层(像更换舞台背景板)
  • JS动画的悲剧:修改width触发全局装修(重排+重绘150次!)

车间主任备忘录:JS动画=让工人拆了建建了拆;CSS动画=直接换预制板!

所以我们应该选择何种方式来做动画呢?答案显然易见

终章:红盒子的动画哲学

当浏览器再次刷新,我完成了最后一次表演。回顾今日历程,凝结为三点觉悟:

  1. 浏览器不是魔术师而是流水线工人:理解渲染管线(DOM→CSSOM→渲染树→布局→分层→绘制)才能避免“无效加班”

  2. 性能的本质是减少沟通成本

    • JS动画:频繁让CPU和GPU开会协调(重排/重绘)
    • CSS动画:预先签订GPU加速协议(transform/opacity)
  3. setTimeout(0)不是魔法而是排队策略:它让渲染线程优先拿到麦克风(事件循环机制)

谢幕彩蛋:当主人同时使用JS和CSS修改我的属性时——
“求求你们别同时让我做仰卧起坐和跳广场舞了!”(浏览器哭晕在渲染层)