平台工程

reduced-motion 本机陷阱 · 动效降级纯淡入而非跳过

入档:2026-07-24 来源:蛛网之上首页「滚动渐现」动效上线后作者本机毫无变化的排查与修复(above-the-web src/pages/index.astro) 性质:⚠️ 二次踩坑升律。第一次见 网页主图logo流动动效迭代_液态变形与碎裂问题_v1(2026-06-26,当时用 !important 定向豁免收场);本次给出更通用的「降级而非禁用」模式,单独立档。

一句话律

Windows 关了「动画效果」的机器,所有浏览器都会全局上报 prefers-reduced-motion: reduce;网页动效对它的正确响应是「降级为纯淡入」,不是「整体跳过」——纯透明度渐变没有位移,不触发运动不适,规范允许。

对本机开发者的推论:你自己的机器就是 reduce 机时,凡是”严格尊重 reduce=跳过动画”的实现,你自测永远看不到效果,会误判”改动没生效”。

原理

事故现场(两次)

次序项目现象当时修法
① 2026-06-26鲸海拾贝首页 logo 光效”太淡 + 看不到”氛围光效用 !important 从 reduce 定向豁免
② 2026-07-24蛛网之上首页滚动渐现”我没有感觉到变化诶”reduce 下降级纯淡入(本篇正解)

两次的共同根因都不在代码,在本机是 reduce 机。第二次先查了部署、缓存、脚本执行,最后才用 PowerShell 查系统开关定性——下次遇到”动效本机看不到”,第一步就查这个:

$sig = '[DllImport("user32.dll")] public static extern bool SystemParametersInfo(uint uiAction, uint uiParam, ref bool pvParam, uint fWinIni);'
$t = Add-Type -MemberDefinition $sig -Name SPI -Namespace Win32 -PassThru
$anim = $false; [Win32.SPI]::SystemParametersInfo(0x1042, 0, [ref]$anim, 0) | Out-Null
"动画效果开启: $anim"   # False = 本机是 reduce 机

可复用成品:滚动渐现的完整实现模式

进入视口才淡入上移、同批错峰,四条硬要求全部满足(渐进增强 / reduce 降级 / 不抢 hover / 与筛选共存):

/* 隐藏态只在 JS 挂上 html.rv-on 后生效 —— 无 JS / 爬虫全量可见 */
html.rv-on .rv { opacity: 0; }
html.rv-on .rv.rv-in { animation: rv-in .65s cubic-bezier(.2,.7,.3,1) both; }
@keyframes rv-in { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } }
/* reduce:降级纯淡入,不是跳过 */
@media (prefers-reduced-motion: reduce) {
  html.rv-on .rv.rv-in { animation: rv-fade .5s ease both; }
}
@keyframes rv-fade { from { opacity: 0; } to { opacity: 1; } }
if ('IntersectionObserver' in window) {          // 只守 IO,不守 reduce(reduce 交给 CSS 降级)
  const els = [...document.querySelectorAll('.sec-head, .card, /* … */')];
  document.documentElement.classList.add('rv-on');
  const settle = (el) => { el.classList.remove('rv','rv-in'); el.style.animationDelay = ''; };
  els.forEach((el) => { el.classList.add('rv');
    el.addEventListener('animationend', () => settle(el), { once: true }); });
  const io = new IntersectionObserver((entries) => {
    let batch = 0;
    for (const en of entries) {
      if (!en.isIntersecting) continue;
      io.unobserve(en.target);
      const delay = Math.min(batch++ * 70, 350);          // 同批错峰
      en.target.style.animationDelay = delay + 'ms';
      en.target.classList.add('rv-in');
      setTimeout(() => settle(en.target), delay + 900);   // animationend 兜底
    }
  }, { rootMargin: '0px 0px -8% 0px', threshold: 0.05 });
  els.forEach((el) => io.observe(el));
}

设计要点(为什么这么写):

验证方法(headless Chrome 三坑)

  1. 不加参数的 headless 测到的就是本机真实路径:本机是 reduce 机时,headless 也上报 reduce——第一次看到”没触发”别急着断定代码坏了,先想想在测哪条路径。
  2. 强制关 reduce 的参数是 --blink-settings=prefersReducedMotion=false。注意 --force-prefers-reduced-motion 只能强制,给它拼 =no-preference 无效(实测仍是 reduce)。
  3. 统计口径两坑:
    • --virtual-time-budget 会把虚拟时间快进——动画早已放完、settle 早已摘类,元素既不在”待触发”也不带 rv-in,数错口径会误判”没生效”(其实是”已生效并清理完毕”,要数被还原成原始 class 的元素);
    • 构建器(如 Astro)会把 <style> 抽进外部 /_astro/*.css——grep 线上 HTML 验证 CSS 关键字会永远 0 命中,要先从 HTML 里摘出样式表 URL 再 curl 那个文件。

举一反三

关联文档

类型/平台工程