Взгляните на стратегии кеширования со слоем NodeJS перед Next.js

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

Почему так много запросов?

Прежде чем углубиться в процесс сборки Next.js, давайте посмотрим, как другой генератор статических сайтов, Gatsby, делает то же самое. У Gatsby совсем другой механизм, поскольку он выполняет SSG только тогда, когда Next.js запускается как инфраструктура SSR.

У Gatsby есть несколько этапов процесса сборки, но основными являются два: получение данных и заполнение компонентов данными, а затем рендеринг. Плагины Gatsby обычно используют уровень данных GraphQL для хранения данных из источников данных (конечно, вы можете создавать без уровня данных GraphQL и использовать неструктурированные данные). Гэтсби запускает плагины данных, затем они извлекают данные и помещают их на уровень данных. Затем он получает запросы GraphQL со страниц и подготавливает данные для шаблонов. После этого заполняет компоненты данными. Такой способ создания страниц кажется простым и понятным. Но Next.js работает иначе.

Как я уже говорил, Next.js изначально был фреймворком только для SSR с добавленной позже функцией SSG. У него есть API, где вы получаете данные и передаете их на каждую страницу, но этот метод нарушает оптимизацию SSG. Я говорю о getInitialData, когда он используется внутри _app.js

Чтобы иметь страницы SSG, мы используем только getStaticProps или просто делаем страницы без выборки данных. Когда Next.js строит страницы, он запускает функцию выборки данных, если она находится внутри файла страницы, а затем отображает страницу с этими данными. И этот процесс повторяется страница за страницей. Таким образом, если данные передаются, они будут запрашиваться каждый раз для каждой страницы. Если количество страниц в приложении увеличивается, то растет и количество запросов.

Мое приложение требует отображения списка городов на каждой странице, и эти города должны быть определены на сервере. Я знал, как работает Гэтсби, и хотел добавить аналогичный механизм, но попроще и без GraphQL 🙂

Мой первый подход

Первая идея была действительно простой. Когда первая страница вызывает, например, fetchCities, я сохраняю обещание, возвращаемое fetch, в переменную с областью видимости модуля. Когда следующая страница вызывает fetchCities, я проверяю, указывает ли наша переменная на обещание, и возвращаю его, если это так.

Идея проста. Мы просто сохраняем обещание запроса в области модуля, а затем возвращаем его всем следующим вызовам. Проблема в том, что такой подход не работает. Причина в рабочих. Next.js запускает каждую сборку страницы в своем собственном воркере. Это означает, что я не могу обмениваться данными между страницами и у меня нет доступа к месту, где я могу обмениваться данными между работниками без особых усилий. Кроме того, я проверяю, работает ли наш запрос в CI, потому что fetchCities может быть вызван при повторной проверке, и в этом случае я хочу каждый раз получать свежие данные.

Может быть, Google может помочь?

У меня была еще пара идей, но я решил погуглить. Может быть, я не первый, кто сталкивается с этой проблемой. Я нашел других людей, которые думали над тем же вопросом, и у них были разные причины сделать это. Кто-то только начал использовать Next.js, и его серверная часть не могла обслуживать приличное количество запросов. Другие люди пытались перейти с Gatsby на Next и поняли, что их бэкэнд не слишком быстр для 300 запросов в секунду. Я знал, как работает Гэтсби, потому что столкнулся с этим на предыдущей работе. Гэтсби делает это иначе. Прежде всего, подготавливает сборку путем сбора данных в локальный кеш, и в этом случае все ресурсы выбираются только один раз. Чтобы каждая страница получала данные из этого кеша. Моя идея была похожа, но мне не нужно было добавлять кеш поверх Next.js.

Кэшировать данные в файлах

Один из возможных способов решения проблемы - кеширование в файлах. Нам нужно добавить еще один шаг в наш процесс сборки и написать что-то вроде этого.

Мы запускаем скрипт, который извлекает данные и сохраняет их в файлы. Недостаток этого метода в том, что нам нужно собрать в нем все конечные точки для выборки данных.

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

Эти варианты хороши, но имеют свои минусы. Например, мы должны собрать все конечные точки для кеширования и обновить их. Или вам следует подождать, пока все ресурсы будут кешированы. С другой стороны, все ресурсы кэшируются на диске и не остаются в памяти. Я хотел поделиться этим фрагментом кода, чтобы показать вам все варианты.

Кэшировать прямо в памяти при сборке

Да, это самый простой и быстрый способ кэшировать данные и ничего не ждать. Просто передайте другой API_PATH на этапе сборки и запустите сервер кеширования перед запуском next build.

Код нашего сервера может выглядеть так:

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

Заключение

Собственно, команда Next.js планировала добавить возможность использования getStaticProps в _app.js. После этого вся проблема могла быть решена без дополнительных усилий. Я слежу за этой дискуссией, но она пока приостановлена.

Возможно, у вас есть аналогичная проблема с источником данных, и я буду рад, если эта статья вам поможет. Целью было не просто решить проблему, а попробовать использовать разные подходы. Надеюсь, я показал что-то новое, и, возможно, это вдохновит вас на решение некоторых проблем и выполнение некоторых умственных упражнений. Эти примеры могут быть полезны для решения других проблем, когда вам нужно кэшировать данные. Вы можете использовать их и изменять под свои нужды.
Полные примеры хранятся на GitHub.