配了三年 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(),感受一下「并行」带来的流畅度提升。
评论区
登录后可评论。