Dodaj błyskawiczną nawigację i płynne przejścia między stronami do aplikacji internetowej

1. Zanim zaczniesz

Dzięki tym ćwiczeniom w Codelabs dowiesz się, jak dodać do przykładowej aplikacji internetowej szybką nawigację i płynne przejścia między stronami za pomocą najnowszych interfejsów API obsługiwanych natywnie przez Google Chrome.

Przykładowa aplikacja internetowa sprawdza wartości odżywcze popularnych owoców i warzyw. Strony z listą owoców i szczegółami owoców mają postać aplikacji jednostronicowej (SPA), a strony z listą warzyw i szczegółów warzyw mają postać tradycyjnej wielostronicowej aplikacji (MPA).

Zrzut ekranu przykładowej aplikacji na urządzeniu mobilnym Zrzut ekranu przykładowej aplikacji na urządzeniu mobilnym

Przede wszystkim wdrożysz renderowanie wstępne, pamięć podręczną stanu strony internetowej i prywatny serwer proxy pobierania z wyprzedzeniem na potrzeby natychmiastowej nawigacji oraz przejścia elementów głównych i udostępnionych w celu płynnego przejścia między stronami. W przypadku stron MPA wdrożysz renderowanie wstępne i pamięci podręcznej stanu strony internetowej oraz przejścia elementów udostępnionych na stronach SPA.

Szybkość witryny jest zawsze ważnym czynnikiem wpływającym na wygodę użytkowników, dlatego wprowadziliśmy podstawowe wskaźniki internetowe, czyli zestaw danych mierzących wydajność wczytywania, interaktywność i stabilność wizualną stron internetowych. Dzięki temu możemy ocenić rzeczywiste wrażenia użytkowników. Najnowsze interfejsy API pomagają poprawić wynik podstawowych wskaźników internetowych witryny, zwłaszcza jeśli chodzi o jej wczytywanie.

obraz demonstracyjny poprawiający czas wczytywania strony bfcache

Demonstracja od Mindvalley

Użytkownicy są też przyzwyczajeni do stosowania przejść, dzięki którym nawigacja i zmiany stanów są niezwykle intuicyjne w natywnych aplikacjach mobilnych. Niestety powielanie takich działań w internecie nie jest łatwe. Dotychczasowe interfejsy API platformy internetowej mogą dać podobne efekty, jednak programowanie może być zbyt trudne lub złożone, zwłaszcza w porównaniu z podobnymi funkcjami w aplikacjach na Androida lub iOS. Sprawne interfejsy API zostały zaprojektowane tak, aby wypełnić lukę między aplikacjami a stroną internetową.

Prezentacja interfejsu Shared Element Migrates API z Pixiv Prezentacja interfejsu Shared Element Migrates API z Tokopedii

Demonstracje z Pixiv i Tokopedii

Wymagania wstępne

Wiedza z zakresu:

Czego się nauczysz:

Jak to zrobić:

  • Renderowanie wstępne
  • bfcache
  • Prywatny serwer proxy pobierania z wyprzedzeniem
  • Przejścia elementów głównych/udostępnionych

Co utworzysz

Przykładowa aplikacja internetowa stworzona w oparciu o Next.js, wzbogacona o najnowsze, natychmiastowe i bezproblemowe funkcje przeglądarki:

  • Szybka nawigacja z renderowaniem wstępnym
  • bfcache umożliwia natychmiastowe ładowanie za pomocą przycisków przeglądarki do tyłu i do przodu.
  • Świetne pierwsze wyświetlenia dzięki nawigacji między domenami z prywatnym serwerem proxy pobierania z wyprzedzeniem lub Signed Exchange (SXG)
  • Płynne przejście między stronami z przejściem między stronami z elementami głównymi i udostępnionymi

Czego potrzebujesz

  • Chrome w wersji 101 lub nowszej,

2. Rozpocznij

Włącz flagi Chrome

  1. Otwórz stronę about://flags i włącz flagi środowiska wykonawczego Prerender2 i documentTransition API.
  2. Ponownie uruchom przeglądarkę.

Pobierz kod

  1. Otwórz kod z tego repozytorium GitHub w ulubionym środowisku programistycznym:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
  1. Zainstaluj zależności wymagane do uruchomienia serwera:
npm install
  1. Uruchom serwer na porcie 3000:
npm run dev
  1. Otwórz w przeglądarce stronę http://localhost:3000.

Teraz możesz edytować i ulepszać swoją aplikację. Po każdym wprowadzeniu zmian aplikacja wczytuje się ponownie, a zmiany są od razu widoczne.

3. Zintegruj renderowanie wstępne

Na potrzeby tej wersji demonstracyjnej strona z informacjami o warzywach w przykładowej aplikacji wczytuje się bardzo wolno ze względu na arbitralne opóźnienie po stronie serwera. Dzięki renderowaniu wstępnemu eliminujesz ten czas oczekiwania.

Aby dodać przyciski wstępnego renderowania do strony z listą warzyw i umożliwić im uruchomienie wstępnego renderowania po kliknięciu przez użytkownika:

  1. Utwórz komponent przycisku, który dynamicznie wstawia tag skryptu reguł spekulacyjnych:

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. Zaimportuj komponent PrerenderButton do pliku 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. Utwórz komponent, aby dodać interfejs Speculation Rules API.

Komponent SpeculationRules dynamicznie wstawia tag skryptu na stronie, gdy aplikacja aktualizuje stan 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. Zintegruj komponenty z aplikacją

strony/_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. Kliknij Renderowanie wstępne.

Teraz widać znaczącą poprawę w zakresie wczytywania. W rzeczywistym przypadku renderowanie wstępne jest wywoływane na stronie, którą prawdopodobnie odwiedzi użytkownik dzięki danym heurystycznym.

Przykładowa wersja demonstracyjna aplikacji do wstępnego renderowania

Analytics

Domyślnie plik analytics.js w przykładowej aplikacji internetowej wysyła zdarzenie odsłony po wystąpieniu zdarzenia DOMContentLoaded. To nie do końca rozsądna sytuacja, ponieważ to zdarzenie jest wywoływane podczas fazy renderowania wstępnego.

Aby wprowadzić zdarzenie document.prerendering i prerenderingchange, aby rozwiązać ten problem:

  • Przepisz plik 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()
  })
  ...

Świetnie, udało Ci się zmodyfikować statystyki, by były zgodne z renderowaniem wstępnym. Dzienniki wyświetlania strony są teraz widoczne w konsoli przeglądarki z odpowiednim czasem.

4. Usuń blokady Bfcache

Usuń moduł obsługi zdarzeń unload

Zbędne zdarzenie unload to bardzo częsty błąd, który nie jest już zalecany. Nie tylko uniemożliwia to działanie pamięci podręcznej stanu strony internetowej, ale również powoduje zawodność. Na przykład nie zawsze uruchamia się na urządzeniach mobilnych i w Safari.

Zamiast zdarzenia unload używasz zdarzenia pagehide, które jest wywoływane we wszystkich przypadkach wywołania zdarzenia unload i umieszczenia strony w pamięci podręcznej stanu strony internetowej.

Aby usunąć moduł obsługi zdarzeń unload:

  • Zastąp w pliku analytics.js kod modułu obsługi zdarzenia unload kodem modułu obsługi zdarzenia 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')
})

Zaktualizuj nagłówek kontroli pamięci podręcznej

Strony wyświetlane z nagłówkiem HTTP Cache-control: no-store nie korzystają z funkcji bfcache w przeglądarce, dlatego dobrze jest taki nagłówek zachować. W szczególności, jeśli strona nie zawiera spersonalizowanych ani krytycznych informacji, takich jak stan zalogowania, prawdopodobnie nie trzeba jej udostępniać razem z nagłówkiem HTTP Cache-control: no-store.

Aby zaktualizować nagłówek kontroli pamięci podręcznej przykładowej aplikacji:

  • Zmień kod getServerSideProps:

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

stron/warzywa/[nazwa].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')
  ...

Sprawdzanie, czy strona jest przywracana z pamięci podręcznej stanu strony internetowej

Zdarzenie pageshow uruchamia się tuż po zdarzeniu load, gdy strona wczyta się na początku i po każdym przywróceniu strony z pamięci podręcznej stanu strony internetowej. Zdarzenie pageshow ma właściwość persisted, która ma wartość true (prawda), jeśli strona została przywrócona z pamięci podręcznej stanu strony internetowej, oraz wartość false (fałsz), jeśli nie została przywrócona. Możesz użyć właściwości persisted, aby odróżnić zwykłe wczytywanie strony od przywracania bfcache. Główne usługi analityczne powinny wiedzieć o pamięci podręcznej stanu strony internetowej, ale możesz sprawdzić, czy strona została przywrócona z pamięci podręcznej stanu strony internetowej, i ręcznie wysłać zdarzenia.

Aby sprawdzić, czy strona zostanie przywrócona z pamięci podręcznej stanu strony internetowej:

  • Dodaj ten kod do pliku 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()
    }
  })

Debugowanie strony internetowej

Narzędzia dla programistów Chrome pomogą Ci przetestować strony, by upewnić się, że są zoptymalizowane pod kątem pamięci podręcznej stanu strony internetowej, a także zidentyfikować problemy, które mogą uniemożliwiać ich stosowanie.

Aby przetestować konkretną stronę:

  1. Otwórz stronę w Chrome.
  2. W Narzędziach deweloperskich w Chrome kliknij Aplikacja > Pamięć podręczna stanu strony internetowej > Przeprowadź test.

Narzędzia Chrome dla programistów próbują przejść na inną stronę, a następnie z powrotem, aby określić, czy stronę można przywrócić z pamięci podręcznej stanu strony internetowej.

49bf965af35d5324.png

Jeśli operacja się uda, panel poinformuje Cię, że strona została przywrócona z pamięci podręcznej stanu strony internetowej:

47015a0de45f0b0f.png

Jeśli się to uda, panel poinformuje Cię, że strona nie została przywrócona i dlaczego. Jeśli problem jest związany z problemem, deweloper również Cię o tym poinformuje.

dcf0312c3fc378ce.png

5. Włącz wstępne pobieranie z innych witryn

Wstępne pobieranie rozpoczyna się z wyprzedzeniem, dzięki czemu bajty są już w przeglądarce, gdy użytkownik porusza się po niej, co przyspiesza nawigację. To prosty sposób na poprawę podstawowych wskaźników internetowych i wyeliminowanie części aktywności w sieci przed rozpoczęciem nawigacji. Powoduje to bezpośrednio największe wyrenderowanie treści (LCP) i daje więcej miejsca na opóźnienie przy pierwszym działaniu (FID) i skumulowane przesunięcie układu (CLS) podczas nawigacji.

Prywatny serwer proxy pobierania z wyprzedzeniem umożliwia pobieranie z wyprzedzeniem z innych witryn, ale nie ujawnia serwerowi docelowemu prywatnych informacji o użytkowniku.

Jak działa prywatny serwer proxy pobierania z wyprzedzeniem

Włącz wstępne pobieranie z innych witryn z prywatnym serwerem proxy pobierania z wyprzedzeniem

Właściciele witryn zachowują kontrolę nad pobieraniem z wyprzedzeniem, korzystając ze znanego źródła porad dotyczących ruchu, podobnego do /robots.txt w przypadku robotów indeksujących, który pozwala serwerowi HTTP zadeklarować, że agenty implementujące powinny stosować odpowiednie porady. Obecnie właściciele witryn mogą doradzić agentowi blokowanie lub ograniczanie połączeń sieciowych. W przyszłości możemy dodać inne wskazówki.

Aby hostować zasób z poradami dotyczącymi ruchu:

  1. Dodaj ten plik w stylu JSON:

publiczna/.well-known/traffic-advice

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

Pole google_prefetch_proxy_eap jest polem specjalnym dla programu wcześniejszego dostępu, a pole fraction to pole do kontrolowania odsetka żądanych pobrań z wyprzedzeniem, które są wysyłane przez prywatny serwer proxy pobierania z wyprzedzeniem.

Porada dotycząca ruchu powinna być zwracana z typem MIME application/trafficadvice+json.

  1. W pliku next.config.js skonfiguruj nagłówek odpowiedzi:

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. Zintegruj interfejs Shared Element Migrates API

Gdy użytkownik przechodzi w internecie z jednej strony na drugą, widziana zawartość zmienia się nagle i nieoczekiwanie, gdy pierwsza strona znika, a pojawia się nowa strona. Taki sekwencyjny, pozbawiony połączenia element interfejsu może dezorientować i zwiększać obciążenie poznawcze, ponieważ użytkownik jest zmuszony do samodzielnego ułożenia, jak dotrzeć do miejsca, w którym się znajduje. Dodatkowo zwiększa to to, jak bardzo użytkownicy będą postrzegać wczytywanie strony, gdy oczekują na wczytanie żądanego miejsca docelowego.

Płynne animacje wczytywania zmniejszają obciążenie poznawcze, ponieważ użytkownicy pozostają w kontekście podczas przechodzenia między stronami, a zmniejszają opóźnienie wczytywania, ponieważ użytkownicy widzą w międzyczasie coś ciekawego i ciekawego. Z tego względu większość platform udostępnia łatwe w obsłudze elementy podstawowe, które pozwalają deweloperom tworzyć płynne przejścia. Dotyczy to systemów Android, iOS, MacOS i Windows.

Interfejs Shared Element Migrates API zapewnia deweloperom te same możliwości w internecie, niezależnie od tego, czy przejścia są w różnych dokumentach (MPA), czy w obrębie dokumentu (SPA).

Prezentacja interfejsu Shared Element Migrates API z Pixiv Prezentacja interfejsu Shared Element Migrates API z Tokopedii

Demonstracje z Pixiv i Tokopedii

Aby zintegrować interfejs Shared Element Migrates API na potrzeby części przykładowej aplikacji SPA:

  1. Utwórz niestandardowy punkt zaczepienia do zarządzania przejściem w pliku 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. Wywołaj niestandardowy punkt zaczepienia usePageTransitionPrep() na stronie z listą, a następnie wywołaj funkcję asynchroniczną, aby aktywować metodę transition.start() wewnątrz zdarzenia click.

Elementy klasy shared-element są w niej zbierane i rejestrowane jako elementy udostępnione.

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. Na stronie z informacjami wywołaj hak usePageTransition(), aby zakończyć funkcję wywołania zwrotnego transition.start().

W wywołaniu zwrotnym rejestrowane są też elementy udostępnione na stronie szczegółów.

strony/owoce/[nazwa].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>
  )
...
}

Teraz widać, że elementy graficzne są wspólne na stronach z listą i informacjami oraz płynnie łączą się ze sobą podczas przechodzenia między stronami. Możesz nawet dostosować animację i nadać jej bardziej wyrazisty charakter za pomocą pseudoelementów CSS.

Przykładowa wersja demonstracyjna aplikacji bez przenoszenia wspólnego elementu Przykładowa wersja demonstracyjna aplikacji z przeniesieniem udostępnionego elementu

7. Gratulacje

Gratulacje! Udało Ci się stworzyć błyskawiczną, wygodną aplikację internetową, która jest łatwa w obsłudze, angażująca i intuicyjna.

Więcej informacji

Renderowanie wstępne

bfcache

Pobieranie z wyprzedzeniem między witrynami

Signed Exchange

Przejścia elementów głównych/udostępnionych

Te interfejsy API są wciąż na wczesnym etapie opracowywania, więc prześlij nam swoją opinię na crbug.com lub prześlij informację o problemie w repozytorium GitHub odpowiednich interfejsów API.