1. Trước khi bắt đầu
Lớp học lập trình này hướng dẫn bạn cách thêm tính năng điều hướng tức thì và chuyển đổi trang liền mạch vào một ứng dụng web mẫu bằng các API mới nhất mà Google Chrome hỗ trợ nguyên bản.
Ứng dụng web mẫu này kiểm tra giá trị dinh dưỡng của các loại trái cây và rau quả phổ biến. Các trang fruit-list và fruit-details được tạo dưới dạng một ứng dụng một trang (SPA), còn các trang vegetable-list và vegetable-details được tạo dưới dạng một ứng dụng truyền thống có nhiều trang (MPA).

Cụ thể, bạn triển khai tính năng kết xuất trước, bộ nhớ đệm cho thao tác tiến/lùi (bfcache) và Private Prefetch Proxy để điều hướng tức thì, cũng như hiệu ứng chuyển đổi thành phần gốc/dùng chung để chuyển đổi trang liền mạch. Bạn triển khai tính năng kết xuất trước và bộ nhớ đệm cho thao tác tiến/lùi cho các trang MPA, cũng như hiệu ứng chuyển đổi phần tử được chia sẻ cho các trang ứng dụng trang đơn (SPA).
Tốc độ trang web luôn là một khía cạnh quan trọng của trải nghiệm người dùng. Đó là lý do Google giới thiệu Core Web Vitals, một nhóm chỉ số đo lường hiệu suất tải, khả năng tương tác và độ ổn định của hình ảnh trên các trang web để đánh giá trải nghiệm thực tế của người dùng. Các API mới nhất giúp bạn cải thiện điểm số Core Web Vitals của trang web trong thực tế, đặc biệt là về hiệu suất tải.

Bản minh hoạ của Mindvalley
Người dùng cũng quen với việc sử dụng hiệu ứng chuyển cảnh để giúp các thao tác điều hướng và thay đổi trạng thái trở nên cực kỳ trực quan trong các ứng dụng gốc dành cho thiết bị di động. Rất tiếc, việc sao chép những trải nghiệm người dùng như vậy không hề đơn giản trên web. Mặc dù có thể đạt được các hiệu ứng tương tự bằng các API nền tảng web hiện tại, nhưng quá trình phát triển có thể quá khó hoặc phức tạp, đặc biệt là khi so sánh với các tính năng tương đương trong ứng dụng Android hoặc iOS. Các API liền mạch được thiết kế để lấp đầy khoảng trống về trải nghiệm người dùng và nhà phát triển giữa ứng dụng và web.

Bản minh hoạ từ pixiv và Tokopedia
Điều kiện tiên quyết
Có kiến thức về:
- HTML
- CSS
- JavaScript
- Công cụ cho nhà phát triển Chrome
Kiến thức bạn sẽ học được:
Cách triển khai:
- Kết xuất trước
- bfcache
- Private Prefetch Proxy
- Hiệu ứng chuyển đổi phần tử gốc/dùng chung
Sản phẩm bạn sẽ tạo ra
Một ứng dụng web mẫu được tạo bằng Next.js, có thêm các tính năng mới nhất của trình duyệt, hoạt động tức thì và liền mạch:
- Điều hướng gần như tức thì bằng tính năng kết xuất trước
- bfcache để tải tức thì bằng các nút quay lại và chuyển tiếp của trình duyệt
- Tạo ấn tượng đầu tiên tuyệt vời từ hoạt động điều hướng trên nhiều nguồn gốc bằng Private Prefetch Proxy hoặc trao đổi có chữ ký (SXG)
- Hiệu ứng chuyển đổi liền mạch giữa các trang có hiệu ứng chuyển đổi phần tử gốc/được chia sẻ
Bạn cần có
- Chrome phiên bản 101 trở lên
2. Bắt đầu
Bật cờ Chrome
- Chuyển đến about://flags, rồi bật cờ thời gian chạy
Prerender2vàdocumentTransition API. - Khởi động lại trình duyệt.
Lấy mã
- Mở mã từ kho lưu trữ GitHub này trong môi trường phát triển mà bạn yêu thích:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
- Cài đặt các phần phụ thuộc cần thiết để chạy máy chủ:
npm install
- Khởi động máy chủ trên cổng 3000:
npm run dev
- Chuyển đến http://localhost:3000 trong trình duyệt.
Giờ đây, bạn có thể chỉnh sửa và cải thiện ứng dụng của mình. Bất cứ khi nào bạn thực hiện thay đổi, ứng dụng sẽ tải lại và các thay đổi của bạn sẽ xuất hiện ngay lập tức.
3. Tích hợp tính năng kết xuất trước
Để minh hoạ, thời gian tải trang vegetable-details trong ứng dụng mẫu rất chậm do có độ trễ tuỳ ý ở phía máy chủ. Bạn có thể loại bỏ thời gian chờ này bằng cách kết xuất trước.
Để thêm các nút kết xuất trước vào trang vegetable-list và cho phép các nút này kích hoạt tính năng kết xuất trước sau khi người dùng nhấp vào:
- Tạo một thành phần nút, thành phần này sẽ chèn thẻ tập lệnh speculation-rules một cách linh động:
components/prerender-button.js
import { useContext } from 'react'
import ResourceContext from './resource-context'
// You use resource context to manage global states.
// In the PrerenderButton component, you update the prerenderURL parameter when the button is clicked.
export default function PrerenderButton() {
const { dispatch } = useContext(ResourceContext)
const handleClick = (e) => {
e.preventDefault()
e.stopPropagation()
const parent = e.target.closest('a')
if (!parent) {
return
}
const href = parent.getAttribute('href')
dispatch({ type: 'update', prerenderURL: href })
}
return (
<button className='ml-auto bg-gray-200 hover:bg-gray-300 px-4 rounded' onClick={handleClick}>
Prerender
</button>
)
}
- Nhập thành phần
PrerenderButtontrong tệplist-item.js.
components/list-item.js
// Codelab: Add a PrerenderButton component.
import PrerenderButton from './prerender-button'
...
function ListItemForMPA({ item, href }) {
return (
<a href={href} className='block flex items-center'>
<Icon src={item.image} />
<div className='text-xl'>{item.name}</div>
{/* Codelab: Add PrerenderButton component. */}
<PrerenderButton />
</a>
)
}
- Tạo một thành phần để thêm Speculation Rules API.
Thành phần SpeculationRules sẽ tự động chèn một thẻ tập lệnh vào trang khi ứng dụng cập nhật trạng thái prerenderURL.
components/speculationrules.js
import Script from 'next/script'
import { useContext, useMemo } from 'react'
import ResourceContext from './resource-context'
export default function SpeculationRules() {
const { state } = useContext(ResourceContext)
const { prerenderURL } = state
return useMemo(() => {
return (
<>
{prerenderURL && (
<Script id='speculationrules' type='speculationrules'>
{`
{
"prerender":[
{
"source": "list",
"urls": ["${prerenderURL}"]
}
]
}
`}
</Script>
)}
</>
)
}, [prerenderURL])
}
- Tích hợp các thành phần với ứng dụng.
pages/_app.js
// Codelab: Add the SpeculationRules component.
import SpeculationRules from '../components/speculationrules'
function MyApp({ Component, pageProps }) {
useAnalyticsForSPA()
return (
<ResourceContextProvider>
<Layout>
<Component {...pageProps} />
</Layout>
{/* Codelab: Add SpeculationRules component */}
<SpeculationRules />
<Script id='analytics-for-mpa' strategy='beforeInteractive' src='/analytics.js' />
</ResourceContextProvider>
)
}
export default MyApp
- Nhấp vào Kết xuất trước.
Giờ đây, bạn có thể thấy mức độ cải thiện đáng kể về tốc độ tải. Trong trường hợp sử dụng thực tế, quá trình kết xuất trước được kích hoạt cho trang mà người dùng có khả năng truy cập tiếp theo bằng một số phương pháp phỏng đoán.

Số liệu phân tích
Theo mặc định, tệp analytics.js trong ứng dụng web mẫu sẽ gửi một sự kiện page_view khi sự kiện DOMContentLoaded xảy ra. Rất tiếc, điều này không nên vì sự kiện này sẽ kích hoạt trong giai đoạn kết xuất trước.
Cách giới thiệu sự kiện document.prerendering và prerenderingchange để khắc phục vấn đề này:
- Viết lại tệp
analytics.js:
public/analytics.js
const sendEvent = (type = 'pageview') => {
// Codelab: Make analytics prerendering compatible.
// The pageshow event could happen in the prerendered page before activation.
// The prerendered page should be handled by the prerenderingchange event.
if (document.prerendering) {
return
}
console.log(`Send ${type} event for MPA navigation.`)
fetch(`/api/analytics?from=${encodeURIComponent(location.pathname)}&type=${type}`)
}
...
// Codelab: Make analytics prerendering compatible.
// The prerenderingchange event is triggered when the page is activated.
document.addEventListener('prerenderingchange', () => {
console.log('The prerendered page was activated.')
sendEvent()
})
...
Tuyệt vời! Bạn đã sửa đổi thành công số liệu phân tích để chúng tương thích với tính năng kết xuất trước. Giờ đây, bạn có thể thấy nhật ký lượt xem trang với thời gian chính xác trong bảng điều khiển trình duyệt.
4. Xoá các thành phần chặn bfcache
Xoá trình xử lý sự kiện unload
Việc có một sự kiện unload không cần thiết là một sai lầm rất phổ biến mà bạn không nên mắc phải nữa. Không chỉ ngăn bfcache hoạt động mà cách này còn không đáng tin cậy. Ví dụ: không phải lúc nào sự kiện này cũng kích hoạt trên thiết bị di động và Safari.
Thay vì sự kiện unload, bạn sẽ sử dụng sự kiện pagehide. Sự kiện này sẽ kích hoạt trong mọi trường hợp khi sự kiện unload kích hoạt và khi một trang được đưa vào bfcache.
Cách xoá trình xử lý sự kiện unload:
- Trong tệp
analytics.js, hãy thay thế mã cho trình xử lý sự kiệnunloadbằng mã cho trình xử lý sự kiệnpagehide:
public/analytics.js
// Codelab: Remove the unload event handler for bfcache.
// The unload event handler prevents the content from being stored in bfcache. Use the pagehide event instead.
window.addEventListener('pagehide', () => {
sendEvent('leave')
})
Cập nhật tiêu đề cache-control
Những trang được phân phát bằng tiêu đề HTTP Cache-control: no-store không được hưởng lợi từ tính năng bfcache của trình duyệt, vì vậy, bạn nên sử dụng tiêu đề này một cách tiết kiệm. Cụ thể, nếu trang không chứa thông tin được cá nhân hoá hoặc thông tin quan trọng (chẳng hạn như trạng thái đã đăng nhập), thì có thể bạn không cần phân phát trang đó bằng tiêu đề HTTP Cache-control: no-store.
Cách cập nhật tiêu đề cache-control của ứng dụng mẫu:
- Sửa đổi mã
getServerSideProps:
pages/vegetables/index.js
export const getServerSideProps = middleware(async (ctx) => {
const { req, res } = ctx
// Codelab: Modify the cache-control header.
res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59')
...
pages/vegetables/[name].js
export const getServerSideProps = middleware(async (ctx) => {
const { req, res, query } = ctx
// Codelab: Modify the cache-control header.
res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59')
...
Xác định xem một trang có được khôi phục từ bfcache hay không
Sự kiện pageshow sẽ kích hoạt ngay sau sự kiện load khi trang tải lần đầu và bất cứ khi nào trang được khôi phục từ bộ nhớ đệm chuyển tiếp/lùi. Sự kiện pageshow có một thuộc tính persisted. Thuộc tính này có giá trị true nếu trang được khôi phục từ bộ nhớ đệm chuyển tiếp và lùi lại (bfcache) và có giá trị false nếu không được khôi phục. Bạn có thể dùng thuộc tính persisted để phân biệt các lượt tải trang thông thường với các lượt khôi phục bfcache. Các dịch vụ phân tích chính cần biết về bfcache, nhưng bạn có thể kiểm tra xem trang có được khôi phục từ bfcache hay không và gửi sự kiện theo cách thủ công.
Cách xác định xem một trang có được khôi phục từ bfcache hay không:
- Thêm mã này vào tệp
analytics.js.
public/analytics.js
// Codelab: Use the pageshow event handler for bfcache.
window.addEventListener('pageshow', (e) => {
// If the persisted flag exists, the page was restored from bfcache.
if (e.persisted) {
console.log('The page was restored from bfcache.')
sendEvent()
}
})
Gỡ lỗi trang web
Công cụ cho nhà phát triển Chrome có thể giúp bạn kiểm thử các trang để đảm bảo rằng các trang đó được tối ưu hoá cho bộ nhớ đệm chuyển tiếp và quay lại (bfcache), đồng thời xác định mọi vấn đề có thể khiến các trang đó không đủ điều kiện.
Cách kiểm tra một trang cụ thể:
- Chuyển đến trang đó trong Chrome.
- Trong Công cụ cho nhà phát triển Chrome, hãy nhấp vào Ứng dụng > Bộ nhớ đệm đi và về > Chạy kiểm thử.
Công cụ cho nhà phát triển Chrome cố gắng chuyển hướng rồi quay lại để xác định xem trang có thể được khôi phục từ bộ nhớ đệm chuyển tiếp/quay lại hay không.

Nếu thành công, bảng điều khiển sẽ cho bạn biết rằng trang đã được khôi phục từ bộ nhớ đệm cho thao tác tiến/lùi:

Nếu không thành công, bảng điều khiển sẽ cho bạn biết rằng trang đó không được khôi phục và lý do. Nếu lý do là điều mà bạn có thể giải quyết với tư cách là nhà phát triển, thì bảng điều khiển cũng sẽ cho bạn biết.

5. Bật tính năng tìm nạp trước trên nhiều trang web
Hoạt động tìm nạp trước bắt đầu tìm nạp sớm để các byte đã có sẵn trong trình duyệt khi người dùng di chuyển, nhờ đó tăng tốc quá trình di chuyển. Đây là một cách dễ dàng để cải thiện Core Web Vitals và bù đắp một số hoạt động mạng trước khi điều hướng. Điều này giúp tăng tốc trực tiếp Nội dung lớn nhất hiển thị (LCP) và có thêm không gian cho Độ trễ đầu vào đầu tiên (FID) và Mức thay đổi bố cục tích luỹ (CLS) khi điều hướng.
Private Prefetch Proxy cho phép tìm nạp trước trên nhiều trang web nhưng không tiết lộ thông tin riêng tư về người dùng cho máy chủ đích.

Bật tính năng tìm nạp trước trên nhiều trang web bằng Private Prefetch Proxy
Chủ sở hữu trang web vẫn có quyền kiểm soát việc tìm nạp trước thông qua một tài nguyên traffic-advice (lời khuyên về lưu lượng truy cập) nổi tiếng, tương tự như /robots.txt đối với trình thu thập dữ liệu web. Tài nguyên này cho phép một máy chủ HTTP khai báo rằng các tác nhân triển khai nên áp dụng lời khuyên tương ứng. Hiện tại, chủ sở hữu trang web có thể khuyên tác nhân không cho phép hoặc điều tiết các kết nối mạng. Trong tương lai, chúng tôi có thể sẽ bổ sung thêm các lời khuyên khác.
Để lưu trữ tài nguyên tư vấn về giao thông:
- Thêm tệp tương tự như JSON này:
public/.well-known/traffic-advice
[
{
"user_agent": "prefetch-proxy",
"google_prefetch_proxy_eap": {
"fraction": 1
}
}
]
Trường google_prefetch_proxy_eap là một trường đặc biệt dành cho chương trình tiếp cận sớm và trường fraction là một trường để kiểm soát tỷ lệ phần trăm số lần tìm nạp trước được yêu cầu mà Private Prefetch Proxy gửi.
Thông tin tư vấn về giao thông phải được trả về bằng loại MIME application/trafficadvice+json.
- Trong tệp
next.config.js, hãy định cấu hình tiêu đề phản hồi:
next.config.js
const nextConfig = {
// Codelab: Modify content-type for traffic advice file.
async headers() {
return [
{
source: '/.well-known/traffic-advice',
headers: [
{
key: 'Content-Type',
value: 'application/trafficadvice+json',
},
],
},
]
},
}
module.exports = nextConfig
6. Tích hợp Shared Element Transitions API
Khi người dùng di chuyển trên web từ trang này sang trang khác, nội dung họ thấy sẽ thay đổi đột ngột và bất ngờ khi trang đầu tiên biến mất và trang mới xuất hiện. Trải nghiệm người dùng rời rạc, theo trình tự này khiến người dùng bị mất phương hướng và dẫn đến tải nhận thức cao hơn vì người dùng buộc phải ghép nối cách họ đến được vị trí hiện tại. Ngoài ra, trải nghiệm này còn giúp người dùng cảm nhận được mức độ tải trang trong khi họ chờ tải đích đến mong muốn.
Ảnh động tải mượt mà giúp giảm tải nhận thức vì người dùng vẫn nắm được bối cảnh trong khi di chuyển giữa các trang, đồng thời giảm độ trễ cảm nhận của quá trình tải vì người dùng thấy nội dung hấp dẫn và thú vị trong thời gian chờ. Vì những lý do này, hầu hết các nền tảng đều cung cấp các thành phần cơ bản dễ sử dụng, cho phép nhà phát triển tạo ra các hiệu ứng chuyển đổi liền mạch, chẳng hạn như Android, iOS, MacOS và Windows.
Shared Element Transitions API (API hiệu ứng chuyển đổi phần tử được chia sẻ) cung cấp cho nhà phát triển khả năng tương tự trên web, bất kể hiệu ứng chuyển đổi là trên nhiều tài liệu (MPA) hay trong một tài liệu (SPA).

Bản minh hoạ từ pixiv và Tokopedia
Cách tích hợp Shared Element Transitions API cho phần SPA của ứng dụng mẫu:
- Tạo một hook tuỳ chỉnh để quản lý hiệu ứng chuyển đổi trong tệp
use-page-transition.js:
utils/use-page-transition.js
import { useEffect, useContext, useRef, useCallback } from 'react'
import ResourceContext from '../components/resource-context'
// Call this hook on this first page before you start the page transition. For Shared Element Transitions, you need to call the transition.start() method before the next page begins to render, and you need to do the Document Object Model (DOM) modification or setting of new shared elements inside the callback so that this hook returns the promise and defers to the callback resolve.
export const usePageTransitionPrep = () => {
const { dispatch } = useContext(ResourceContext)
return (elm) => {
const sharedElements = elm.querySelectorAll('.shared-element')
// Feature detection
if (!document.createDocumentTransition) {
return null
}
return new Promise((resolve) => {
const transition = document.createDocumentTransition()
Array.from(sharedElements).forEach((elm, idx) => {
transition.setElement(elm, `target-${idx}`)
})
transition.start(async () => {
resolve()
await new Promise((resolver) => {
dispatch({ type: 'update', transition: { transition, resolver } })
})
})
})
}
}
// Call this hook on the second page. Inside the useEffect hook, you can refer to the actual DOM element and set them as shared elements with the transition.setElement() method. When the resolver function is called, the transition is initiated between the captured images and newly set shared elements.
export const usePageTransition = () => {
const { state, dispatch } = useContext(ResourceContext)
const ref = useRef(null)
const setRef = useCallback((node) => {
ref.current = node
}, [])
useEffect(() => {
if (!state.transition || !ref.current) {
return
}
const { transition, resolver } = state.transition
const sharedElements = ref.current.querySelectorAll('.shared-element')
Array.from(sharedElements).forEach((elm, idx) => {
transition.setElement(elm, `target-${idx}`)
})
resolver()
return () => {
dispatch({ type: 'update', transition: null })
}
})
return setRef
}
- Gọi hook tuỳ chỉnh
usePageTransitionPrep()trong trang danh sách, sau đó gọi hàm không đồng bộ để kích hoạt phương thứctransition.start()bên trong sự kiệnclick.
Bên trong hàm, các phần tử lớp shared-element được thu thập và đăng ký làm phần tử dùng chung.
components/list-item.js
// Codelab: Add the Shared Element Transitions API.
import { usePageTransitionPrep } from '../utils/use-page-transition'
...
function ListItemForSPA({ item, href }) {
// Codelab: Add Shared Element Transitions.
const transitionNextState = usePageTransitionPrep()
const handleClick = async (e) => {
const elm = e.target.closest('a')
await transitionNextState(elm)
}
return (
<Link href={href}>
<a className='block flex items-center' onClick={handleClick}>
<Icon src={item.image} name={item.name} className='shared-element' />
<div className='text-xl'>{item.name}</div>
</a>
</Link>
)
}
- Trên trang thông tin chi tiết, hãy gọi hook
usePageTransition()để hoàn tất hàm callbacktransition.start().
Trong lệnh gọi lại này, các phần tử dùng chung trong trang chi tiết cũng được đăng ký.
pages/fruits/[name].js
// Codelab: Add the Shared Element Transitions API.
import { usePageTransition } from '../../utils/use-page-transition'
const Item = ({ data }) => {
const { name, image, amountPer, nutrition } = data
// Codelab: Add the Shared Element Transitions API.
const ref = usePageTransition()
return (
<div className={'flex flex-col items-center justify-center py-4 px-4 sm:flex-row'} ref={ref}>
<div className='flex flex-col items-center sm:w-2/4'>
<Image
className='object-cover border-gray-100 border-2 rounded-full shared-element'
src={image}
width='240'
height='240'
alt={`picture of ${name}`}
/>
<h1 className='text-4xl font-bold mt-4'>{name}</h1>
</div>
<div className='sm:w-2/4 w-full'>
<Nutrition amountPer={amountPer} nutrition={nutrition} />
</div>
</div>
)
...
}
Giờ đây, bạn có thể thấy các phần tử hình ảnh được chia sẻ trên trang danh sách và trang thông tin chi tiết, đồng thời được kết nối liền mạch trong quá trình chuyển đổi trang. Bạn thậm chí có thể tuỳ chỉnh ảnh động để làm cho ảnh động trở nên bắt mắt hơn bằng các phần tử giả CSS.

7. Xin chúc mừng
Xin chúc mừng! Bạn đã tạo một ứng dụng web tức thì và liền mạch với trải nghiệm người dùng hấp dẫn, trực quan và có ít rào cản.
Tìm hiểu thêm
Kết xuất trước
- Kết xuất trước, được cải tiến
- Đưa tính năng tải trang tức thì vào trình duyệt thông qua tính năng kết xuất trước theo suy đoán
- quicklink
bfcache
Tìm nạp trước trên nhiều trang web
Trao đổi có chữ ký
Hiệu ứng chuyển đổi phần tử gốc/dùng chung
- Chuyển tiếp phần tử dùng chung
- Chuyển đổi trang mượt mà và đơn giản bằng Shared Element Transitions API
Các API này vẫn đang trong giai đoạn phát triển ban đầu, vì vậy, vui lòng chia sẻ ý kiến phản hồi của bạn tại crbug.com hoặc dưới dạng các vấn đề trong kho lưu trữ Github của các API có liên quan.