根本原因是CSS变量未注册@property,导致transition失效;应注册@property声明语法类型,JS仅更新--x/--y变量,由CSS通过transform: translate(var(--x), var(--y))和will-change: transform实现GPU加速平滑拖拽。

移动端手势拖拽卡片卡顿、脱手、松手后跳变,根本原因不是 JS 逻辑慢,而是 CSS 层面没把位移控制权交还给渲染引擎——transform 直接写死在 JS 里会触发主线程重算样式,而 --x、--y 这类 CSS 变量配合 @property 才能让浏览器自己插值、合成、丢帧时平滑降级。
必须注册 @property,否则变量无法参与动画插值
只声明 --x: 0px 不够。浏览器默认把自定义属性当普通字符串处理,transition: --x 0.2s 会静默失效。必须用 @property 显式告诉浏览器这是可动画的数值类型:
/* 必须加在 :root 或对应作用域 */
@property --x {
syntax: '<length>';
inherits: false;
initial-value: 0px;
}- 不写
@property,transition对--x完全无效,拖动只能靠 JS 强制重绘,掉帧明显 -
syntax值必须匹配实际使用单位:<length></length>(支持 px/em/rem/vw)、<number></number>(无单位)、<percentage></percentage>;写成<color></color>就没法做位移 - iOS Safari 16.4+ 和 Chrome 115+ 支持,旧版 Android WebView 需降级为
transform: translateX(0)+will-change
拖拽中只更新变量,不碰 style.transform
JS 里每帧都调 el.style.transform = `translateX(${x}px)` 是性能黑洞。正确路径是:JS 只改变量,CSS 负责映射和过渡:
.card {
--x: 0px;
--y: 0px;
transform: translate(var(--x), var(--y));
transition: --x 0.15s cubic-bezier(0.4, 0, 0.2, 1),
--y 0.15s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}- touchmove 回调里只执行
el.style.setProperty('--x', x + 'px'),其余交给 CSS 引擎 -
transition写在变量上,不是transform上;否则变量变、transform 不变,动画不触发 -
will-change: transform必须保留,否则 GPU 图层不提前创建,首次拖动仍卡顿 - 松手后惯性滚动要另起一套逻辑(如用
scroll-behavior: smooth模拟),CSS 变量本身不带物理引擎
边界限制必须用 getBoundingClientRect() 实时算,别信 offsetWidth
移动端元素可能被缩放、父容器有 transform、或启用了 text-size-adjust,导致 offsetWidth 返回错误尺寸。边界判断必须基于真实渲染位置:
立即学习“前端免费学习笔记(深入)”;
- 每次
touchmove前,先调const rect = el.getBoundingClientRect() - 再结合父容器
parentEl.getBoundingClientRect()算出左/右/上/下最大允许值 - 对
--x做 clamp:Math.min(maxX, Math.max(minX, x)),然后才setProperty - 别用
clientX - startX累加,手指快速滑动时会因事件节流导致累计误差,应始终以当前触点绝对坐标为准
真正难的是变量粒度——--x 和 --y 分开过渡能避免斜向拖拽时一个方向抢帧,但多变量会让 @property 注册变复杂;而把位移打包进 --pos: 100px 200px 又没法单独过渡。这些取舍,只有在真机连拖三十次、换三台安卓机对比帧率后才会心里有数。


















