motion-anything 的动效在页面里不动 / 是容器尺寸还是初始化顺序的问题?

文章导读
动效完全不动时,先别急着改引擎参数。更常见的两个原因是:容器在页面里实际宽高为 0,或初始化调用发生在容器挂载之前。这两件事可以分开验证:先在控制台量容器的真实宽高,再把初始化调用挪到 DOM 就绪之后做一次对照,最后用一个不依赖交互的最小动效确认引擎链路本身跑没跑起来。三步都做完,再把原因写下来,结论才站得住。
📋 目录
  1. 一 在控制台确认容器是否真的有宽高
  2. 二 把初始化调用挪到 DOM 就绪之后做对照
  3. 三 用一个最小静态动效验证引擎链路
  4. 四 区分没渲染与渲染了但看不见
  5. 五 写下归因结论并保留两次对照记录
A A

动效完全不动时,先别急着改引擎参数。更常见的两个原因是:容器在页面里实际宽高为 0,或初始化调用发生在容器挂载之前。这两件事可以分开验证:先在控制台量容器的真实宽高,再把初始化调用挪到 DOM 就绪之后做一次对照,最后用一个不依赖交互的最小动效确认引擎链路本身跑没跑起来。三步都做完,再把原因写下来,结论才站得住。

动效完全不动,通常先量容器宽高,再做 DOM 时机对照,最后用最小静态动效验证引擎链路。容器宽高为 0 时,需要检查父级 display、flex/grid 高度和 overflow;宽高正常但仍不动,才怀疑初始化早于挂载,常见做法是把调用放进 DOMContentLoaded 或双 requestAnimationFrame。三步都无变化,问题多半在“渲染了但看不见”,要去查透明度、层级和裁剪。结论只对应本次浏览器与构建产物。

在控制台确认容器是否真的有宽高

先把容器当成一个普通 DOM 元素来量,不要先看动效代码。打开页面,在控制台执行下面这段,选择器换成你项目里真实的容器:

const el = document.querySelector('#stage');
if (!el) {
  console.warn('容器不存在,先确认选择器和挂载顺序');
} else {
  const r = el.getBoundingClientRect();
  const cs = getComputedStyle(el);
  console.log('rect', r.width, r.height);
  console.log('offset', el.offsetWidth, el.offsetHeight);
  console.log('display', cs.display, 'position', cs.position, 'overflow', cs.overflow);
  const p = el.parentElement;
  if (p) {
    const pr = p.getBoundingClientRect();
    console.log('parent', pr.width, pr.height, getComputedStyle(p).display);
  }
}

父级把容器压扁的情况,通常有这几类,按出现频率从高到低:父级高度是 auto,子元素又全部绝对定位,父级实际高度就被算成 0;flex 容器里子项没设 flex-basis 或 min-height,被内容收缩到 0;grid 行高没定义,容器落在高度为 0 的行里;某个祖先 display:none 后又改为可见,但尺寸计算没有重新触发;容器 width:100%,而更上层的祖先宽度本身是 0;还有祖先写了 transform: scale(0) 或 overflow:hidden 配合高度为 0。

页面表现也能帮忙判断:尺寸为 0 时,往往“容器位置什么都没有”,但内部绝对定位元素可能溢出到别处仍然可见,容易让人误以为渲染成功;尺寸正常时,容器区域至少能用鼠标选中或用高亮工具框出来。宽高为 0 就先修布局,别去改动效参数。

把初始化调用挪到 DOM 就绪之后做对照

宽高确认正常再动时机。对照实验只改一件事:调用位置。其他变量固定住——同一份构建产物、同一浏览器、同一容器选择器、同一组动效参数、清空缓存后刷新、关闭网络面板的限速,避免把网络慢当成时机问题。

motion-anything 的动效在页面里不动 / 是容器尺寸还是初始化顺序的问题?
// A 组:脚本一执行就调用
console.log('[A] readyState=', document.readyState, 't=', performance.now());
console.log('[A] container=', !!document.querySelector('#stage'));
initMotion(document.querySelector('#stage'));

// B 组:等 DOM 就绪后再调用
document.addEventListener('DOMContentLoaded', () => {
  console.log('[B] readyState=', document.readyState, 't=', performance.now());
  const host = document.querySelector('#stage');
  console.log('[B] container=', !!host);
  initMotion(host);
});

如果框架里容器是异步渲染出来的,DOMContentLoaded 也可能早于容器出现,这时可以再退一步,用两次 requestAnimationFrame 或 ResizeObserver 第一次回调触发初始化,并把每次调用的时间和当时容器是否存在一起打出来:

requestAnimationFrame(() => requestAnimationFrame(() => {
  const host = document.querySelector('#stage');
  console.log('[C] t=', performance.now(), 'host=', !!host, 'w=', host && host.offsetWidth);
  if (host) initMotion(host);
}));

两次输出要连起来看顺序:如果 A 组打印 container=false 或宽度为 0,而 B、C 组为 true 且宽度正常,并且动效在 B、C 组下开始播放,基本可以判定是初始化时机问题。如果三组都是 false,问题在容器挂载本身,回到上一节。

用一个最小静态动效验证引擎链路

时机也对齐了还不播放,就要确认引擎有没有被真正调用起来。写一个加载即播放、不需要点击或滚动的最小页面,容器尺寸直接写死,排除布局干扰:

<div id="stage" style="width:320px;height:180px;position:relative;background:#f2f2f2"></div>
<script type="module">
  // 这里的 createMotion 换成你项目里真实的创建/挂载入口
  const host = document.querySelector('#stage');
  const inst = createMotion({ container: host, autoplay: true });
  console.log('instance', inst);
  console.log('children after init', host.children.length, 'canvas', host.querySelectorAll('canvas').length);

  const t0 = performance.now();
  const timer = setInterval(() => {
    const node = host.firstElementChild;
    console.log('t=', Math.round(performance.now() - t0),
      node && getComputedStyle(node).transform,
      node && getComputedStyle(node).opacity);
  }, 200);
  setTimeout(() => clearInterval(timer), 3000);
</script>

记录方式固定成三句话:初始化后容器里新增了几个节点或画布;采样期间 transform / opacity 的值变没变;变化持续了多久(例如采样 3 秒内前几次有变化、之后归于某个固定值,就说明播放过但被停住)。节点都没有,是引擎没执行;节点在、值不动,是播放未启动或被暂停;值在动但页面看不到,进下一节。

区分没渲染与渲染了但看不见

“看不见”经常被误判成“没渲染”。检查顺序建议是:先看容器自身的 opacity、visibility,再看子元素的 opacity 与 z-index,然后看祖先有没有 stacking context 和 overflow:hidden、clip-path。可以在控制台逐个往上打印:

motion-anything 的动效在页面里不动 / 是容器尺寸还是初始化顺序的问题?
let n = document.querySelector('#stage');
while (n && n !== document.documentElement) {
  const s = getComputedStyle(n);
  console.log(n.tagName, n.id || n.className, 'opacity', s.opacity, 'visibility', s.visibility,
    'overflow', s.overflow, 'z-index', s.zIndex, 'transform', s.transform, 'clip-path', s.clipPath);
  n = n.parentElement;
}

再把容器临时可视化,加几行样式刷新页面看:

#stage { outline: 2px solid #e00; background: rgba(0,0,0,.06); }
#stage > * { outline: 1px dashed #08f; }

判断依据很直接:加了边框后,能看到虚线框里的内容在动,说明元素在渲染,问题在透明度、层级或裁剪(含画布 CSS 尺寸与 canvas 宽高属性不一致、被父级 transform 缩放后缩到不可见);边框在、里面始终是空的,是没渲染;连边框都只有一条线、没有高度,说明容器仍为 0 尺寸,回第一节处理。

写下归因结论并保留两次对照记录

排查到最后一步,把过程落成可复查的记录,不要只留一句“改好了”。建议用同一张表,一条改动一行:

  1. 改动项:例如“初始化调用从模块顶层移到 DOMContentLoaded”“容器父级加 min-height”“临时加 outline 边框”。
  2. 观察现象:控制台打印的时间戳、容器宽高数值、采样到的 transform / opacity 变化、页面上是否可见。
  3. 排除掉的原因:例如“容器宽高正常,排除零尺寸”“B 组与 A 组表现一致,排除单纯时机问题”。
  4. 最终结论:一句话写清是尺寸、时机、引擎调用还是可见性问题,并附上作出判断的那条输出。

两次对照记录(改前 / 改后)都要留着,最好把控制台输出复制到一个文件里,标注浏览器版本、屏幕尺寸和构建产物标识。结论只适用于本次测试环境:换浏览器、换嵌入容器、改布局后,零尺寸和初始化顺序都可能重新出现,需要按上面五步再走一遍,而不是直接套用上次的结论。