react-spring — 用真实弹簧的物理写网页动画
是什么
react-spring 是一个让你用”真实弹簧的物理参数”写动画的 React 库。日常类比:传统 CSS 动画像写一个机器人指令——“你 300 毫秒内从 A 走到 B”;react-spring 则是给你一个真实的弹簧——你只设定弹簧多硬、阻尼多大、物体多重,然后撒手让它自己弹过去。
你写:
const { x } = useSpring({ from: { x: 0 }, to: { x: 200 } })库内部每一帧用牛顿第二定律 + 胡克定律算出 x 应该在哪里——会冲过头一点,再回弹,再稳定。整个过程没有”持续时间”这个参数——只有弹性和阻尼。
这种”参数化物理”是 react-spring 区别于 framer-motion / CSS transition / anime 的核心,也是它能把”被打断的动画”做得最丝滑的原因。
为什么重要
不理解 react-spring 的物理思路,下面这些事就解释不了:
- 为什么手势驱动的 UI(拖拽卡片、Pinterest 长按)用 react-spring 会比 Framer Motion 更”贴手”
- 为什么 react-spring 比同类动画库小一半(~16KB vs 30KB+)但功能不少
- 为什么 react 树重渲染时 react-spring 仍能跑满 60fps——它绕过了 React 渲染
- 为什么 motion-one / Framer Motion 后来都加了
type: "spring"选项——是在追这条路线
核心要点
react-spring 的内部架构可以拆成 三层:
-
SpringValue(数学层):单个数值的物理引擎。每帧根据
tension(弹性)+friction(阻尼)+mass(质量)用欧拉积分算下一帧的位置。类比:一个独立的”弹簧+砝码”装置。 -
Controller(协调层):把多个 SpringValue(x / y / opacity / scale 等)协调起来——支持串行、并行、链式
.then()。类比:乐队指挥,让每个乐器(每个属性)按节拍一起演奏。 -
useSpring(React 桥):把 Controller 装进 React 组件的 ref,把数值暴露成
<animated.div>能订阅的”动画值”。关键:动画值不会触发 React 重渲染,而是直接改 DOM。
三层加起来背后还有一个全局 FrameLoop——所有 SpringValue 注册到同一个 requestAnimationFrame 循环里,静止后自我注销。
实践案例
案例 1:最小例子,让方块从 0 滑到 200px
import { useSpring, animated } from '@react-spring/web'
function Demo() { const styles = useSpring({ from: { x: 0 }, to: { x: 200 }, config: { tension: 170, friction: 26 }, }) return <animated.div style={styles}>hi</animated.div>}tension: 170 / friction: 26 是默认值——感觉像中等劲度的现实弹簧。把 friction 改成 12,方块会冲过 200 再回弹几次才稳定;改成 100,方块像爬过去。
案例 2:手势驱动,拖完撒手回弹
import { useSpring, animated } from '@react-spring/web'import { useDrag } from '@use-gesture/react'
function Card() { const [{ x, y }, api] = useSpring(() => ({ x: 0, y: 0 })) const bind = useDrag(({ down, movement: [mx, my] }) => { api.start({ x: down ? mx : 0, y: down ? my : 0 }) }) return <animated.div {...bind()} style={{ x, y }} />}按住时卡片跟手——撒手时速度连续地弹回原位,不是从撒手位置匀速回到 0。这种”打断时速度连续”是 spring 模型的天然能力。
案例 3:列表过场,进出场都丝滑
import { useTransition, animated } from '@react-spring/web'
function List({ items }) { const transitions = useTransition(items, { from: { opacity: 0, y: -20 }, enter: { opacity: 1, y: 0 }, leave: { opacity: 0, y: 20 }, }) return transitions((style, item) => ( <animated.li style={style}>{item}</animated.li> ))}新元素从上方滑入并淡入,移除元素往下滑出——每个元素单独跑一组 spring,互不干扰。
踩过的坑
-
极端参数会震荡发散:tension 10000 + friction 1 这种组合下欧拉积分会越积越大、动画疯了——库不会报警,靠预设
slow / wobbly / stiff引导你走稳定区间,新手凭直觉调数字会踩雷。 -
DevTools 看不到 animated 值:
<animated.div style={{ x }}>直接改 DOM 不走 React 渲染。React DevTools / Profiler 都看不到 x 的实时值,加console.log也只看得到 SpringValue 实例不是数字——必须订阅onChange。 -
SSR 第一帧会闪:
useLayoutEffect在服务器端被 React 警告,库降级到useEffect,hydrate 后第一帧值是from,下一帧才跳到to——视觉上闪一下。SSR 重的项目要注意。 -
列表别用 1000 个 useSpring:每个
useSpring各自一个 Controller,1000 个就是 1000 个 Controller。用useSprings(1000, ...)让一个 Controller 管 1000 个 SpringValue 才不会卡。
适用 vs 不适用场景
适用:
- 手势驱动 UI(拖拽、滑动、长按反馈)—— spring 的速度连续是杀手锏
- 物理感强的过场(卡片回弹、橡皮筋边缘)
- 需要小包体的 React 项目(~16KB vs Framer 的 30KB+)
- 多渲染目标(react-three-fiber、konva、native)共享一套动画 API
不适用:
- 复杂时间线动画(游戏过场、广告 banner)—— gsap 的 timeline 强得多
- 设计师习惯”3 秒内做这个”思维 —— Framer Motion 的 keyframe API 更直觉
- 纯静态 hover 过渡 —— CSS
transition0KB 更简单 - SSR 重 + 首屏带动画 —— 第一帧闪烁的代价值得考虑
历史小故事(可跳过)
- 2016 年:Cheng Lou 写了 react-motion,第一个用 spring 物理做 React 动画的库,但 API 是 render-prop 风格,hook 时代不友好。
- 2017 年:Paul Henschel 受 react-motion 启发写了 react-spring,最初也是 render-prop,v9 重写为 hook API。
- 2019 年:Paul 创立 pmndrs(Poimandres)开源集体,react-spring 和 react-three-fiber、zustand、jotai 一起进入这个生态。
- 2021 年:Framer Motion 加了
type: "spring"选项,相当于在自家 duration 引擎里”塞了一个 react-spring”——侧面证明 spring 范式是对的。
学到什么
- 物理参数比 duration 更通用:duration 是结果(“花多久”),spring 是原因(“多硬多重”)——用原因描述,打断时行为天然合理。
- 绕过框架渲染换性能:animated values 直接改 DOM 是高频动画的标准技巧,代价是调试体验损失——这是工程权衡。
- 全局调度器 + 自我注销:FrameLoop 模式可迁移到任何”高频回调 + 多订阅者”场景(心跳、IntersectionObserver、setInterval 任务队列)。
- API 设计帮用户做对:暴露
slow/wobbly/stiff预设而非 tension 数字,把”哪些参数稳定”知识沉淀进 API。
延伸阅读
- 官方文档:react-spring docs —— 大量交互式 demo,参数所见即所得
- 演讲视频:Paul Henschel — How to Animate React —— 作者亲自讲设计哲学
- 源码精读:SpringValue.ts —— 看欧拉积分怎么实现
- 物理基础:Spring Animation Bouncy —— 把 tension/friction 讲成肌肉记忆
- 配套手势:@use-gesture/react —— pmndrs 同生态,和 react-spring 黄金搭档
关联
- framer-motion —— 最大竞品,duration-first,包大但 API 直觉,spring 是后加的选项
- motion-one —— 走 WAAPI 路线的轻量库,和 react-spring 哲学不同但场景重叠
- anime —— 老牌 timeline 动画库,非 React 专属,无 spring 物理
- gsap —— 复杂时间线之王,react-spring 不和它正面竞争
- react —— react-spring 深度耦合 React 生命周期与 hook
- dnd-kit —— React 现代拖拽工具,常和 react-spring 联手做物理感拖拽
- konva —— Canvas 渲染目标之一,react-spring 通过 targets/konva 适配
反向链接
- anime —— anime.js — 一行 JS 让网页元素按时间线动起来
- d3 —— D3.js — 不是图表库,是写图表库的乐高
- dnd-kit —— dnd-kit — React 现代拖拽 toolkit
- framer-motion —— Framer Motion — React 声明式动画
- gsap —— GSAP — GreenSock 高性能动画
- konva —— Konva — 给 HTML5 Canvas 装一棵会响应的节点树
- motion-one —— Motion One — 把动画交给浏览器自己跑
- react —— React UI 组件库
- react-dnd —— react-dnd — React 时代第一个把拖拽拆成四层的库
- recharts —— Recharts — 用 JSX 直接拼出图表的 React 组件库
- styled-components —— styled-components — React 生态最早的 CSS-in-JS 库
- visx —— visx — 把 d3 拆成 30 块乐高的 React 可视化原语