Добавьте в веб-приложение мгновенную навигацию и плавные переходы между страницами

1. Подготовка

В этой практической работе вы узнаете, как добавить в пример веб-приложения мгновенную навигацию и плавные переходы между страницами с помощью новейших API, которые изначально поддерживаются в Google Chrome.

Пример веб-приложения проверяет пищевую ценность популярных фруктов и овощей. Страницы со списком фруктов и информацией о них созданы как одностраничное приложение, а страницы со списком овощей и информацией о них – как традиционное многостраничное приложение.

Скриншот примера приложения на мобильном устройстве Скриншот примера приложения на мобильном устройстве

В частности, для мгновенной навигации вы реализуете предварительную отрисовку, возвратный кеш (bfcache) и частный прокси-сервер предварительной загрузки, а для плавных переходов между страницами – переходы между корневыми/общими элементами. Вы реализуете предварительную отрисовку и bfcache для страниц MPA, а также переходы между общими элементами для страниц SPA.

Скорость загрузки сайта – важный аспект удобства для пользователей. Именно поэтому Google использует Core Web Vitals, набор показателей, которые позволяют оценить скорость загрузки, интерактивность и визуальную стабильность веб-страниц, чтобы измерить реальное удобство для пользователей. Новейшие API помогают улучшить показатели Core Web Vitals вашего сайта, особенно скорость загрузки.

Демонстрационное изображение, показывающее, как bfcache улучшает время загрузки

Демонстрация от Mindvalley

Пользователи также привыкли к использованию переходов, чтобы сделать навигацию и изменения состояния чрезвычайно интуитивно понятными в мобильных нативных приложениях. К сожалению, воспроизвести такие возможности в интернете непросто. Хотя вы можете добиться аналогичных эффектов с помощью текущих API веб-платформы, разработка может быть слишком сложной, особенно по сравнению с аналогами функций в приложениях для Android или приложениях iOS. API Seamless призваны устранить этот разрыв между приложениями и сайтами.

Демоверсия Shared Element Transitions API от pixiv Демонстрация Shared Element Transitions API от Tokopedia

Демонстрации от pixiv и Tokopedia

Требования

Знание:

Вы узнаете:

Рекомендации по внедрению

  • Предварительная отрисовка
  • bfcache
  • Частный прокси-сервер для предварительной загрузки
  • Переходы между корневыми/общими элементами

Что вы создадите

Пример веб-приложения, созданного с помощью Next.js и дополненного новейшими возможностями браузера для мгновенной и бесперебойной работы:

  • Мгновенная навигация с предварительной отрисовкой
  • bfcache для мгновенной загрузки при использовании кнопок "Назад" и "Вперед" в браузере;
  • Отличное первое впечатление от перекрестной навигации с помощью Private Prefetch Proxy или подписанного обмена (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. Создайте компонент кнопки, который динамически вставляет тег скрипта 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>
  )
}
  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. Интегрируйте компоненты в приложение.

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. Нажмите Предварительная отрисовка.

Теперь вы видите, что скорость загрузки значительно увеличилась. В реальных условиях предварительная отрисовка запускается для страницы, которую пользователь, скорее всего, посетит следующей.

Демонстрационное видео о предварительной отрисовке в примере приложения

Аналитика

По умолчанию файл 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 – очень распространенная ошибка, которой следует избегать. Оно не только мешает работе bfcache, но и само по себе ненадежно. Например, оно не всегда срабатывает на мобильных устройствах и в браузере 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.

Чтобы обновить заголовок cache-control в примере приложения, выполните следующие действия:

  • Измените код 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')
  ...

Как определить, была ли страница восстановлена из bfcache

Событие pageshow активируется сразу после события load при первоначальной загрузке страницы и каждый раз, когда страница восстанавливается из bfcache. У события pageshow есть свойство persisted, которое принимает значение true, если страница была восстановлена из bfcache, и false, если нет. Чтобы отличать обычные загрузки страниц от восстановлений из bfcache, можно использовать свойство 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, и выявить проблемы, которые могут препятствовать этому.

Чтобы протестировать определенную страницу, выполните следующие действия:

  1. Откройте страницу в Chrome.
  2. В инструментах разработчика в Chrome нажмите Application (Приложение) > Back-forward Cache (Кеш перехода назад и вперед) > Run Test (Запустить проверку).

Инструменты разработчика в Chrome пытаются перейти на другую страницу, а затем вернуться, чтобы определить, можно ли восстановить страницу из bfcache.

49bf965af35d5324.png

Если все прошло успешно, на панели появится сообщение о том, что страница была восстановлена из возвратного кеша.

47015a0de45f0b0f.png

Если восстановить страницу не удалось, на панели будет указана причина. Если вы можете устранить причину, это также будет указано на панели.

dcf0312c3fc378ce.png

5. Включить межсайтовую предзагрузку

Предварительная выборка позволяет заранее загрузить данные, чтобы они уже были в браузере, когда пользователь перейдет на страницу. Это ускоряет навигацию. Это простой способ улучшить показатели Core Web Vitals и компенсировать часть сетевой активности до перехода на страницу. Это напрямую ускоряет Largest Contentful Paint (LCP) и дает больше времени для First Input Delay (FID) и Cumulative Layout Shift (CLS) при навигации.

Частный прокси-сервер для предзагрузки позволяет выполнять предзагрузку между сайтами, но не передает конфиденциальную информацию о пользователе целевому серверу.

Как работает частный прокси-сервер для предварительной загрузки

Как включить межсайтовую предзагрузку с помощью Private Prefetch Proxy

Владельцы сайтов могут управлять предзагрузкой с помощью ресурса traffic-advice, который работает аналогично файлу /robots.txt для поисковых роботов. Он позволяет HTTP-серверу объявить, что агенты реализации должны применять соответствующие рекомендации. В настоящее время владельцы сайтов могут рекомендовать агенту запрещать или ограничивать сетевые подключения. В будущем могут быть добавлены другие советы.

Чтобы разместить ресурс traffic-advice, выполните следующие действия:

  1. Добавьте следующий файл в формате JSON:

public/.well-known/traffic-advice

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

Поле google_prefetch_proxy_eap предназначено для программы раннего доступа, а поле fraction – для управления долей запрошенных предзагрузок, которые отправляет частный прокси-сервер предзагрузки.

Traffic Advice должен возвращаться с 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 Transitions API от pixiv Демонстрация Shared Element Transitions API от Tokopedia

Демонстрации от pixiv и Tokopedia

Чтобы интегрировать Shared Element Transitions API для одностраничной части примера приложения, выполните следующие действия:

  1. Создайте специальный хук для управления переходом в файле 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. Вызовите пользовательский хук usePageTransitionPrep() на странице списка, а затем вызовите асинхронную функцию, чтобы активировать метод 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. На странице приложения вызовите обработчик usePageTransition(), чтобы завершить функцию обратного вызова transition.start().

В этом обратном вызове также регистрируются общие элементы на странице сведений.

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.