正确做法是用 requestAnimationFrame 更新 transform,配合 pointer-events: none 和 touchmove 兼容处理。需声明 lastX/lastY 存坐标,用 transform: translate3d() 触发 GPU 加速,禁用 background-position,移动端须加 {passive: false} 并用 touches[0],且设 touch-action: none。

用 mousemove 监听坐标但别直接改 left/top
直接在 mousemove 里写 img.style.left = e.clientX + 'px' 看似简单,实际会高频触发重排,鼠标一甩就卡顿、拖影。现代浏览器每秒可能发上百次 mousemove,但屏幕只刷新 60 次,多余计算全白费。
正确做法是只存坐标,不操作 DOM:
- 声明两个变量
lastX、lastY,在事件里只更新它们 - 用
requestAnimationFrame包一层回调,在回调里读取最新值并更新style.transform - 确保图片的
position是absolute或fixed,否则transform位移会失效
为什么必须用 transform: translate() 而不是 background-position
background-position 改的是背景图自身偏移,它走的是 layout → paint 流程,CPU 占用高,低端机上明显掉帧;而 transform: translate() 触发合成层(compositor)渲染,不重排不重绘,稳在 60fps。
另外注意:
立即学习“前端免费学习笔记(深入)”;
- 如果图片是作为背景图存在(比如
div的background-image),那它本质上不是“图片元素”,不能被transform移动——得换成<img>标签或伪元素承载 - 用
translate3d(x, y, 0)比translate(x, y)更容易触发 GPU 加速,尤其在 Safari 上效果更稳 - 别忘了加
will-change: transform,但只加在需要跟随的元素上,别全局写
移动端 touchmove 必须处理 preventDefault()
mousemove 在 iOS Safari 和多数安卓浏览器里压根不触发,纯靠它,手机端就是空白。必须监听 touchmove 并手动提取触点坐标。
常见翻车点:
- 没加
{ passive: false }就调e.preventDefault(),结果报错且滚动照常发生 - 用了
e.changedTouches[0],但手指刚落时它可能为空;应该用e.touches[0] - 没设
touch-action: none,导致双指缩放、长按菜单等系统手势干扰跟随逻辑 - 只监听了
touchmove,却忘了在touchstart时初始化状态,iOS Safari 要求用户交互后才能激活 touch 事件监听
pointer-events: none 不是可选项,是必选项
跟随的图片默认会盖在页面内容上,挡住下层按钮、链接、hover 效果——这不是 bug,是 CSS 层叠规则的自然结果。唯一解法是给该图片加 pointer-events: none。
这个样式意味着:鼠标能“穿过”它,点击、悬停、拖拽都作用于它底下的真实元素。没有这行,用户点不到按钮,菜单 hover 不出来,整个交互链就断了。
顺带提醒:
- 如果图片本身需要响应点击(比如做成可交互图标),那就不能加
pointer-events: none,得换思路:用z-index控制层级,或把事件委托到父容器 - 滚动页面时,
clientX/clientY坐标仍是视口内坐标,不用加window.scrollX/Y——除非你用的是position: absolute且想锚定文档流位置
真正难的不是让图片动起来,而是让它在缩放、滚动、高 DPR 屏幕、混合输入(鼠标+触摸)下都不偏移、不卡顿、不遮挡。这些细节不测真机根本发现不了。



















