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

В Kbee мы позволяем пользователям превращать свои Google Документы в сверхбыструю вики. Важной частью в ускорении является обеспечение того, чтобы наши пользователи получали статический HTML-код из CDN. У наших пользователей могут быть тысячи документов, и Kbee не знает, когда контент изменяется, поэтому Kbee использует ISR для создания статических снимков документов и динамически обновляет контент по мере поступления новых запросов.

Он идеально подходит для ISR, а Vercel делает все это так же просто, как git push! Остальная часть этого сообщения в блоге предполагает, что вы используете Vercel, поскольку у других провайдеров могут быть другие особенности и нюансы.

(Примечание: сегодня мы в прямом эфире на ProductHunt! Приходите проверить!)

Обратная сторона ISR

Но, как и все хорошее в жизни, за этот волшебный опыт приходится платить. Начальное время загрузки или содержание ISR значительно медленнее, чем SSR или даже содержание CSR. Вроде на порядки медленнее!

Здесь вы можете увидеть две версии загрузки одной и той же страницы. Один - SSR, другой - начальная страница ISR.

Вау, это ОГРОМНАЯ разница!

Как ОГРОМНЫЙ!

В этот момент вы можете подумать: «Да, но с ISR страницы нужно создавать только один раз! После этого они кешируются и работают очень быстро! »

К сожалению, Vercel полностью стирает кеш при каждом развертывании. Если у вас были тысячи, десятки тысяч или даже миллионы страниц статических страниц, все эти страницы необходимо перестроить. Если вы выполняете развертывание несколько раз в день, пользователи определенно будут постоянно сталкиваться с медленными нагрузками!

Попытка решения №1: развертывание сборок времени

Решение этой проблемы - перестроить эти страницы во время развертывания и предварительно кэшировать их. Для этого вы используете getStaticPaths, чтобы запросить вашу базу данных, получить все текущие страницы и построить их все. Теперь уже созданные страницы остаются быстрыми, а новые страницы проходят обычный процесс ISR!

Итак, мы сделали правильно? Ну не так быстро. Это может быть хорошо, если у вас есть несколько сотен страниц, но если у вас есть миллионы страниц или страниц, которые дороги для рендеринга, время сборки может быстро увеличиться до нескольких часов.

Для нас сборка ушла с 6 минут (что медленно, но не страшно) до 20 минут! А клиентов у нас было всего несколько!

Масштабировать этот метод не собирался. Мы должны были сделать что-то другое.

Почему это медленно?

Чтобы сделать что-то быстрым, нам нужно понять, почему это происходит медленно. Очевидным местом для поиска было фактическое построение страниц. Kbee выполняет несколько вызовов API Google Диска и Документов для создания страницы, каждый из которых может составлять до 100 миллисекунд. Тем не менее, создание каждой страницы занимало максимум полсекунды. Это соответствовало времени загрузки SSR, но время загрузки ISR было значительно медленнее.

Что еще более странно, вызовы навигационных страниц, которые делали только один быстрый запрос в Firestore, по-прежнему занимали несколько секунд для рендеринга в режиме ISR.

Так почему же ISR такая медленная? Подсказкой была одна строчка в журналах сборки:

Этот этап загрузки занимал все больше времени, чем больше страниц было статически построено во время развертывания. И тогда все это имело смысл. Первоначальное время загрузки ISR было медленным из-за этого шага загрузки!

При рендеринге на стороне сервера процесс выглядит примерно так:
1. Клиент делает запрос
2. Сервер отображает страницу
3. Клиент отображает страницу

При рендеринге на стороне клиента процесс выглядит примерно так:
1. Клиент делает запрос
2. Сервер отправляет статический HTML-код
3. Клиент запускает javascript для получения данных

4 Страница отображается

При использовании ISR начальный процесс выглядит следующим образом (я делаю обоснованное предположение):
1. Клиент делает запрос

2. Сервер отправляет резервную страницу
3. Сервер отображает страницу
4. Сервер загружает страницу в CDN

5. Сервер сообщает клиенту, что CDN готов.
6. Клиент отправляет запрос в CDN.
7. Клиент заменяет откат на контент из CDN.

Таким образом, создание страницы происходит не так медленно, а наоборот, с CDN!

Попытка решения №2: рендеринг на стороне клиента при откате

Если получение данных происходит относительно быстро, можем ли мы создать страницу быстрее на стороне клиента, чем ждать завершения танца CDN?

Ответ однозначный: да.

Это работает следующим образом: вы создаете маршрут API, который возвращает те же данные, что и метод getStaticProps, используемый для получения данных для пути ISR. Если страница находится в резервном режиме, вы вызываете маршрут API и визуализируете страницу, используя традиционный рендеринг на стороне клиента. Как только танец CDN будет завершен, страница автоматически переключится на статический HTML. Любые новые запросы будут загружать данные из CDN напрямую и не будут выполнять рендеринг на стороне клиента.

Для Kbee я разделил выборку данных на два вызова. Первый быстро получает метаданные, такие как стиль, тема и логотипы, хранящиеся в Firestore, для создания каркаса страницы, а другой выполняет более медленный запрос фактического содержимого Документов Google.

Вот базовое воспроизведение кода для рендеринга такой страницы:

import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'

export async function getStaticPaths() {
    return { paths: [], fallback: true }
}
export async function getStaticProps({ params }) {
    // Run your data fetching code here
    const data = 'something cool'
    return { props: { data }, revalidate: 30, }
}
export default function Page({ data }) {
    const [clientData, setClientData] = useState(null)
    const { isFallback } = useRouter()

    useEffect(() => {
       if (isFallback && !clientData) {
           // Get Data from API
           fetch('/api/getPageData').then(async resp => {
               setClientData(await resp.json())
           })
       }
     }, [clientData, isFallback])

    if (isFallback || !data) return <RenderPage data={clientData} />
    else return <RenderPage data={data} />
}

Здесь приходится платить, потому что вы загружаете одни и те же данные дважды. Один раз для страницы ISR и один раз для страницы CSR. На мой взгляд, это небольшая цена, и она намного меньше, чем цена, которую вы бы заплатили за традиционные SSR.

Но есть проблема, она всегда есть

Next.js имеет действительно классный компонент ‹Link›, который выглядит и ощущается как традиционный тег привязки (подумайте ‹a href=''›), но вместо полной перезагрузки страницы он выполняет маршрутизацию на стороне клиента в стиле SPA, что делает это супер быстро и эффективно. Он также выполняет предварительную выборку страниц, что позволяет разогреть кеш перед тем, как пользователь посетит страницу, и это здорово.

За исключением одной маленькой проблемы. При переходе на страницу ISR Next.js не показывает откат и вместо этого ожидает завершения танца CDN, прежде чем показывать новую страницу!

Поскольку загрузка страниц занимает 5–6 секунд, пользователь думает, что нажатие на ссылку ничего не делает, и веб-сайт не работает! Вы можете увидеть, как люди жалуются на это в этом выпуске Github.

Попытка решения № 3: быстрое прерывание SPA

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

Хотя индикатор выполнения выглядит неплохо, переход на новую страницу все равно занимает много времени. Что мы действительно хотим сделать, так это заставить Next.js показывать резервную страницу.

Наивное решение - заменить все ‹Links› стандартными тегами ‹a›. Это удалит маршрутизацию на стороне клиента и вызовет полную перезагрузку страницы при каждой навигации. Это решение действительно отстой, особенно для людей, у которых нет хорошего интернета. Он кажется дряблым и медленным, и вы теряете одну из действительно мощных функций Next.js.

Итак, я придумал дурацкое решение, которое назвал «Fast SPA Abort». Это очень просто. Если переход по странице занимает больше 100 мс, прервите маршрутизацию SPA и выполните полное обновление страницы. Вот и все!

В свой _app.js добавьте следующий код:

import NProgress from 'nprogress'
import Router from 'next/router'

Router.onRouteChangeStart = (url) => {
    if (url !== window.location.pathname) {
        window.routeTimeout = setTimeout(() => 
             window.location = url, 100)
        NProgress.start()
    }
}
Router.onRouteChangeComplete = (url) => {
    clearTimeout(window.routeTimeout)
    NProgress.done()
}

Когда маршрут начинает меняться, мы создаем тайм-аут, который обновит страницу через 100 мс. По окончании смены маршрута мы отменяем этот тайм-аут. В 99% случаев при выполнении навигации SPA страницы ISR кэшируются в CDN, поэтому маршрутизация будет мгновенной, и этот тайм-аут никогда не будет вызван. Однако, если страница не кэшируется в CDN, маршрутизация будет прервана, и страница будет обновлена, показывая откат!

Бум, теперь ты получишь лучшее из всех миров. Быстрая маршрутизация SPA, динамические статические страницы с ISR и сборки с нулевым временем развертывания с CSR!

P.S. Если вы дочитали до этого места, зацените Kbee на ProductHunt! Я бы хотел вашу поддержку ♥