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

1. Прежде чем начать

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

В представленном примере веб-приложения проверяется пищевая ценность популярных фруктов и овощей. Страницы со списком фруктов и подробным описанием фруктов созданы как одностраничное приложение (SPA), а страницы со списком овощей и подробным описанием овощей — как традиционное многостраничное приложение (MPA).

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

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

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

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

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

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

Демонстрация API переходов между общими элементами с Pixiv.Демонстрация API общих переходов элементов из Токопедии

Демонстрационные версии с Pixiv и Tokopedia.

Предварительные требования

Знание:

Что вы узнаете:

Как реализовать:

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

Что вы построите

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

  • Практически мгновенная навигация с предварительной отрисовкой.
  • bfcache для мгновенной загрузки страниц при нажатии кнопок «Назад» и «Вперед» в браузере.
  • Отличное первое впечатление от кросс-доменной навигации с использованием Private Prefetch Proxy или подписанного обмена (SXG).
  • Плавный переход между страницами с помощью перехода между корневыми/общими элементами.

Что вам понадобится

  • Chrome версии 101 или выше

2. Начните

Включить флаги Chrome

  1. Перейдите по адресу about://flags и включите флаги среды выполнения API 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. Создайте компонент для добавления 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])
}
  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 в примере веб-приложения отправляет событие page-view при возникновении события 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, если нет. Вы можете использовать свойство persisted , чтобы отличать обычные загрузки страниц от восстановления из bfcache. Основные аналитические сервисы должны знать о 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 нажмите «Приложение» > «Кэш» > «Запустить тест».

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

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

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

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

Для размещения ресурса с советами по организации дорожного движения:

  1. Добавьте этот JSON-подобный файл:

общедоступные/известные/советы по дорожному движению

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

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

Рекомендации по управлению трафиком должны возвращаться с 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. Интеграция API переходов для общих элементов.

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

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

API Shared Element Transitions предоставляет разработчикам одинаковые возможности в веб-среде независимо от того, являются ли переходы междокументными (MPA) или внутридокументными (SPA).

Демонстрация API переходов между общими элементами с Pixiv.Демонстрация API общих переходов элементов из Токопедии

Демонстрационные версии с Pixiv и Tokopedia.

Для интеграции API переходов между общими элементами в одностраничное приложение (SPA) в демонстрационном примере приложения:

  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 собираются и регистрируются как 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.