Menambahkan navigasi instan dan transisi halaman yang lancar ke aplikasi web

1. Sebelum memulai

Codelab ini mengajarkan cara menambahkan navigasi instan dan transisi halaman yang lancar ke aplikasi web contoh dengan API terbaru yang didukung Google Chrome secara native.

Aplikasi web contoh memeriksa nilai gizi buah dan sayuran populer. Halaman daftar buah dan detail buah dibuat sebagai aplikasi halaman tunggal (SPA), dan halaman daftar sayuran serta detail sayuran dibuat sebagai aplikasi beberapa halaman tradisional (MPA).

Screenshot aplikasi contoh di seluler Screenshot aplikasi contoh di seluler

Secara khusus, Anda menerapkan pra-rendering, back-forward cache (bfcache), dan Proxy Pengambilan Data Pribadi untuk navigasi instan, dan transisi elemen bersama/root untuk transisi halaman yang lancar. Anda menerapkan pra-rendering dan bfcache untuk halaman MPA, dan transisi elemen bersama untuk halaman SPA.

Kecepatan situs selalu menjadi aspek penting pengalaman pengguna, itulah sebabnya Google memperkenalkan Data Web Inti, kumpulan metrik yang mengukur performa pemuatan, interaktivitas, dan stabilitas visual halaman web untuk mengukur informasi pengalaman pengguna di seluruh dunia. API terbaru membantu Anda meningkatkan skor Data Web Inti situs Anda di lapangan, terutama untuk performa pemuatan.

gambar demo cara bfcache mengoptimalkan waktu pemuatan

Demo dari Mindvalley

Pengguna juga terbiasa dengan penggunaan transisi untuk membuat navigasi dan perubahan status sangat intuitif di aplikasi native seluler. Sayangnya, replikasi pengalaman pengguna tersebut di web tidaklah jelas. Meskipun Anda mungkin dapat mencapai efek yang serupa dengan API platform web saat ini, pengembangan mungkin terlalu sulit atau rumit, terutama jika dibandingkan dengan fitur serupa pada aplikasi Android atau iOS. API yang lancar didesain untuk mengisi kesenjangan pengalaman pengguna dan developer antara aplikasi dan web.

Demo Shared Element Transitions API dari pixiv Shared Element Transitions API dari Tokopedia

Demo dari pixiv dan Tokopedia

Prasyarat

Pengetahuan tentang:

Yang akan Anda pelajari:

Cara menerapkan:

  • Pra-rendering
  • bfcache
  • Proxy Pengambilan Data Pribadi
  • Transisi elemen root/bersama

Yang akan Anda buat

Aplikasi web contoh yang dibuat dengan Next.js yang diperkaya dengan kemampuan browser instan dan lancar terbaru:

  • Navigasi yang nyaris seketika dengan pra-rendering
  • bfcache untuk pemuatan instan dengan tombol maju dan mundur browser
  • Kesan pertama yang memuaskan dari navigasi lintas asal dengan Proxy Pengambilan Data Pribadi atau Signed HTTP Exchange (SXG)
  • Transisi yang lancar antara halaman dengan transisi elemen root/bersama

Yang Anda perlukan

  • Chrome versi 101 atau yang lebih baru

2. Memulai

Mengaktifkan tanda Chrome

  1. Buka about://flags, lalu aktifkan tanda runtime Prerender2 dan documentTransition API.
  2. Mulai ulang browser.

Mendapatkan kode

  1. Buka kode dari repositori GitHub ini di lingkungan pengembangan favorit Anda:
git clone -b codelab git@github.com:googlechromelabs/instant-seamless-demo.git
  1. Instal dependensi yang diperlukan untuk menjalankan server:
npm install
  1. Mulai server pada port 3000:
npm run dev
  1. Buka http://localhost:3000 di browser Anda.

Sekarang Anda dapat mengedit dan meningkatkan aplikasi kualitas. Setiap kali Anda melakukan perubahan, aplikasi akan dimuat ulang dan perubahannya akan langsung terlihat.

3. Mengintegrasikan pra-rendering

Untuk tujuan demo ini, waktu pemuatan halaman detail sayuran di aplikasi contoh sangat lambat karena terjadi keterlambatan arbitrer di sisi server. Anda menghapus waktu tunggu ini dengan pra-rendering.

Untuk menambahkan tombol pra-render ke halaman daftar sayuran, biarkan tombol tersebut memicu pra-rendering setelah pengguna mengklik:

  1. Buat komponen tombol, yang menyisipkan tag skrip aturan spekulasi secara dinamis:

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. Impor komponen PrerenderButton di file 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. Buat komponen untuk menambahkan Speculation Rules API.

Komponen SpeculationRules akan menyisipkan tag skrip secara dinamis ke dalam halaman saat aplikasi memperbarui status 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. Integrasikan komponen dengan aplikasi.

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
  1. Klik Pra-render.

Sekarang Anda dapat melihat pengoptimalan pemuatan yang signifikan. Dalam kasus penggunaan yang sebenarnya, pra-rendering dipicu untuk halaman yang kemungkinan dikunjungi pengguna berikutnya menurut sejumlah heuristik.

Video demo aplikasi contoh untuk pra-rendering

Analisis

Secara default, file analytics.js di aplikasi web contoh mengirimkan peristiwa kunjungan halaman saat peristiwa DOMContentLoaded terjadi. Sayangnya, tindakan ini kurang tepat karena peristiwa ini dilepaskan selama fase pra-rendering.

Untuk memperkenalkan peristiwa document.prerendering dan prerenderingchange guna memperbaiki masalah ini:

  • Tulis ulang file 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()
  })
  ...

Bagus. Anda berhasil mengubah analisis sehingga kompatibel dengan pra-rendering. Sekarang Anda dapat melihat log kunjungan halaman dengan waktu yang tepat di konsol browser.

4. Menghapus pemblokir bfcache

Menghapus pengendali peristiwa unload

Peristiwa unload yang tidak diperlukan adalah kesalahan yang sangat umum. Hal ini tidak disarankan lagi. Tidak hanya mencegah bfcache agar berfungsi, tetapi peristiwa ini juga tidak dapat diandalkan. Misalnya, peristiwa ini tidak selalu aktif di seluler dan Safari.

Daripada menggunakan peristiwa unload, gunakan peristiwa pagehide, yang aktif di semua kasus saat peristiwa unload aktif dan saat halaman dimasukkan ke dalam bfcache.

Untuk menghapus pengendali peristiwa unload:

  • Di file analytics.js, ganti kode untuk pengendali peristiwa unload dengan kode untuk pengendali peristiwa 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')
})

Memperbarui header kontrol cache

Halaman yang ditayangkan dengan header HTTP Cache-control: no-store tidak mendapatkan manfaat dari fitur bfcache browser, sehingga sebaiknya Anda menghemat waktu dengan header ini. Khususnya, jika halaman tidak berisi informasi yang dipersonalisasi atau informasi penting, seperti status login, Anda mungkin tidak perlu menayangkannya dengan header HTTP Cache-control: no-store.

Untuk memperbarui header kontrol cache dari aplikasi contoh:

  • Ubah kode 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')
  ...

Menentukan apakah halaman dipulihkan dari bfcache

Peristiwa pageshow aktif tepat setelah peristiwa load, saat halaman pertama kali dimuat dan setiap kali halaman dipulihkan dari bfcache. Peristiwa pageshow memiliki properti persisted, yang benar jika halaman dipulihkan dari bfcache dan salah jika tidak. Anda dapat menggunakan properti persisted untuk membedakan pemuatan halaman reguler dari pemulihan bfcache. Layanan analisis utama harus mengetahui tentang bfcache, tetapi Anda dapat memeriksa apakah halaman dipulihkan dari bfcache dan mengirim peristiwa secara manual.

Untuk menentukan apakah halaman dipulihkan dari bfcache:

  • Tambahkan kode ini ke file 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()
    }
  })

Men-debug halaman web

Chrome Developer Tools dapat membantu Anda menguji halaman guna memastikan bahwa halaman dioptimalkan untuk bfcache dan mengidentifikasi masalah yang dapat membuatnya tidak memenuhi syarat.

Untuk menguji halaman tertentu:

  1. Buka halaman di Chrome.
  2. Di Chrome Developer Tools, klik Application > Back-forward Cache > Run Test.

Chrome Developer Tools berusaha menutup lalu membuka untuk menentukan apakah halaman dapat dipulihkan dari bfcache.

49bf965af35d5324.png

Jika berhasil, panel akan memberi tahu Anda bahwa halaman dipulihkan dari back-forward cache:

47015a0de45f0b0f.png

Jika tidak berhasil, panel akan memberi tahu Anda bahwa halaman tidak dipulihkan beserta alasannya. Jika alasannya adalah sesuatu yang dapat Anda tangani sebagai developer, panel juga akan menyampaikannya kepada Anda.

dcf0312c3fc378ce.png

5. Mengaktifkan pengambilan data lintas situs

Pengambilan data dimulai lebih awal sehingga byte sudah ada di browser saat pengguna melakukan navigasi, sehingga mempercepat navigasi. Ini adalah cara mudah untuk meningkatkan Data Web Inti dan mengimbangi beberapa aktivitas jaringan sebelum navigasi. Tindakan ini secara langsung mempercepat Largest Contentful Paint (LCP), dan memberikan lebih banyak ruang untuk First Input Delay (Penundaan Input Pertama) (FID) dan Cumulative Layout Shift (Pergeseran Tata Letak Kumulatif) (CLS) setelah navigasi.

Proxy Pengambilan Data Pribadi memungkinkan pengambilan data lintas situs, namun tidak mengungkapkan informasi pribadi tentang pengguna ke server tujuan.

Cara kerja Proxy Pengambilan Data Pribadi

Mengaktifkan pengambilan data lintas situs dengan Proxy Pengambilan Data Pribadi

Pemilik situs mempertahankan kontrol pengambilan data melalui resource traffic-advice yang sudah diketahui, serupa dengan /robots.txt untuk web crawler, yang memungkinkan server HTTP mendeklarasikan bahwa agen penerapan harus menerapkan saran yang sesuai. Saat ini, pemilik situs dapat menyarankan agen untuk melarang atau membatasi koneksi jaringan. Di masa mendatang, saran lain mungkin ditambahkan.

Untuk menghosting resource traffic-advice:

  1. Tambahkan file mirip JSON ini:

public/.well-known/traffic-advice

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

Kolom google_prefetch_proxy_eap adalah kolom khusus untuk program akses awal dan kolom fraction adalah kolom untuk mengontrol fraksi pengambilan data yang diminta yang dikirim oleh Proxy Pengambilan Data Pribadi.

Saran traffic harus ditampilkan dengan jenis MIME application/trafficadvice+json.

  1. Pada file next.config.js, konfigurasikan header respons:

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. Mengintegrasikan Shared Element Transitions API

Saat pengguna menavigasi di web dari satu halaman ke halaman lainnya, konten yang mereka lihat berubah secara tiba-tiba dan tidak terduga saat halaman pertama menghilang dan halaman baru muncul. Pengalaman pengguna yang terputus dan berurutan ini membingungkan dan berujung pada beban kognitif yang lebih tinggi karena pengguna terpaksa menggabungkan cara mencapai ke lokasi mereka. Selain itu, pengalaman ini meningkatkan seberapa banyak pengguna merasakan pemuatan halaman saat menunggu tujuan yang diinginkan untuk dimuat.

Animasi pemuatan yang lancar menurunkan pemuatan kognitif karena pengguna tetap berada dalam konteks saat mereka berpindah antar-halaman, dan mengurangi latensi pemuatan yang dirasakan karena sementara itu pengguna melihat sesuatu yang menarik dan menyenangkan. Karena alasan ini, sebagian besar platform menyediakan primitif yang mudah digunakan yang memungkinkan developer membuat transisi yang lancar, seperti Android, iOS, MacOS, dan Windows.

Shared Element Transitions API memberi developer kemampuan yang sama di web, terlepas dari apakah transisi lintas dokumen (MPA) atau intra-dokumen (SPA).

Demo Shared Element Transitions API dari pixiv Shared Element Transitions API dari Tokopedia

Demo dari pixiv dan Tokopedia

Untuk mengintegrasikan Shared Element Transitions API untuk bagian SPA aplikasi contoh:

  1. Buat hook kustom untuk mengelola transisi dalam file 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. Panggil hook kustom usePageTransitionPrep() di halaman daftar, lalu panggil fungsi asinkron untuk memicu metode transition.start() di dalam peristiwa click.

Di dalam fungsi, elemen class shared-element dikumpulkan dan didaftarkan sebagai elemen bersama.

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. Pada halaman detail, panggil hook usePageTransition() untuk menyelesaikan fungsi callback transition.start().

Dalam callback ini, elemen bersama di halaman detail juga terdaftar.

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>
  )
...
}

Sekarang Anda dapat melihat bahwa elemen gambar dibagikan di halaman daftar dan detail, serta terhubung dengan lancar dalam transisi halaman. Anda bahkan dapat menyesuaikan animasi agar lebih menarik dengan elemen pseudo CSS.

Video demo aplikasi contoh tanpa Transisi Elemen Bersama Video demo aplikasi contoh dengan Transisi Elemen Bersama

7. Selamat

Selamat! Anda telah membuat aplikasi web yang cepat dan lancar dengan pengalaman pengguna yang lancar, menarik, dan intuitif.

Pelajari lebih lanjut

Pra-rendering

bfcache

Pengambilan data lintas situs

Signed HTTP Exchange

Transisi elemen root/bersama

API ini masih dalam tahap awal pengembangan, jadi sampaikan masukan Anda di crbug.com atau sebagai masalah di repositori GitHub API terkait.