配了三年 View Transitions,每次想给列表和侧边栏同时加动画都要二选一——今天这件事被 element.startViewTransition() 的多实例并行彻底原生化了

配了三年 View Transitions,每次想给列表和侧边栏同时加动画都要二选一——今天这件事被 element.startViewTransition() 的多实例并行彻底原生化了


每次做复杂页面动效,最头疼的不是动效本身,而是「只能跑一个」。

比如一个商品列表页:顶部有个 tab 切换,侧边栏有个排序下拉,底部还有推荐区块。如果每个都要用 View Transitions 做平滑过渡,按传统做法只能排队——等一个跑完再跑下一个,不然就会互相覆盖。

但问题是,用户操作的时候,tab 切了,侧边栏排序也在变,推荐区块还在异步加载——这些本来就应该同时发生的。

Chrome 126 给了一个解法:在同一个 DOM 子树级别上跑多个独立的 element.startViewTransition(),每个只锁自己的那一小块,其他区域完全不受影响。

举一个最直接的场景:商品列表页。

// 列表区域单独跑 transition
const list = document.querySelector(".product-grid");
list.startViewTransition(() => {
  // 重新渲染列表
  list.innerHTML = newItems;
});

// 同时,侧边栏筛选区也跑自己的 transition
const filterPanel = document.querySelector(".filter-panel");
filterPanel.startViewTransition(() => {
  // 更新筛选状态
  updateFilterState(activeFilters);
});

这两个 transition 各自有独立的快照范围,互不干扰。用户点筛选的时候,列表在动,侧边栏也在动,整个页面看起来是协调的,而不是一个接一个地排队跳。

这背后的原理是:element.startViewTransition()::view-transition 伪树注入到调用它的那个元素上,而不是 document 级别。所以每个 transition 根元素只「暂停」自己的渲染子树,其他区域继续正常渲染和交互。

// DOM 结构大概长这样
// <ul class="product-grid">
//   ├── ::view-transition           ← 只在这里
//   │   └── ::view-transition-group(root)
//   │       ├── ::view-transition-old(root)
//   │       └── ::view-transition-new(root)
//   ├── <li>...</li>
//   └── ...
// </ul>

// 同时,filter-panel 那边是另一棵独立的伪树
// <aside class="filter-panel">
//   ├── ::view-transition           ← 另一棵
//   │   └── ::view-transition-group(root)
//   └── <div>...</div>
// </aside>

这两个 ::view-transition 树互相隔离,各自有各自动画时间线。你甚至可以给它们设置不同的动画时长和缓动曲线。

/* 列表区域:快切,300ms */
.product-grid::view-transition-group(root) {
  animation-duration: 300ms;
  animation-timing-function: ease-out;
}

/* 侧边栏:慢一点,500ms */
.filter-panel::view-transition-group(root) {
  animation-duration: 500ms;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

一个容易踩的坑:命名冲突。

如果两个不同区域的元素恰好都有 view-transition-name: card,在 document.startViewTransition() 里会冲突,必须手动改成不同的名字。但用 element.startViewTransition() 就省心——每个 transition 根有自己独立的名字空间,里面的 view-transition-name 是局部作用域,不会相互干扰。

/* 两个不同的 scoped transition 里都可以用同一个名字 */
.product-grid .card {
  view-transition-name: card;
}

.filter-panel .card {
  view-transition-name: card;  /* 不会冲突,因为在不同 scope */
}

另一个坑:overflow clipping。

document.startViewTransition() 里,有时候 overflow 的元素在 transition 期间会「漏」到容器外面。但在 element.startViewTransition() 里,溢出内容在 transition 期间保持被裁剪,不会跑到伪树的边界外面。官方文档特别提到了这个行为差异。

适合场景:

  • 仪表盘页面的多组件独立刷新
  • 列表筛选 + 排序同时触发
  • 表单里多个 section 的独立保存动画
  • 电商列表的快速切换排序(列表在动,侧边栏推荐也在刷新)

不适合场景:

  • 需要两个元素做跨区域的联合动画(比如一个元素从左侧飞到右侧,中间经过了另一个 transition 区域)
  • Safari/Firefox 用户(目前只有 Chrome 126+,覆盖率 ~68%,生产环境必须做 feature detection)
// 渐进增强写法
function animateWithTransition(el, updateFn) {
  if (el.startViewTransition) {
    el.startViewTransition(updateFn);
  } else {
    updateFn();
  }
}

Chrome 126 给了前端工程师一个以前只能靠框架才能实现的能力:多个独立区域同时跑动效,互不阻塞。FLIP 时代的「测两次,动画一次」被进一步简化成了「调用一次,自动快照」。如果你的页面有多个区域经常同时变,现在可以让他们各自优雅地动起来了。

下一步:找个有多个动态区域的真实页面,把其中一到两个改成 element.startViewTransition(),感受一下「并行」带来的流畅度提升。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 11 阅读