1. לפני שמתחילים
ב-Codelab הזה תלמדו איך להוסיף ניווט מהיר ומעברים חלקים בין דפים לאפליקציית אינטרנט לדוגמה, באמצעות ממשקי ה-API העדכניים שנתמכים באופן מובנה ב-Google Chrome.
אפליקציית האינטרנט לדוגמה בודקת את הערכים התזונתיים של פירות וירקות פופולריים. הדפים fruit-list ו-fruit-details בנויים כאפליקציה בדף יחיד (SPA), והדפים vegetable-list ו-vegetable-details בנויים כאפליקציה מסורתית מרובת דפים (MPA).

באופן ספציפי, אתם מטמיעים טרום-רינדור, מטמון לדף הקודם/הבא (bfcache) ושרת Proxy פרטי לאחזור מראש כדי לאפשר ניווט מיידי, ומעברים של רכיבי שורש/רכיבים משותפים כדי לאפשר מעברים חלקים בין דפים. אתם מטמיעים טרום-עיבוד ומטמון אחורי/קדימה בדפי MPA, ומעברים של רכיבים משותפים בדפי SPA.
מהירות האתר היא תמיד היבט חשוב של חוויית המשתמש, ולכן Google הציגה את Core Web Vitals, קבוצת מדדים שמודדים את ביצועי הטעינה, האינטראקטיביות והיציבות החזותית של דפי אינטרנט כדי להעריך את חוויית המשתמש בפועל. ממשקי ה-API העדכניים עוזרים לשפר את הציון של מדדי ה-Core Web Vitals של האתר בשטח, במיוחד את ביצועי הטעינה.

הדגמה מ- Mindvalley
בנוסף, המשתמשים רגילים לשימוש במעברים כדי להפוך את הניווט ואת שינויי המצב לאינטואיטיביים במיוחד באפליקציות מקוריות לנייד. לצערנו, שכפול חוויות משתמש כאלה באינטרנט הוא לא פשוט. אפשר להשיג אפקטים דומים באמצעות ממשקי API של פלטפורמת האינטרנט הקיימים, אבל יכול להיות שהפיתוח יהיה קשה או מורכב מדי, במיוחד בהשוואה לתכונות מקבילות באפליקציות ל-Android או ל-iOS. ממשקי API חלקים נועדו לגשר על הפער הזה בחוויית המשתמש והמפתחים בין האפליקציה לאתר.

דרישות מוקדמות
ידע ב:
- HTML
- CSS
- JavaScript
- הכלים למפתחים ב-Chrome
מה תלמדו:
איך מטמיעים:
- עיבוד מראש
- bfcache
- Private Prefetch Proxy
- מעברים של רכיבי בסיס או רכיבים משותפים
מה תפַתחו
אפליקציית אינטרנט לדוגמה שנבנתה באמצעות Next.js, עם היכולות העדכניות ביותר של הדפדפן, שמאפשרות מעבר חלק ומהיר בין האתרים:
- ניווט כמעט מיידי באמצעות עיבוד מראש
- מטמון לדף הקודם/הבא לטעינה מיידית באמצעות הלחצנים 'הקודם' ו'הבא' בדפדפן
- יצירת רושם ראשוני מצוין באמצעות ניווט בין מקורות שונים עם שרת proxy של אחזור מראש פרטי או חתימה על חילופי נתונים (SXG)
- מעבר חלק בין דפים עם מעבר של אלמנטים משותפים או אלמנטים ברמת הבסיס
הדרישות
- Chrome מגרסה 101 ואילך
2. שנתחיל?
הפעלה של התכונות הניסיוניות של Chrome
- עוברים אל about://flags ומפעילים את דגלי זמן הריצה
Prerender2ו-documentTransition API. - מפעילים מחדש את הדפדפן.
קבל את הקוד
- פותחים את הקוד ממאגר ה-GitHub הזה בסביבת הפיתוח המועדפת:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
- מתקינים את יחסי התלות שנדרשים להפעלת השרת:
npm install
- מפעילים את השרת ביציאה 3000:
npm run dev
- בדפדפן, עוברים אל http://localhost:3000.
עכשיו אפשר לערוך ולשפר את האפליקציה. בכל פעם שמבצעים שינויים, האפליקציה נטענת מחדש והשינויים מוצגים באופן ישיר.
3. שילוב של עיבוד מראש
לצורך ההדגמה הזו, זמן הטעינה של הדף vegetable-details באפליקציה לדוגמה איטי מאוד בגלל עיכוב שרירותי בצד השרת. בעזרת טרום-עיבוד אפשר לבטל את זמן ההמתנה הזה.
כדי להוסיף לחצני טרום-עיבוד לדף vegetable-list ולאפשר להם להפעיל עיבוד מראש אחרי שהמשתמש לוחץ:
- יוצרים רכיב של לחצן, שמוסיף באופן דינמי את תג הסקריפט speculation-rules:
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>
)
}
- מייבאים את הרכיב
PrerenderButtonבקובץlist-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>
)
}
- יוצרים רכיב כדי להוסיף את Speculation Rules API.
הרכיב SpeculationRules מוסיף באופן דינמי תג script לדף כשהאפליקציה מעדכנת את הסטטוס של 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])
}
- משלבים את הרכיבים באפליקציה.
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
- לוחצים על טרום-רינדור.
עכשיו אפשר לראות את השיפור המשמעותי בטעינה. בתרחיש השימוש האמיתי, טכנולוגיית הטרום-עיבוד מופעלת בדף שהמשתמש צפוי לבקר בו בהמשך, על סמך היוריסטיקה מסוימת.

Analytics
כברירת מחדל, הקובץ analytics.js באפליקציית האינטרנט לדוגמה שולח אירוע של צפייה בדף כשהאירוע DOMContentLoaded מתרחש. לצערנו, זו לא פעולה חכמה כי האירוע הזה מופעל במהלך שלב הטרום-רינדור.
כדי להוסיף אירוע document.prerendering ואירוע prerenderingchange ולפתור את הבעיה:
- לשכתב את הקובץ
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()
})
...
מעולה, שינית בהצלחה את הגדרות הניתוח כך שיתאימו לטרום-עיבוד. עכשיו אפשר לראות את יומני הצפיות בדף עם התזמון הנכון במסוף הדפדפן.
4. הסרת חסימות של bfcache
הסרת הגורם שמטפל באירוע unload
הוספת אירוע unload שלא לצורך היא טעות נפוצה מאוד שכבר לא מומלצת. היא לא רק מונעת את הפעולה של מטמון הדף הקודם, אלא גם לא אמינה. לדוגמה, הוא לא תמיד מופעל בנייד וב-Safari.
במקום אירוע unload, משתמשים באירוע pagehide, שמופעל בכל המקרים שבהם מופעל אירוע unload, וגם כשדף מוכנס למטמון bfcache.
כדי להסיר את הגורם שמטפל באירוע unload:
- בקובץ
analytics.js, מחליפים את הקוד של הגורם המטפל באירועunloadבקוד של הגורם המטפל באירועpagehide:
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')
})
עדכון הכותרת Cache-Control
דפים שמוצגים עם כותרת HTTP מסוג Cache-control: no-store לא נהנים מהתכונה bfcache של הדפדפן, ולכן מומלץ להשתמש בכותרת הזו במשורה. במיוחד, אם הדף לא מכיל מידע בהתאמה אישית או מידע קריטי, כמו מצב מחובר, סביר להניח שלא צריך להציג אותו עם כותרת ה-HTTP Cache-control: no-store.
כדי לעדכן את כותרת בקרת המטמון של אפליקציית הדוגמה:
- משנים את הקוד
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')
...
איך קובעים אם דף שוחזר מהמטמון לדף הקודם/הבא
האירוע pageshow מופעל מיד אחרי האירוע load כשהדף נטען בפעם הראשונה, ובכל פעם שהדף משוחזר מהמטמון לדף הקודם/הבא. לאירוע pageshow יש מאפיין persisted, שמקבל את הערך true אם הדף שוחזר מ-bfcache ואת הערך false אם הוא לא שוחזר ממנו. אפשר להשתמש במאפיין persisted כדי להבחין בין טעינות רגילות של דפים לבין שחזורים של bfcache. שירותי ניתוח נתונים מרכזיים אמורים להיות מודעים ל-bfcache, אבל אתם יכולים לבדוק אם הדף שוחזר מ-bfcache ולשלוח אירועים באופן ידני.
כדי לקבוע אם דף שוחזר מהמטמון לדף הקודם/הבא:
- מוסיפים את הקוד הזה לקובץ
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()
}
})
ניפוי באגים בדף אינטרנט
הכלים למפתחים ב-Chrome יכולים לעזור לכם לבדוק את הדפים כדי לוודא שהם מותאמים ל-bfcache ולזהות בעיות שעלולות לפסול אותם.
כדי לבדוק דף מסוים:
- עוברים לדף ב-Chrome.
- בכלים למפתחים ב-Chrome, לוחצים על Application > Back-forward Cache > Run Test (אפליקציה > מטמון של חזרה קדימה ואחורה > הפעלת בדיקה).
הכלים למפתחים ב-Chrome מנסים לנווט החוצה ואז לחזור כדי לקבוע אם אפשר לשחזר את הדף ממטמון bfcache.

אם הפעולה בוצעה ללא שגיאות, בחלונית תוצג ההודעה שהדף שוחזר מהמטמון לדף הקודם/הבא:

אם השחזור לא מצליח, בחלונית מופיעה הודעה שהדף לא שוחזר והסיבה לכך. אם הסיבה היא משהו שאתם יכולים לטפל בו כמפתחים, היא תופיע גם בחלונית.

5. הפעלת אחזור מראש חוצה-אתרים
הטכניקה הזו מתחילה להוריד מראש את הנתונים מוקדם יותר, כך שהבייטים כבר נמצאים בדפדפן כשהמשתמש עובר לדף אחר, וכך הניווט מהיר יותר. זו דרך קלה לשפר את Core Web Vitals ולצמצם חלק מפעילות ברשת לפני הניווט. הפעולה הזו מאיצה באופן ישיר את המהירות שבה נטען רכיב התוכן הכי גדול (LCP), ומשפרת את מהירות התגובה לאינטראקציה ראשונה (FID) ואת מדד יציבות חזותית (CLS) במהלך הניווט.
Private Prefetch Proxy מאפשר שליפה מראש של נתונים מאתרים שונים, אבל לא חושף מידע פרטי על המשתמש לשרת היעד.

הפעלה של אחזור מראש חוצה-אתרים באמצעות Private Prefetch Proxy
בעלי אתרים שומרים על השליטה באחזור מראש באמצעות משאב מוכר בשם traffic-advice, בדומה ל-/robots.txt לסורקי אינטרנט, שמאפשר לשרת HTTP להצהיר שסוכנים מיישמים צריכים לפעול בהתאם להמלצות המתאימות. בשלב הזה, בעלי אתרים יכולים להנחות את הסוכן לא לאפשר חיבורים לרשת או להגביל אותם. יכול להיות שבעתיד נוסיף עוד עצות.
כדי לארח משאב של מידע על תנועת הגולשים:
- מוסיפים את הקובץ הזה שדומה ל-JSON:
public/.well-known/traffic-advice
[
{
"user_agent": "prefetch-proxy",
"google_prefetch_proxy_eap": {
"fraction": 1
}
}
]
השדה google_prefetch_proxy_eap הוא שדה מיוחד לתוכנית הגישה המוקדמת, והשדה fraction הוא שדה לשליטה בחלק מבקשות הטעינה מראש שה-Private Prefetch Proxy שולח.
המלצות לגבי תעבורת נתונים צריכות להיות מוחזרות עם סוג ה-MIME application/trafficadvice+json.
- בתוך הקובץ
next.config.js, מגדירים את כותרת התגובה:
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. שילוב Shared Element Transitions API
כשמשתמש מנווט באינטרנט מדף אחד לדף אחר, התוכן שהוא רואה משתנה באופן פתאומי ובלתי צפוי כשהדף הראשון נעלם והדף החדש מופיע. חוויית המשתמש הזו, שכוללת רצף של פעולות לא קשורות, עלולה לבלבל את המשתמשים ולגרום להם להתאמץ יותר כדי להבין איך הם הגיעו לאן שהגיעו. בנוסף, הממשק הזה משפר את תפיסת המשתמשים לגבי טעינת הדף בזמן שהם ממתינים לטעינת היעד הרצוי.
אנימציות טעינה חלקות מפחיתות את העומס הקוגניטיבי כי המשתמשים נשארים בהקשר בזמן שהם עוברים בין הדפים, והן מפחיתות את זמן האחזור שנתפס של הטעינה כי המשתמשים רואים בינתיים משהו מעניין ומהנה. לכן, רוב הפלטפורמות מספקות פרימיטיבים קלים לשימוש שמאפשרים למפתחים ליצור מעברים חלקים, כמו Android, iOS, MacOS ו-Windows.
Shared Element Transitions API מספק למפתחים את אותה יכולת באינטרנט, בלי קשר לשאלה אם המעברים הם בין מסמכים (MPA) או בתוך מסמך (SPA).

כדי לשלב את Shared Element Transitions API בחלק ה-SPA של אפליקציית הדוגמה:
- יוצרים hook בהתאמה אישית כדי לנהל את המעבר בקובץ
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
}
- מבצעים קריאה ל-custom hook
usePageTransitionPrep()בדף הרשימה, ואז מבצעים קריאה לפונקציה האסינכרונית כדי להפעיל את methodtransition.start()בתוך אירועclick.
בתוך הפונקציה, רכיבי המחלקה shared-element נאספים ונרשמים כרכיבים משותפים.
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>
)
}
- בדף הפרטים, קוראים ל-
usePageTransition()hook כדי לסיים את פונקציית הקריאה החוזרתtransition.start().
ב-callback הזה, נרשמים גם רכיבים משותפים בדף הפרטים.
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>
)
...
}
עכשיו אפשר לראות שרכיבי התמונה משותפים בדפי הרשימה והפרטים, ומחוברים בצורה חלקה במעבר בין הדפים. אפשר אפילו להתאים אישית את האנימציה כדי להוסיף לה אלמנטים עיצוביים באמצעות פסאודו-אלמנטים של CSS.

7. מזל טוב
מעולה! יצרתם אפליקציית אינטרנט מיידית וחלקה עם חוויית משתמש אינטואיטיבית, מעוררת עניין ונוחה.
מידע נוסף
עיבוד מראש
bfcache
שליפה מראש באתרים שונים
פעולות החלפה חתומות
מעברים של רכיבי בסיס או רכיבים משותפים
ממשקי ה-API האלה עדיין בשלבי פיתוח מוקדמים, לכן נשמח לקבל משוב בכתובת crbug.com או כבעיות במאגר Github של ממשקי ה-API הרלוונטיים.