הוספה של ניווט מיידי ומעברים חלקים בין דפים לאפליקציית אינטרנט

1. לפני שמתחילים

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

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

צילום מסך של אפליקציה לנייד לדוגמה צילום מסך של אפליקציה לנייד לדוגמה

באופן ספציפי, אתם מטמיעים עיבוד מראש, מטמון לדף הקודם/הבא (bfcache) ושרת proxy לשליפה מראש פרטי בשביל הניווט המיידי, ומעברים של רכיב בסיס/משותף למעברים חלקים בין דפים. אתם מטמיעים את העיבוד מראש ואת המטמון לדף הקודם/הבא של דפי ה-MPA, ומעברים בין רכיבים משותפים בדפי ה-SPA.

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

תמונת הדגמה – איך המטמון לדף הקודם/הבא משפר את זמן הטעינה

הדגמה של Mindvalley

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

הדגמה של Shared Element Switchs API מ-pixiv הדגמה של Shared Element Transitions API מ-Tokopedia

הדגמות מ-pixiv ומ-Tokopedia

דרישות מוקדמות

הידע של:

מה תלמדו:

איך מטמיעים:

  • עיבוד מראש
  • bfcache
  • שרת Proxy פרטי לשליפה מראש (prefetch)
  • מעברים של רכיב בסיס/משותף

מה תפַתחו

דוגמה לאפליקציית אינטרנט שפותחה באמצעות Next.js, מוענקת ביכולות העדכניות ביותר של הדפדפן לשימוש מיידי ובצורה חלקה:

  • ניווט כמעט מיידי עם עיבוד מראש
  • המטמון לדף הקודם/הבא לטעינות מיידיות באמצעות הלחצנים של הדפדפן אחורה וקדימה
  • חשיפות ראשונות מעולות מניווט ממקורות שונים עם שרת proxy לשליפה מראש (prefetch) פרטי או החלפה חתומה (SXG)
  • מעבר חלק בין דפים עם מעבר של רכיב בסיס/משותף

למה תזדקק?

  • Chrome מגרסה 101 ואילך

2. שנתחיל?

הפעלת תכונות ניסיוניות ב-Chrome

  1. עוברים אל about://flags ומפעילים את הדגלים של סביבת זמן הריצה Prerender2 ו-documentTransition API.
  2. מפעילים מחדש את הדפדפן.

קבל את הקוד

  1. פותחים את הקוד ממאגר ה-GitHub הזה בסביבת הפיתוח המועדפת:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
  1. מתקינים את יחסי התלות הנדרשים להפעלת השרת:
npm install
  1. מפעילים את השרת ביציאה 3000:
npm run dev
  1. עוברים אל http://localhost:3000 בדפדפן.

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

3. שילוב עיבוד מראש

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

כדי להוסיף לחצנים לעיבוד מראש לדף רשימת הירקות, כך שהם יפעילו את העיבוד מראש אחרי שהמשתמש לוחץ:

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

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>
  )
}
  1. ייבוא של הרכיב 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>
  )
}
  1. יוצרים רכיב כדי להוסיף את Speculation Rules API.

הרכיב SpeculationRules מוסיף תג סקריפט באופן דינמי לדף כשהאפליקציה מעדכנת את המצב 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])
}
  1. משלבים את הרכיבים עם האפליקציה.

דפים/_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
  1. לוחצים על עיבוד מראש.

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

סרטון הדגמה של אפליקציה לעיבוד מראש

ניתוח נתונים

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

כדי ליצור אירוע 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 מופעל וכשדף נוסף למטמון לדף הקודם/הבא.

כדי להסיר את הגורם המטפל באירועים של 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')
})

עדכון הכותרת לבקרת המטמון

דפים שמוצגים עם כותרת HTTP של Cache-control: no-store לא מפיקים תועלת מהתכונה bfcache של הדפדפן, ולכן מומלץ להימנע מהכותרת הזו. באופן ספציפי, אם הדף לא מכיל מידע אישי או קריטי, למשל מצב התחברות, כנראה שאין צורך להציג אותו עם כותרת ה-HTTP Cache-control: no-store.

כדי לעדכן את כותרת בקרת המטמון של האפליקציה לדוגמה:

  • משנים את הקוד של getServerSideProps:

page/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')
  ...

page/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, והוא מתקיים אם הדף שוחזר מהמטמון לדף הקודם/הבא, ו-FALSE אם לא. אפשר להשתמש במאפיין persisted כדי להבדיל בין טעינות דפים רגילות לבין שחזורים של 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 יכולים לעזור לכם לבדוק את הדפים ולוודא שהם מותאמים לשמירה במטמון לדף הקודם/הבא, ולזהות בעיות שעלולות לגרום לכך שהם לא יעמדו בדרישות.

כדי לבדוק דף מסוים:

  1. עוברים לדף ב-Chrome.
  2. בכלים למפתחים ב-Chrome, לוחצים על Application > מטמון לדף הקודם/הבא > הרצת הבדיקה.

הכלים למפתחים ב-Chrome מנסים לנווט החוצה ואז לחזור כדי לבדוק אם ניתן לשחזר את הדף מהמטמון לדף הקודם/הבא.

49bf965af35d5324.png

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

47015a0de45f0b0f.png

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

dcf0312c3fc378ce.png

5. הפעלת שליפה מראש (prefetch) מאתרים שונים

שליפה מראש (prefetch) מתחילה אחזור מוקדם כך שהבייטים כבר נמצאים בדפדפן כשהמשתמש מנווט, מה שלזרז את הניווט. זו דרך קלה לשפר את מדדי הליבה לבדיקת חוויית המשתמש באתר ולהיסט חלק מהפעילות ברשת לפני הניווט. כך ניתן להאיץ ישירות את המהירות שבה נטען רכיב התוכן הכי גדול (LCP), וכך יש יותר מקום לעיכוב בקלט הראשון (FID) ולשינוי הפריסה המצטברת (CLS) בזמן הניווט.

שרת proxy לשליפה מראש (prefetch) מאפשר שליפה מראש מאתרים שונים, אבל לא חושף מידע פרטי על המשתמש לשרת היעד.

איך פועל שרת proxy לשליפה מראש (prefetch) של פרטי

הפעלת שליפה מראש (prefetch) מאתרים שונים באמצעות שרת proxy לשליפה מראש פרטיים

לבעלי האתרים יש שליטה על השליפה מראש (prefetch) דרך משאב ידוע של ייעוץ בתנועה, בדומה ל-/robots.txt בשביל סורקי אינטרנט, שמאפשר לשרת HTTP להצהיר שהסוכנים המטמיעים צריכים ליישם את העצה המתאימה. בשלב הזה, בעלי אתרים יכולים לייעץ לסוכן לאסור חיבורי רשת או לווסת אותם. יכול להיות שנוסיף עוד עצות בעתיד.

כדי לארח משאב של ייעוץ תנועה:

  1. מוסיפים את הקובץ דמוי JSON:

public/.well-known/traffic-advisor

[
  {
    "user_agent": "prefetch-proxy",
    "google_prefetch_proxy_eap": {
      "fraction": 1
    }
  }
]

השדה google_prefetch_proxy_eap הוא שדה מיוחד לתוכנית הגישה המוקדמת, והשדה fraction הוא שדה שעוזר לקבוע את אחוז השליפות מראש המבוקשות ששרת Proxy לשליפה מראש (פרטי) שולח.

העצות בנושא תנועה צריכות להיות מוחזרות עם סוג MIME של application/trafficadvice+json.

  1. בקובץ 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 Switchs API מ-pixiv הדגמה של Shared Element Transitions API מ-Tokopedia

הדגמות מ-pixiv ומ-Tokopedia

כדי לשלב את Shared Element Transitions API עבור החלק של ה-SPA באפליקציה לדוגמה:

  1. יצירת הוק (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
}
  1. קוראים להוק (hook) מותאם אישית usePageTransitionPrep() בדף הרשימה ולאחר מכן קוראים לפונקציה האסינכרונית כדי להפעיל את ה-method transition.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>
  )
}
  1. בדף הפרטים, צריך להפעיל את ההוק (hook) usePageTransition() כדי לסיים את פונקציית הקריאה החוזרת transition.start().

בקריאה החוזרת הזו, גם רכיבים משותפים בדף הפרטים רשומים.

page/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

שליפה מראש (prefetch) בין אתרים

בורסות שנחתמו

מעברים של רכיב בסיס/משותף

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