现代网站正通过日益宏大的视觉效果来争夺用户的注意力。视差英雄图(Parallax heroes)向视口深处延伸。轮播图(Carousels)无止境地滚动。背景视频在注册表单后自动播放。在设计作品集中,这些模式看起来精致且引人入胜。但对于一部分受众来说,它们会引发生理反应。

患有前庭功能障碍(vestibular disorders)的人在面对大面积持续运动时,会感到恶心、头晕或偏头痛。为了保护他们,每个主流操作系统现在都包含一个“减弱动态效果”(Reduce Motion)的辅助功能设置。Web 浏览器通过 prefers-reduced-motion 媒体查询来公开这一偏好。想要优雅地遵循这一设置的 React 开发者可以使用来自 @reactuses/coreuseReducedMotion hook。它将操作系统偏好读取为一个布尔值,如果用户更改了设置,它会实时更新,并且可以在不崩溃的情况下处理服务端渲染(SSR)。

为什么手动实现的方案会失效

你可以自己使用 window.matchMedia('(prefers-reduced-motion: reduce)') 来查询该偏好。许多开发者确实是这么做的,但也因此带来了许多隐蔽的 bug。

首先,你很容易忘记添加变更监听器。初始查询仅在组件挂载时运行一次。如果用户打开你的应用,然后因为轮播图让他们感到不适而在系统设置中切换了“减弱动态效果”,你的组件将永远无法接收到更新。媒体查询对象支持 addEventListener,但正确地进行绑定、在卸载时移除它,以及为旧版的 addListener 语法编写 polyfill,这些样板代码极易引发复制粘贴错误。

其次,在 SSR 期间 window 对象并不存在。如果你的应用在服务端渲染,一个简单的 matchMedia 调用就会抛出引用错误并导致渲染中断。你最终不得不将检查逻辑包裹在 typeof window !== 'undefined' 的保护语句中,猜测默认值,并祈祷客户端的水合(hydrate)过程能与之匹配。

第三,在每个动画组件中重复该逻辑会变成维护债。一位队友用一种方式编写监听器,另一位完全忽略它,而第三位则硬编码了一个倾向于动态效果的默认值。一个 hook 可以将这些混乱集中管理。

useReducedMotion 解决了所有这三个问题。它返回一个简单的布尔值。它在服务端可以安全地执行空操作(no-ops)。它会自动挂载并清理监听器。

三种应用方式

一旦你拿到了这个布尔值,你就需要一套应对策略。以下是三种模式,其适用范围可以从单个组件扩展到整个应用程序。

1. 通过控制 CSS 类来避免无效工作

最直接的方法是防止沉重的 JavaScript 动画循环启动。假设你有一个视差图像组件,它通常会监听滚动事件,并在 requestAnimationFrame 循环中转换图层。与其运行该逻辑然后再抑制视觉结果,不如先检查 useReducedMotion

如果 hook 返回 true,则使用静态变体类(static variant class)渲染组件,并跳过绑定滚动监听器的 useEffect。这样浏览器的工作量更小。对动态效果敏感的用户会看到一张静态图像,而其他用户则会看到移动的图层。由于动画代码从未初始化,你还可以顺便节省 CPU 和电池电量。

2. 直接将其传递给动画库

如果你使用像 Framer Motion 这样的库,该 hook 可以直接接入你的 prop 定义。Motion 组件接受用于 variants、transitions 和 gestures 的配置对象。你可以使用来自 useReducedMotion 的布尔值来有条件地在 transitions 中设置 duration: 0,或者