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

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 для мгновенной загрузки с помощью кнопок браузера «назад» и «вперед»
  • Отличные первые впечатления от навигации между источниками с помощью частного прокси-сервера предварительной выборки или подписанного обмена (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:

компоненты/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 .

компоненты/список-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 динамически вставляет тег сценария на страницу, когда приложение обновляет состояние prerenderURL .

компоненты/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. Нажмите Предварительный рендеринг .

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

Пример демонстрационного видео приложения для предварительного рендеринга

Аналитика

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

Чтобы ввести события document.prerendering и prerenderingchange для устранения этой проблемы:

  • Перепишите файл analytics.js :

общественный/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 :

общественный/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 :

страницы/овощи/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')
  ...

страницы/овощи/[имя].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 .

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

Предварительная выборка начинается раньше, так что байты уже находятся в браузере, когда пользователь перемещается, что ускоряет навигацию. Это простой способ улучшить основные веб-показатели и компенсировать некоторую сетевую активность перед навигацией. Это напрямую ускоряет отрисовку наибольшего содержимого (LCP) и дает больше места для задержки первого ввода (FID) и совокупного смещения макета (CLS) при навигации.

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

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

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

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

Чтобы разместить ресурс с рекомендациями по дорожному движению:

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

public/.well-known/traffic-advice

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

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

Рекомендации по трафику должны возвращаться с MIME-типом application/trafficadvice+json .

  1. В файле next.config.js настройте заголовок ответа:

следующий.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 Shared Element Transitions для части 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() на странице списка, а затем вызовите функцию async, чтобы вызвать метод transition.start() внутри события click .

Внутри функции элементы класса shared-element собираются и регистрируются как общие элементы.

компоненты/список-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() .

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

страницы/фрукты/[имя].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 .

Пример демонстрационного видео приложения без перехода к общему элементуПример демонстрационного видео приложения с Shared Element Transition

7. Поздравления

Поздравляем! Вы создали мгновенное и простое веб-приложение с простым, привлекательным и интуитивно понятным пользовательским интерфейсом.

Узнать больше

Предварительный рендеринг

bfcache

Межсайтовая предварительная загрузка

Подписанные биржи

Переходы корневого/общего элемента

Эти API все еще находятся на ранних стадиях разработки, поэтому поделитесь своими отзывами на crbug.com или о проблемах в репозитории соответствующих API на Github.