欢迎光临

View Transitions API 深度实战:用原生浏览器能力实现丝滑页面过渡动画

在前端开发中,页面切换时的视觉体验一直是个痛点。传统的单页应用(SPA)通过路由切换内容,但往往缺少流畅的过渡效果,页面变化显得突兀生硬;而多页应用(MPA)则只能依赖浏览器原生的页面加载行为,更谈不上什么过渡动画。为了解决这个问题,开发者不得不引入 Framer Motion、GSAP、Vue Transition 等第三方库,写大量的状态管理代码来实现元素进出动画。

View Transitions API 的出现彻底改变了这一局面。它是浏览器原生提供的页面过渡方案,无需任何第三方依赖,就能实现堪比原生 App 的丝滑过渡效果。从 Chrome 111 开始支持,到如今已覆盖 Chromium 全系列浏览器,Safari 18 也已跟进支持。本文将从原理到实战,带你全面掌握这项令人兴奋的 Web 新能力。

一、View Transitions API 的核心原理

View Transitions API 的核心思想非常巧妙——它并不直接操作 DOM,而是利用浏览器的合成层(Compositing)能力,在旧状态和新状态之间创建视觉过渡。整个流程分为四个关键步骤:

  • 捕获旧状态:浏览器对当前页面的可见元素进行截图,生成旧状态的视觉快照
  • DOM 更新:开发者正常修改 DOM(添加、删除、移动元素),浏览器不受限制
  • 捕获新状态:浏览器对新 DOM 状态进行截图,生成新状态的视觉快照
  • 动画过渡:浏览器在旧快照和新快照之间执行交叉淡入淡出(cross-fade)动画,或根据匹配的 view-transition-name 执行元素级别的过渡动画

这个设计的精妙之处在于:过渡动画完全由浏览器的合成线程处理,不会阻塞主线程,因此即使 DOM 更新较为复杂,动画依然能保持 60fps 的流畅度。

伪元素层级结构

当 View Transition 激活时,浏览器会创建以下伪元素树:


1
2
3
4
5
6
7
8
9
10
::view-transition
├── ::view-transition-group(root)
│   ├── ::view-transition-old(root)
│   └── ::view-transition-new(root)
├── ::view-transition-group(header)
│   ├── ::view-transition-old(header)
│   └── ::view-transition-new(header)
└── ::view-transition-group(card-1)
    ├── ::view-transition-old(card-1)
    └── ::view-transition-new(card-1)

其中 ::view-transition-old() 是旧状态的截图,::view-transition-new() 是新状态的截图。默认情况下,浏览器对 root 组执行交叉淡入淡出动画,而自定义的命名过渡组则可以配置独立的动画效果。

二、SPA 中的基础用法

在单页应用中使用 View Transitions API 非常简单,核心就是 document.startViewTransition() 方法。它接受一个回调函数,在回调中执行 DOM 更新操作:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 基础用法:点击切换照片
async function switchPhoto(newIndex) {
  // 检测浏览器是否支持
  if (!document.startViewTransition) {
    updatePhotoDOM(newIndex);
    return;
  }

  // 启动 View Transition
  const transition = document.startViewTransition(() => {
    updatePhotoDOM(newIndex);
  });

  // 可选:等待过渡动画完成
  await transition.finished;
  console.log('过渡动画已完成');
}

function updatePhotoDOM(index) {
  const img = document.querySelector('.photo');
  img.src = photos[index].url;
  img.alt = photos[index].title;
  document.querySelector('.photo-title').textContent = photos[index].title;
}

仅此而已!无需配置任何动画参数,浏览器默认就会为新旧状态添加一个 250ms 的交叉淡入淡出效果。虽然效果简单,但已经比生硬的瞬间切换好太多了。

配合 Vue / React 使用

在框架中使用时,关键是在 DOM 更新前后正确调用 API:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Vue 3 组合式 API 示例
import { nextTick } from 'vue';

async function navigateWithTransition(route) {
  if (!document.startViewTransition) {
    router.push(route);
    return;
  }

  const transition = document.startViewTransition(async () => {
    router.push(route);
    await nextTick(); // 等待 Vue 完成 DOM 更新
  });
  await transition.finished;
}

// React 示例
function handleNavigation(path) {
  if (!document.startViewTransition) {
    navigate(path);
    return;
  }

  document.startViewTransition(async () => {
    navigate(path);
    // React 18 自动批处理,flushSync 确保立即更新
    flushSync(() => {});
  });
}

注意 React 中需要使用 flushSync 强制同步更新,否则 React 的批处理机制会导致 DOM 更新延迟,从而让新状态截图为空。

三、自定义过渡动画:view-transition-name

默认的交叉淡入淡出虽然不错,但真正的威力在于 view-transition-name。通过给元素指定唯一的过渡名称,浏览器可以自动匹配新旧状态中的同一元素,并为其创建独立的过渡动画——包括位置变化时的平滑移动和尺寸变化时的平滑缩放。


1
2
3
4
5
6
7
8
9
10
11
12
13
/* 为卡片元素分配唯一的过渡名称 */
.photo-card {
  view-transition-name: none; /* 默认不参与过渡 */
}

.photo-card.active {
  view-transition-name: active-photo;
}

/* 标题的过渡名称 */
.page-title {
  view-transition-name: page-title;
}

关键规则:每个 view-transition-name 在同一时刻必须是唯一的。如果两个可见元素拥有相同的名称,浏览器会跳过该名称的过渡。这就是为什么上面的例子中只有 .active 的卡片才分配名称。

用 JavaScript 动态分配名称

在列表场景中,我们通常需要动态分配过渡名称:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 给每个列表项分配基于 ID 的唯一过渡名称
function setupTransitionNames() {
  document.querySelectorAll('.list-item').forEach(item => {
    const id = item.dataset.id;
    item.style.viewTransitionName = `item-${id}`;
  });
}

// 切换详情页时,给目标元素分配固定名称
async function openDetail(itemId) {
  // 先给即将进入详情的元素分配 'detail-hero' 名称
  const card = document.querySelector(`[data-id="${itemId}"]`);
  card.style.viewTransitionName = 'detail-hero';

  const transition = document.startViewTransition(() => {
    // 更新 DOM 到详情视图
    renderDetail(itemId);
    // 详情页的大图也用 'detail-hero' 名称
    document.querySelector('.detail-hero').style.viewTransitionName = 'detail-hero';
  });

  await transition.finished;

  // 过渡完成后清理,避免后续冲突
  card.style.viewTransitionName = '';
}

这种”动态分配、匹配过渡、清理名称”的模式是实现列表到详情页 Hero 动画的核心技巧。

四、CSS 自定义动画效果

通过伪元素,我们可以完全控制过渡动画的时长、缓动函数和关键帧:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* 自定义根级别的过渡效果 */
::view-transition-old(root) {
  animation: fade-out 300ms ease-out both;
}

::view-transition-new(root) {
  animation: fade-in 300ms ease-in both;
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes fade-in {
  from { opacity: 0; }
}

/* 为命名过渡组自定义动画 */
::view-transition-old(active-photo) {
  animation: shrink-out 400ms cubic-bezier(0.4, 0, 0.2, 1) both;
}

::view-transition-new(active-photo) {
  animation: grow-in 400ms cubic-bezier(0.4, 0, 0.2, 1) both;
}

@keyframes shrink-out {
  to {
    opacity: 0;
    transform: scale(0.8);
  }
}

@keyframes grow-in {
  from {
    opacity: 0;
    transform: scale(1.2);
  }
}

方向感知动画

一个常见需求是根据导航方向(前进/后退)播放不同动画。我们可以利用 :active-view-transition-type() 伪类或 JavaScript 设置类型来实现:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在 startViewTransition 中设置类型
const transition = document.startViewTransition({
  update: () => {
    updateDOM();
  },
  types: ['slide-forward'], // 自定义过渡类型
});

// 后退时使用不同类型
const backTransition = document.startViewTransition({
  update: () => {
    updateDOM();
  },
  types: ['slide-back'],
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 根据过渡类型应用不同动画 */
:active-view-transition-type(slide-forward) {
  ::view-transition-old(root) {
    animation: slide-out-left 300ms ease both;
  }
  ::view-transition-new(root) {
    animation: slide-in-right 300ms ease both;
  }
}

:active-view-transition-type(slide-back) {
  ::view-transition-old(root) {
    animation: slide-out-right 300ms ease both;
  }
  ::view-transition-new(root) {
    animation: slide-in-left 300ms ease both;
  }
}

@keyframes slide-out-left {
  to { transform: translateX(-30%); opacity: 0; }
}
@keyframes slide-in-right {
  from { transform: translateX(30%); opacity: 0; }
}
@keyframes slide-out-right {
  to { transform: translateX(30%); opacity: 0; }
}
@keyframes slide-in-left {
  from { transform: translateX(-30%); opacity: 0; }
}

五、MPA 跨页面过渡

这是 View Transitions API 最令人兴奋的扩展——传统的多页应用也能享受丝滑过渡!Chrome 126+ 支持了跨文档的 View Transition,通过一个简单的 meta 标签即可启用:


1
2
<!-- 在 <head> 中添加 -->
<meta name="view-transition" content="same-origin">

仅此一行,同源页面之间的导航就会自动获得交叉淡入淡出效果。浏览器会在旧页面卸载前截图,在新页面渲染后截图,然后在两者之间执行过渡动画。

跨页面动画的 CSS 配置

由于涉及两个不同的文档,CSS 需要在两个页面中保持一致:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 两个页面中都要定义 */
.site-header {
  view-transition-name: site-header;
}

.sidebar {
  view-transition-name: sidebar;
}

/* 排除不需要过渡的元素 */
.ad-banner,
.cookie-notice {
  view-transition-name: none;
}

浏览器会自动匹配两个页面中相同 view-transition-name 的元素,实现跨页面的平滑过渡。比如导航栏在页面切换时不会闪动,而是平滑地保持在原位。

阻止自动过渡

并非所有导航都需要过渡动画。通过 pageswap 和 pagereveal 事件可以精确控制:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 在页面中监听 pageswap 事件
window.addEventListener('pageswap', event => {
  // 如果是跨域导航,不执行过渡
  if (!event.activation.navigationType.includes('cross-origin')) {
    return;
  }

  // 如果用户按了 Ctrl 点击(新标签页打开),跳过过渡
  if (event.activation.navigationType.includes('new-tab')) {
    event.skipTransition();
  }
});

// 在新页面监听 pagereveal 事件
window.addEventListener('pagereveal', event => {
  // 如果是从外站跳转来的,跳过过渡
  if (!event.activation.fromExternal) {
    return;
  }
  event.skipTransition();
});

六、实战案例:图片画廊 Hero 动画

下面我们用一个完整的图片画廊案例来串联所有知识点。效果是:点击缩略图,图片平滑飞入到详情页大图位置,配合背景淡入:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 缩略图列表 -->
<div class="gallery">
  <img src="photo1.jpg" class="thumb" data-id="1" alt="风景">
  <img src="photo2.jpg" class="thumb" data-id="2" alt="人像">
  <img src="photo3.jpg" class="thumb" data-id="3" alt="建筑">
</div>

<!-- 详情视图 -->
<div class="detail-view" hidden>
  <img class="detail-image" src="" alt="">
  <h2 class="detail-title"></h2>
  <p class="detail-desc"></p>
  <button class="back-btn">← 返回</button>
</div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* CSS 关键样式 */
.thumb {
  cursor: pointer;
  border-radius: 8px;
  transition: transform 0.2s;
}

.thumb:hover {
  transform: scale(1.05);
}

.detail-image {
  view-transition-name: hero-image;
  border-radius: 12px;
}

/* 过渡动画自定义 */
::view-transition-old(hero-image) {
  animation: 350ms ease-out both fade-scale-out;
}

::view-transition-new(hero-image) {
  animation: 350ms ease-in both fade-scale-in;
}

@keyframes fade-scale-out {
  to {
    opacity: 0;
    transform: scale(0.9);
  }
}

@keyframes fade-scale-in {
  from {
    opacity: 0;
    transform: scale(1.1);
  }
}

/* 标题过渡 */
.detail-title {
  view-transition-name: detail-title;
}

::view-transition-group(hero-image) {
  /* 位置和尺寸变化使用更平滑的缓动 */
  animation-duration: 400ms;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// JavaScript 交互逻辑
const gallery = document.querySelector('.gallery');
const detailView = document.querySelector('.detail-view');
const detailImage = document.querySelector('.detail-image');
const detailTitle = document.querySelector('.detail-title');
const backBtn = document.querySelector('.back-btn');

const photos = [
  { id: '1', src: 'photo1.jpg', title: '山间晨雾', desc: '清晨的山谷笼罩在薄雾中...' },
  { id: '2', src: 'photo2.jpg', title: '城市人像', desc: '街头的一瞬间...' },
  { id: '3', src: 'photo3.jpg', title: '古典建筑', desc: '百年历史的建筑...' },
];

// 点击缩略图 → 打开详情
gallery.addEventListener('click', async (e) => {
  const thumb = e.target.closest('.thumb');
  if (!thumb) return;

  const photoId = thumb.dataset.id;
  const photo = photos.find(p => p.id === photoId);

  // 关键:给缩略图分配与详情图相同的过渡名称
  thumb.style.viewTransitionName = 'hero-image';

  const transition = document.startViewTransition(() => {
    // 更新 DOM
    gallery.hidden = true;
    detailView.hidden = false;
    detailImage.src = photo.src;
    detailImage.alt = photo.title;
    detailTitle.textContent = photo.title;
  });

  await transition.finished;

  // 过渡完成后清理缩略图的过渡名称
  thumb.style.viewTransitionName = '';
});

// 点击返回 → 回到画廊
backBtn.addEventListener('click', async () => {
  const currentPhotoId = detailImage.src.match(/photo(\d)/)?.[1];
  const thumb = document.querySelector(`[data-id="${currentPhotoId}"]`);

  // 再次分配过渡名称以实现返回动画
  if (thumb) {
    thumb.style.viewTransitionName = 'hero-image';
  }

  const transition = document.startViewTransition(() => {
    detailView.hidden = true;
    gallery.hidden = false;
  });

  await transition.finished;

  if (thumb) {
    thumb.style.viewTransitionName = '';
  }
});

七、性能优化与注意事项

View Transitions API 虽然由浏览器合成线程驱动,但仍需注意性能问题:

优化项 建议 原因
减少参与过渡的元素 只给关键元素设置 view-transition-name 每个命名过渡组都需要截图和合成,过多会影响性能
避免大面积元素参与 大尺寸容器不要设置过渡名称 截图像素量与元素尺寸成正比,大图会消耗大量 GPU 内存
缩短动画时长 控制在 200-400ms 过渡期间旧状态截图常驻内存,过长的动画增加内存压力
使用 will-change 在过渡前设置 提前创建合成层,减少首帧延迟
及时清理名称 过渡完成后移除动态名称 避免名称冲突导致后续过渡失败

内存管理

过渡期间,浏览器需要同时持有旧状态和新状态的截图。对于包含大量图片或复杂布局的页面,这意味着 GPU 内存消耗会短暂翻倍。在移动设备上,这可能导致页面被系统回收。建议:


1
2
3
4
5
6
7
8
9
10
// 在低端设备上禁用或简化过渡
if (navigator.hardwareConcurrency <= 4 || navigator.deviceMemory <= 2) {
  // 禁用 view-transition-name
  document.documentElement.classList.add('reduce-transitions');
}

/* 配合 CSS */
.reduce-transitions * {
  view-transition-name: none !important;
}

八、浏览器兼容性与渐进增强

截至目前(2025年底),View Transitions API 的浏览器支持情况如下:

  • Chrome 111+:完整支持 SPA 的 startViewTransition()
  • Chrome 126+:支持 MPA 跨文档过渡
  • Safari 18+:支持 SPA 过渡,MPA 过渡仍在开发中
  • Firefox:仍在实现中(Nightly 版本可用)

渐进增强策略非常简单:


1
2
3
4
5
6
7
8
9
10
11
// 标准的渐进增强模式
function updateView() {
  if (document.startViewTransition) {
    document.startViewTransition(() => {
      updateTheDOMSomehow();
    });
  } else {
    // 不支持时直接更新,无过渡效果
    updateTheDOMSomehow();
  }
}

这种写法确保在不支持的浏览器中功能完全正常,只是缺少动画效果。也可以使用 CSS @supports 查询:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 仅在支持 View Transitions 时应用特定样式 */
@supports (view-transition-name: auto) {
  .card {
    view-transition-name: var(--card-name);
  }
}

/* 不支持时的降级方案 */
@supports not (view-transition-name: auto) {
  .card {
    transition: transform 0.3s ease;
  }
  .card:hover {
    transform: scale(1.02);
  }
}

九、与第三方动画库的对比

很多开发者会问:我已经在用 Framer Motion / Vue Transition / GSAP 了,还需要 View Transitions API 吗?答案是:两者并非替代关系,而是互补关系。

特性 View Transitions API Framer Motion GSAP + Flip
依赖 无(浏览器原生) React 生态 需引入库
跨页面过渡 MPA 原生支持 仅 SPA 内 需手动处理
DOM 退出动画 自动处理 需 AnimatePresence 需手动处理
弹簧物理动画 仅 CSS 缓动 内置弹簧系统 可自定义
手势交互动画 不支持 拖拽/滑动 可自定义
性能 合成线程 主线程 主线程
学习成本

最佳实践是:页面级别的过渡用 View Transitions API(简单、高性能、零依赖),组件级别的交互动画继续使用你熟悉的库。两者可以完美共存。

十、总结与展望

View Transitions API 是近年来 Web 平台最令人兴奋的新能力之一。它用极低的 API 表面积解决了长期困扰前端开发的页面过渡问题,让原生 Web 应用的视觉体验终于可以与原生 App 一较高下。

核心要点回顾:

  • SPA 中使用 document.startViewTransition() 即可启用,无需任何配置
  • view-transition-name 是实现元素级过渡动画的关键,名称必须唯一
  • 通过伪元素可以完全自定义动画效果和时长
  • MPA 跨文档过渡只需一行 meta 标签
  • 渐进增强策略简单直接,不支持时自动降级为无动画
  • 注意性能优化,减少参与过渡的元素数量和尺寸

随着 Firefox 的实现推进和 Safari 持续完善 MPA 支持,View Transitions API 将很快成为所有现代浏览器的标准能力。现在就是最佳的学习和接入时机——零成本升级,巨大体验提升,没有理由不开始使用它。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » View Transitions API 深度实战:用原生浏览器能力实现丝滑页面过渡动画
分享到: 更多 (0)