Vấn đề
Hiệu ứng chuyển đổi khung hiển thị giữa các tài liệu tạo ảnh động cho các phần tử giữa hai trang trong quá trình điều hướng cùng nguồn gốc. Trình duyệt chụp nhanh trang cũ, điều hướng, sau đó tạo ảnh động từ ảnh chụp nhanh đến trang mới. Nếu trang mới chưa tải xong các tài nguyên quan trọng (biểu định kiểu, tập lệnh bố cục hoặc phần tử DOM chính), thì hiệu ứng chuyển đổi sẽ tạo ảnh động cho trạng thái chưa hoàn chỉnh hoặc chưa được tạo kiểu. Điều này gây ra các lỗi hình ảnh như các phần tử biến đổi sang vị trí không chính xác, nội dung dàn lại giữa ảnh động hoặc phông chữ dự phòng nhấp nháy thành phông chữ web sau khi quá trình chuyển đổi hoàn tất.
Giải pháp
Sử dụng blocking="render" trên các phần tử <link> và <script> quan trọng trong <head> của trang mới, đồng thời sử dụng <link rel="expect"> để chặn quá trình kết xuất cho đến khi các phần tử DOM cụ thể được phân tích cú pháp. Điều này đảm bảo trình duyệt không bắt đầu ảnh động chuyển đổi khung hiển thị cho đến khi trạng thái hình ảnh của trang mới ổn định. Trình duyệt tiếp tục phân tích cú pháp HTML ở chế độ nền — chỉ trì hoãn việc vẽ.
Chiến lược triển khai
- BẮT BUỘC: Chọn sử dụng hiệu ứng chuyển đổi khung hiển thị giữa các tài liệu bằng quy tắc CSS
@view-transitiontrên cả hai trang. - BẮT BUỘC: Đảm bảo các biểu định kiểu quan trọng nằm trong
<head>. Theo mặc định, các biểu định kiểu trong<head>sẽ chặn hiển thị. Biểu định kiểu được chèn động yêu cầublocking="render"rõ ràng. - NÊN sử dụng
blocking="render"trên các phần tử<script>phải thực thi trước khi hiệu ứng chuyển đổi diễn ra (ví dụ: tập lệnh áp dụng giao diện hoặc ảnh hưởng đến bố cục). - NÊN sử dụng
<link rel="expect" href="#element-id" blocking="render">để chặn việc hiển thị cho đến khi nội dung trong màn hình đầu tiên được phân tích cú pháp. Điều này áp dụng cho tất cả các loại hiệu ứng chuyển đổi: hiệu ứng mờ dần trên toàn trang (để tránh tạo ảnh động cho trang trống), ảnh động biến đổi (để đảm bảo các phần tử được đặt tên tồn tại trong DOM) và bố cục phụ thuộc vào tập lệnh (để đảm bảo nội dung được tạo kiểu được phân tích cú pháp). - KHÔNG chặn kết xuất trên nội dung không quan trọng. Chỉ chặn các tài nguyên và phần tử ảnh hưởng đến khung nhìn ban đầu. Việc chặn quá nhiều nội dung sẽ làm chậm quá trình chuyển đổi và làm giảm hiệu suất nhận biết.
Hướng dẫn triển khai
Bước 1: Chọn sử dụng tính năng Chuyển đổi khung hiển thị giữa các tài liệu
BẮT BUỘC: Cả trang nguồn và trang đích đều phải có quy tắc @ @view-transition. Nếu không có thuộc tính này, quá trình chuyển đổi giữa các tài liệu sẽ không diễn ra.
/*
MANDATORY: Include this rule in every page that participates
in cross-document view transitions.
`navigation: auto` enables transitions for standard navigations
(link clicks, form submissions, back/forward).
*/
@view-transition {
navigation: auto;
}
/* MANDATORY Copy-Paste Safety: Disable cross-document view transitions for users requesting reduced motion */
@media (prefers-reduced-motion: reduce) {
@view-transition {
navigation: none;
}
}
Bước 2: Chặn hiển thị cho đến khi các tập lệnh quan trọng thực thi
Nếu một tập lệnh không chặn trong <head> phải chạy trước khi hiệu ứng chuyển đổi tạo ảnh động (ví dụ: để áp dụng một lớp giao diện hoặc ảnh hưởng đến bố cục), hãy đánh dấu tập lệnh đó bằng blocking="render". Nếu không có thuộc tính này, tập lệnh async, defer hoặc type="module" có thể thực thi sau khi quá trình chuyển đổi đã bắt đầu.
<head>
<!--
DO: Mark layout-critical scripts with blocking="render".
-->
<script type=module blocking="render">
// Example: apply a stored theme before the page renders,
// so the transition snapshot reflects the correct theme.
document.documentElement.dataset.theme =
localStorage.getItem('theme') || 'light';
</script>
</head>
Bước 3: Chặn hiển thị cho đến khi các phần tử DOM chính được phân tích cú pháp
Biểu định kiểu và tập lệnh blocking="render" trong <head> chỉ đảm bảo rằng <head> đã được xử lý đầy đủ. Các tập lệnh này không chờ phân tích cú pháp bất kỳ nội dung <body> nào. Nếu không chặn thêm, trình duyệt có thể chụp nhanh trang mới trước khi các phần tử trong màn hình đầu tiên tồn tại trong DOM, dẫn đến hiệu ứng chuyển đổi tạo ảnh động cho một trang trống hoặc trang được kết xuất một phần.
<link rel="expect"> giải quyết vấn đề này bằng cách chặn quá trình kết xuất cho đến khi một phần tử cụ thể (được xác định bằng id) được phân tích cú pháp. Giá trị href phải là giá trị nhận dạng mảnh (ví dụ: #hero) khớp với thuộc tính id của phần tử mục tiêu. Sau khi thẻ đóng của phần tử đó được phân tích cú pháp, khối kết xuất sẽ được giải phóng. Xin lưu ý rằng <link rel="expect"> chỉ đảm bảo rằng phần tử DOM mục tiêu tồn tại trong tài liệu, chứ không đợi các tài nguyên phụ bên ngoài (chẳng hạn như pixel <img>) tải xuống hoặc giải mã.
NÊN sử dụng <link rel="expect"> trong tất cả các trường hợp sau:
Trường hợp sử dụng 1: Làm mờ dần toàn trang
Ngay cả khi không có phần tử riêng lẻ nào có view-transition-name, thì hiệu ứng chuyển đổi root mặc định sẽ làm mờ toàn bộ trang. Nếu ảnh chụp nhanh của trang mới được chụp trước khi nội dung trong màn hình đầu tiên được phân tích cú pháp, thì hiệu ứng mờ dần sẽ tạo ảnh động từ trang cũ sang trang trống hoặc trang chưa hoàn chỉnh. Chặn hiển thị trên một phần tử đánh dấu điểm kết thúc của nội dung hiển thị trong màn hình đầu tiên.
<head>
<link rel="stylesheet" href="/css/styles.css">
<!--
DO: Block rendering until the main content area is parsed,
even for a simple cross-fade. Without this, the browser may
snapshot the page before visible content exists in the DOM,
causing the cross-fade to reveal a blank or partial page.
-->
<link rel="expect" href="#main-content" blocking="render">
</head>
<body>
<header>...</header>
<main id="main-content">
<h1>Page Title</h1>
<p>Above-the-fold content the user should see immediately.</p>
</main>
<!-- Content below the fold does NOT need to be blocked on -->
<section>...</section>
</body>
Trường hợp sử dụng 2: Ảnh động biến đổi giữa các phần tử cụ thể
Khi các phần tử trên cả hai trang có chung một view-transition-name, trình duyệt sẽ biến đổi các phần tử đó một cách mượt mà trong suốt quá trình điều hướng. Nếu phần tử mục tiêu chưa được phân tích cú pháp khi quá trình chuyển đổi bắt đầu, thì trình duyệt sẽ không tìm thấy phần tử đó — hiệu ứng biến đổi sẽ giảm xuống thành các ảnh động vào và ra riêng biệt. Chặn việc hiển thị cho đến khi phần tử có view-transition-name được phân tích cú pháp.
Vì <link rel="expect"> chỉ tạm dừng hiển thị cho đến khi phần tử DOM mục tiêu được phân tích cú pháp và không đợi dữ liệu hình ảnh tải xuống hoặc giải mã, nên <img> chưa lưu vào bộ nhớ đệm có thể được ghi lại dưới dạng hình chữ nhật trống trong ảnh chụp nhanh trang mới. Để ngăn ảnh chụp nhanh trống hoặc bố cục bị dịch chuyển trong quá trình biến đổi hình ảnh, hãy tải trước hoặc giải mã trước các hình ảnh chuyển đổi quan trọng (ví dụ: bằng <link rel="preload" as="image"> hoặc bằng cách kết xuất trước trang đích) và cung cấp các phương án dự phòng về kích thước rõ ràng (thuộc tính width và height hoặc CSS aspect-ratio, cùng với màu nền của phần giữ chỗ).
<head>
<link rel="stylesheet" href="/css/styles.css">
<!--
DO: Preload critical transition images so their pixels are
available to decode before the new-page snapshot is captured.
<link rel="expect"> only waits for the DOM element to be parsed,
NOT for image pixels to download or decode.
-->
<link rel="preload" as="image" href="/img/product.webp" fetchpriority="high">
<!--
DO: Block rendering until the element participating in the
morph animation has been parsed. Without this, the browser
may start the transition before #hero exists, causing the
morph to degrade to a fade-out/fade-in.
-->
<link rel="expect" href="#hero" blocking="render">
<!--
When multiple blocking="render" resources are present,
rendering is blocked until ALL of them are satisfied.
Here, the browser waits for both the script to execute
AND the #hero element to be parsed — whichever comes last.
-->
<script async blocking="render" src="/js/transition-setup.js"></script>
</head>
<body>
<header>...</header>
<section id="hero">
<h1 style="view-transition-name: page-title">Product Name</h1>
<!--
DO: Provide explicit dimensions (width/height or aspect-ratio)
as a sizing fallback so the snapshot captures the correct layout
box even if image decoding is still in progress.
-->
<img
style="view-transition-name: hero-image"
src="/img/product.webp"
width="800"
height="600"
alt="Product"
>
</section>
</body>
Bước 4: Sử dụng truy vấn đa phương tiện để chặn hiển thị thích ứng
Kích thước khung nhìn khác nhau có thể hiển thị lượng nội dung khác nhau trong phần đầu trang. Sử dụng thuộc tính media trên <link rel="expect"> để chỉ chặn hiển thị nội dung hiển thị ở một chiều rộng khung nhìn nhất định.
<head>
<!--
DO: Use media queries to conditionally block rendering.
On wide screens, both the hero and the sidebar are visible,
so block until both are parsed. On narrow screens, only the
hero is visible initially.
-->
<link
rel="expect"
href="#hero"
blocking="render"
media="screen and (width <= 768px)"
>
<link
rel="expect"
href="#sidebar"
blocking="render"
media="screen and (width > 768px)"
>
</head>
Bước 5: Sử dụng pagereveal cho hiệu ứng chuyển đổi phụ thuộc vào ngữ cảnh (Không bắt buộc)
Sự kiện pagereveal không bắt buộc đối với chiến lược chặn hiển thị chính. Bạn chỉ cần dùng thuộc tính này khi phải chỉ định các giá trị view-transition-name một cách linh động dựa trên nơi người dùng đã chuyển đến, chẳng hạn như biến đổi một mục cụ thể trong danh sách thành tiêu đề trang thông tin chi tiết.
Nếu các giá trị view-transition-name được chỉ định tĩnh trong CSS hoặc nếu bạn chỉ sử dụng hiệu ứng mờ dần toàn trang mặc định, hãy bỏ qua hoàn toàn bước này.
<head>
<!--
MANDATORY: The pagereveal listener must be registered before
the page renders. Inline small setup scripts with blocking="render"
to avoid an extra network round-trip on cold loads, or use an
external async script with blocking="render" so the listener is
registered early without blocking HTML parsing.
If the listener is registered too late (e.g., in a deferred
script), the event may have already fired.
-->
<script async blocking="render" src="/js/transition-setup.js"></script>
</head>
// transition-setup.js
window.addEventListener('pagereveal', async (event) => {
if (!event.viewTransition) return;
const from = navigation.activation?.from;
if (!from) return;
const fromUrl = new URL(from.url);
// DO: Assign view-transition-name based on navigation context.
// This enables a morph animation from the product card on the
// list page to the heading on the detail page.
if (fromUrl.pathname === '/products/') {
const heading = document.querySelector('main h1');
if (!heading) return;
heading.style.viewTransitionName = 'product-title';
try {
await event.viewTransition.ready;
} catch (error) {
// Skipped view transitions reject `ready` with InvalidStateError.
if (error.name !== 'InvalidStateError') {
throw error;
}
} finally {
// MANDATORY: Remove the temporary name after the transition
// finishes. Stale names interfere with subsequent navigations
// and prevent the page from entering the bfcache.
await event.viewTransition.finished;
heading.style.viewTransitionName = '';
}
}
});
Các phương pháp hay nhất
- NÊN chỉ định
view-transition-namethông qua CSS bất cứ khi nào có thể. Dành riêng việc chỉ định JavaScript (thông quapagereveal) cho những trường hợp tên phụ thuộc vào ngữ cảnh điều hướng. - NÊN giữ cho tập lệnh chặn hiển thị nhỏ và nhanh. Trình duyệt có thời gian chờ tích hợp (khoảng 4 giây), sau đó quá trình chuyển đổi sẽ bị bỏ qua hoàn toàn (từ chối
viewTransition.readybằngInvalidStateErrornếu quá trình chuyển đổi được bắt đầu). - KHÔNG sử dụng
<link rel="expect">để chặn các phần tử nằm sâu trong trang mà không xuất hiện trong khung nhìn ban đầu. Việc này làm chậm quá trình chuyển đổi mà không mang lại lợi ích về mặt hình ảnh. - KHÔNG đánh dấu trình tải của bên thứ ba, trình tải của trình quản lý thẻ, mạng quảng cáo hoặc trình tải số liệu phân tích bằng
blocking="render", ngay cả khi các trình tải này chèn nội dung vào khung nhìn ban đầu. Độ trễ mạng của các tài nguyên này được tính vào thời gian chờ chuyển đổi và kết quả của các tài nguyên này không thuộc trạng thái ổn định của trang. - KHÔNG chỉ định cùng một
view-transition-namecho nhiều phần tử trên cùng một trang. Tên trùng lặp sẽ khiến toàn bộ quá trình chuyển đổi bị bỏ qua. - Tác động về thời gian của công nghệ hỗ trợ: Việc sử dụng
blocking="render"sẽ làm chậm quá trình cập nhật hình ảnh và lần hiển thị đầu tiên. Mặc dù điều này giúp ngăn chặn các lỗi hình ảnh cho người dùng có thị lực bình thường, nhưng có thể gây ra độ trễ xử lý hoặc trì hoãn việc khởi chạy cho trình đọc màn hình và các công nghệ hỗ trợ khác phụ thuộc vào cây hỗ trợ tiếp cận được kết xuất. Cân nhắc lợi ích của tính liên tục về hình ảnh so với độ trễ đọc ban đầu đối với người dùng không nhìn thấy, đồng thời đảm bảo các tập lệnh chặn hiển thị ở mức tối thiểu và được tối ưu hoá ở mức cao nhất.
Chiến lược dự phòng
Hiệu ứng chuyển đổi khung hiển thị trên nhiều tài liệu là một lựa chọn tuyệt vời để cải tiến tăng dần. Trong những trình duyệt không hỗ trợ, quy tắc @view-transition sẽ bị bỏ qua và hoạt động điều hướng cùng nguồn gốc tiêu chuẩn sẽ diễn ra giống hệt như khi không có tính năng này. Các trình duyệt hỗ trợ sẽ có hiệu ứng chuyển đổi mượt mà; tất cả các trình duyệt khác sẽ có hiệu ứng điều hướng chuẩn. Việc trình duyệt hỗ trợ hạn chế không phải là lý do để tránh áp dụng.
Tất cả trình duyệt hỗ trợ hiệu ứng chuyển đổi khung hiển thị giữa các tài liệu cũng hỗ trợ blocking="render" và <link rel="expect">, vì vậy, bạn không cần có phương án dự phòng riêng cho các tính năng chặn hiển thị được mô tả trong hướng dẫn này.
Những điều cần xem xét khác
- Tác động đến hiệu suất: Mọi tài nguyên chặn hiển thị đều làm chậm quá trình bắt đầu ảnh động chuyển đổi khung hiển thị. Giảm thiểu số lượng tập lệnh chặn hiển thị và chỉ sử dụng
<link rel="expect">cho các phần tử trong màn hình đầu tiên. Kết xuất trước các trang đích bằng Speculation Rules API để loại bỏ hoàn toàn độ trễ khi tải. - Hành vi hết thời gian chờ: Nếu tổng thời gian chặn hiển thị vượt quá khoảng 4 giây, trình duyệt sẽ bỏ qua quá trình chuyển đổi (và mọi lời hứa
viewTransition.readyđang hoạt động đều từ chối bằngInvalidStateError). Đảm bảo các tài nguyên quan trọng tải tốt trong khoảng thời gian này. - Khả năng tương thích với bfcache: Việc gán
view-transition-nametạm thời không được dọn dẹp sau khi chuyển đổi có thể ngăn trang truy cập vào bfcache. Luôn xoá tên được chỉ định linh động trong lệnh gọi lạifinished.