Автор статьи, перевод которой мы сегодня публикуем, работает программистом в компании Antler. Эта компания представляет собой глобальный генератор стартапов. В Antler несколько раз в году проходят демонстрационные дни, собирающие множество создателей стартапов и инвесторов со всего мира. Ситуация вокруг COVID-19 вынудила Antler перевести свои мероприятия в онлайн-формат.
Компании хотелось сделать так, чтобы посетители их виртуальных мероприятий, ни на что не отвлекаясь, и нигде не застревая, видели бы самое главное. А именно — идеи представляемых публике стартапов, выраженные в виде содержимого веб-страниц. Виртуальные демонстрационные дни могут быть интересны достаточно широкой аудитории. Некоторые представители этой аудитории, возможно, впервые принимают участие в чём-то подобном. Поэтому компании нужно было сделать всё самым лучшим образом и обеспечить высокую скорость загрузки страниц, представляющих стартапы. Они решили, что это — как раз тот случай, когда им может пригодиться высокопроизводительное прогрессивное веб-приложение (PWA, Progressive Web App). Главный вопрос заключался в подборе подходящей технологии для разработки PWA.
Серверный рендеринг или генератор статических сайтов?
Для начала немного введу вас в курс дела. Все наши проекты сделаны на базе React и библиотеки Material-UI. В результате мы изначально решили не отходить от этого стека технологий, что позволило бы нам обеспечить высокую скорость разработки и сделать новый проект совместимым с тем, что у нас уже есть. Ключевое отличие этого нового проекта от других наших React-приложений заключалось в том, что база для них создавалась с помощью create-react-app, и в том, что они полностью рендерились на клиенте (CSR, Client-Side Rendering). Это, в частности, приводило к тому, что пользователи, при первоначальной загрузке приложений, были вынуждены наблюдать пустой белый экран в то время, пока загружался, обрабатывался и выполнялся JavaScript-код проектов.
Нам нужен был бескомпромиссный уровень производительности. Поэтому мы стали размышлять о том, чтобы воспользоваться либо серверным рендерингом (SSR, Server-Side Rendering), либо генератором статических сайтов (SSG, Static Site Generator) для того чтобы первоначальная загрузка приложений выполнялась бы как можно быстрее.
Наши данные хранятся в Cloud Firestore, а обращаемся мы к ним с помощью Algolia. Это даёт нам возможность контролировать, на уровне полей базы данных, публичный доступ к данным с ограниченными ключами API. Это, кроме того, улучшает производительность запросов. Из опыта нам известно то, что запросы Algolia выполняются быстрее обычных, и то, что размер Firestore JavaScript SDK в сжатом виде составляет 86 Кб. В случае с Algolia это — 7.5 Кб.
Мы, кроме того, хотели сделать так, чтобы данные, которые мы отдаём клиентам, были бы как можно более свежими. Это помогло бы нам в очень быстром исправлении ошибочных данных, которые могли бы быть случайно опубликованы. В то время как стандартная практика SSG предусматривает выполнение соответствующих запросов на получение данных во время сборки проекта, мы ожидали того, что в нашу БД данные будут записываться достаточно часто. В частности, речь идёт о записи данных по инициативе администраторов, пользующихся интерфейсом firetable, и по инициативе основателей проектов, пользующихся веб-порталом. Это приводит к конкурентному выполнению сборок проекта. Кроме того, из-за особенностей структуры нашей базы данных, незначительные изменения могут приводить к вызову новых операций сборки проекта. Это делает наш CI/CD-конвейер крайне неэффективным. Поэтому нам нужно было, чтобы запросы на получение данных из хранилища выполнялись бы каждый раз, когда пользователь запрашивает загрузку страницы. К сожалению, это означало, что наше решение не будет образцом «чистого» SSG-проекта.
Изначально наше приложение было создано на базе Gatsby, так как мы уже пользовались лэндинг-страницами, построенными на Gatsby, а на одной из них уже применялась библиотека Material-UI. Первая версия проекта формировала страницу, которая, пока загружаются данные, выводила «скелет». При этом время первой отрисовки контента (First contentful paint, FCP) находилось в районе 1 секунды.
Загрузка «скелета» страницы с последующей загрузкой данных
Решение получилось интересное, но у него были свои минусы, вызванные тем, что данные для вывода страницы загружались по инициативе клиента:
- Пользователям, чтобы увидеть содержимое страницы, пришлось бы ждать загрузки самой этой страницы и данных, выводимых в ней, получаемых посредством 4 запросов к Algolia.
- При таком подходе на JS-движок браузера ложится повышенная нагрузка. Дело в том, что React нужно переключить «скелет» страницы на вывод реального содержимого страницы. А это означает необходимость выполнения дополнительных манипуляций с DOM.
- У поисковых систем могут быть проблемы с индексированием подобных страниц. Поисковые системы, кроме того, обычно предпочитают статические сайты.
В результате я, на длинных выходных, решил поэкспериментировать с SSR-версией проекта, созданной с помощью Next.js. К счастью для меня, в документации к Material-UI был пример проекта для Next.js. Поэтому мне не нужно было изучать весь этот фреймворк с нуля. Мне надо было лишь просмотреть некоторые части учебного руководства и документации. Я преобразовал приложение в проект, который рендерился на сервере. Когда пользователь запрашивал загрузку страницы, запросы на получение данных, необходимых для наполнения страницы, выполнял сервер. Этот шаг позволил решить все три вышеозначенных проблемы. Вот результаты испытания двух вариантов приложения.
Результаты исследования приложений с помощью Google PageSpeed Insights. Слева — Gatsby (SSG), справа — Next.js (SSR) (оригинал изображения)
Показатель FCP для Next.js-варианта проекта оказался примерно в 3 раза больше, чем для его варианта, основанного на Gatsby. Показатель Speed Index у Gatsby-варианта проекта составил 3.3 секунды, а у Next.js-варианта — 6.2 секунды. Время до первого байта (TTFB, Time To First Byte) при использовании Nex.js составило 2.56 секунды, а при использовании Gatsby — 10-20 мс.
Следует отметить, что Next.js-версия сайта была развёрнута на другом сервисе (тут использовались сервисы ZEIT Now и Firebase Hosting — это тоже могло повлиять на повышение TTFB). Но, несмотря на это, было понятно, что перемещение операций по загрузке данных на сервер привело к тому, что сайт кажется медленнее, несмотря на то, что все материалы страницы загружались примерно за то же самое время. Дело в том, что в Next.js-варианте проекта пользователь некоторое время видит лишь пустую белую страницу.
Запись экрана, демонстрирующая загрузку двух версий приложения. Загрузка выполнялась не одновременно. Записи синхронизированы по моменту нажатия на клавишу Enter
Всё это даёт нам важный урок из сферы веб-разработки: нужно давать пользователям визуальную обратную связь. В одном исследовании выяснено, что приложения, в которых используются «скелетные» экраны, кажутся загружающимися быстрее.
Этот результат, кроме того, не соответствует тому настрою, который вы, возможно, уловили, если читали в последние несколько лет статьи про веб-разработку. А именно, речь идёт о том, что в использовании ресурсов клиента нет ничего плохого, и о том, что SSR — это не всеобъемлющее решение проблем с производительностью.
Производительность генерирования статического сайта: сравнение Gatsby и Next.js
В то время как два рассматриваемых фреймворка, Gatsby и Next.js, известны, соответственно, своими возможностями по генерированию статических сайтов и по серверному рендерингу, в Next.js 9.3 была доработана поддержка SSG, что делает его конкурентом Gatsby.
Во время написания этого материала возможность Next.js по генерированию статических сайтов была ещё очень свежей. Ей было чуть больше месяца. О ней всё ещё сообщается на первой странице проекта. Сейчас существует не очень много сравнений SSG-возможностей Gatsby и Next.js (а может, таких сравнений и вовсе пока нет). В результате я решил провести собственный эксперимент.
Я вернул Gatsby-версию проекта к состоянию, когда данные загружались на клиенте, и сделал так, чтобы обе версии приложения обладали бы абсолютно одинаковым набором возможностей. А именно, мне пришлось убрать то, за что отвечают плагины Gatsby: SEO-функции, генерирование фавиконов, PWA-манифест. Для того чтобы сравнить исключительно JavaScript-бандлы, создаваемые фреймворками, я не включал в проекты изображения и другое содержимое, загружаемое из внешних источников. Обе версии приложения были развёрнуты на платформе Firebase Hosting. Для справки, два варианта приложения были созданы на базе Gatsby 2.20.9 и Next.js 9.3.4.
Я запустил Lighthouse на моём компьютере 6 раз для каждой версии. Результаты показали небольшое преимущество Gatsby.
Средние значения показателей, полученных после 6 запусков Lighthouse для каждого фреймворка (оригинал изображения)
В плане общей оценки производительности Next.js-версия оказалась лишь слегка позади Gatsby-версии. То же самое касается показателей FCP и Speed Index. Показатель Max Potential First Input Delay у Next.js-версии приложения немного больше, чем у Gatsby-версии.
Для того чтобы лучше разобраться в происходящем, я обратился к вкладке Network инструментов разработчика Chrome. Как оказалось, в Next.js-версии проекта число фрагментов, на которые разбит JavaScript-код, на 3 больше, чем в Gatsby-версии (не учитывая файлы-манифесты), но размер сжатого кода на 20 Кб меньше. Могут ли дополнительные запросы, необходимые для загрузки этих файлов, настолько перевесить преимущества, которые даёт меньший размер бандла, что это повредило производительности?
В Gatsby-версии проекта выполняется 7 запросов для загрузки 379 Кб данных. В Next.js-версии проекта — 12 запросов для загрузки 359 Кб данных (оригинал изображения)
Если проанализировать производительность JavaScript, то инструменты разработчика говорят о том, что Next.js-версии проекта нужны дополнительные 300 мс на первую отрисовку, и о том, что эта версия тратит больше времени на задачу Evaluate Script. В инструментах разработчика эта задача даже была отмечена как «long task».
Анализ производительности разных вариантов проекта средствами вкладки Performance инструментов разработчика Chrome (оригинал изображения)
Я сравнил код проектов для того чтобы узнать о том, имеются ли в их реализации какие-то различия, которые могли повлиять на производительность. За исключением удаления ненужного кода и исправлений, связанных с отсутствующими TypeScript-типами, единственным различием была реализация плавной прокрутки страницы при переходе к её отдельным частям. Эта возможность раньше была представлена файлом gatsby-browser.js
и была перемещена в динамически импортируемый компонент. В результате этот код запускался бы только в браузере. (Мы пользовались npm-пакетом smooth-scroll, а ему, во время его импорта, нужен объект window
.) Возможно, виновником проблемы является эта особенность, но я просто не знаю о том, как это обрабатывается в Next.js.
Gatsby удобнее с точки зрения разработчика
В итоге я решил остановить свой выбор на Gatsby-версии проекта. Причём, тут я не учитывал то очень небольшое преимущество в производительности, которое Gatsby показал по сравнению с SSG-механизмом Next.js (не стану же я всерьёз цепляться за преимущество в 0.6 секунды?). Дело в том, что в Gatsby-версии проекта уже реализованы многие возможности PWA, и я не видел смысла в том, чтобы снова их реализовывать в Next.js-версии приложения.
Когда я только создавал первую Gatsby-версию проекта, я смог быстро добавить в проект некоторые полезные PWA-возможности. Например, для добавления на каждую страницу собственных мета-тегов, нужных для SEO, мне достаточно было почитать руководство. Для оснащения проекта PWA-манифестом мне понадобилось лишь применить соответствующий плагин. Для того чтобы оснастить проект фавиконами, которые поддерживали бы все доступные платформы (а в этом деле до сих пор царит ужасный беспорядок), мне даже не пришлось ничего делать, так как поддержка фавиконов — это часть плагина, ответственного за манифест. Это очень удобно!
Реализация тех же самых возможностей в Next.js-версии приложения потребовала бы больше работы. Мне пришлось бы искать учебные руководства, всякие «лучшие практики». И то, что у меня получилось бы, всё равно, не дало бы мне никаких плюсов. Ведь, всё же, Next.js-версия проекта не отличается более высокой производительностью, чем его Gatsby-версия. Это, кроме того, послужило причиной, по которой я решил просто отключить соответствующие возможности Gatsby-версии проекта, сравнивая её с Next.js-версией. Документация Next.js более лаконична, чем документация Gatsby (может, дело в том, что Next.js меньше Gatsby). Мне очень нравится геймифицированный учебник по Next.js. Но более пространная документация Gatsby оказывается ценнее при реальной разработке PWA, даже несмотря на то, что на первый взгляд она выглядит огромной.
Документация Gatsby
Правда, я не могу умолчать и о сильных сторонах Next.js:
- Благодаря учебному руководству и краткости документации по Next.js возникает такое ощущение, что этот фреймворк можно изучить быстрее, чем Gatsby.
- В основе системы загрузки данных, используемой в Next.js, лежат асинхронные функции и API Fetch. В результате при освоении Next.js у разработчика не возникает ощущения того, что ему необходимо изучить GraphQL для того чтобы пользоваться возможностями фреймворка на полную мощность.
- В Next.js есть встроенная поддержка TypeScript, в то время как в Gatsby то же самое реализуется с помощью особого плагина, который даже не выполняет проверку типов (для этого нужен отдельный плагин). При переводе проекта на Next.js это привело к появлению некоторых проблем, выражавшихся в виде ошибок компиляции, так как я даже не знал о том, что в проекте не всё благополучно с типами.
Благодаря тому, что в Next.js улучшена поддержка SSG, этот фреймворк стал мощным инструментом, позволяющим, на уровне каждой отдельной страницы, выбирать метод работы с ней. Это может быть SSR, SSG или CSR.
На самом деле, если бы я мог сгенерировать это приложение в полностью статическом виде, то Next.js подошёл бы мне лучше, так как я мог бы использовать стандартный JS-API Algolia и мог бы держать код для загрузки данных в том же файле, что и код компонента. Так как у Algolia нет встроенного API GraphQL, и не существует Gatsby-плагина для Algolia, реализация такого механизма в Gatsby потребовала бы добавление этого кода в новый файл. А это идёт вразрез с интуитивно понятным декларативным способом описания страниц.
О дополнительных путях повышения производительности проекта
После того, как мы решили задачу выбора фреймворка, можно отметить, что существуют и дополнительные способы улучшения производительности проекта, не связанные с фреймворком. Эти улучшения вполне могут приблизить к 100 оценку проекта в Lighthouse.
- В мартовской рассылке Algolia рекомендовано добавлять подсказку
preconnect
для дальнейшего повышения скорости выполнения запросов. (Правда, к сожалению, в рассылке приведён неправильный фрагмент кода. Вот правильный код.) - Статические файлы следует кэшировать навсегда. Сюда входят JS- и CSS-файлы, сгенерированные webpack-конфигурацией Gatsby. В документации Gatsby есть отличная статья на эту тему. Кроме того, даже имеются плагины для генерирования подобных файлов, рассчитанных на Netlify и Amazon S3. Нам, для Firebase Hosting, к сожалению, пришлось писать собственный плагин.
- Мы используем в приложении JPEG- и PNG-изображения, загруженные создателями стартапов. Мы их не сжимаем и не оптимизируем. Улучшение данного аспекта нашего приложения представляется достаточно сложной задачей и выходит за рамки этого проекта. Кроме того, было бы просто замечательно, если бы все эти изображения конвертировались бы в формат WebP. В результате нам пришлось бы хранить изображения, используя лишь один высокоэффективный графический формат. К сожалению, как и в случае с многими другими возможностями PWA, команда разработчиков Safari WebKit затягивает с поддержкой WebP. Сейчас Firefox — это единственный из основных браузеров, который не поддерживает этот формат.
Итоги
Если в двух словах обобщить то, о чём мы тут говорили, то можно сказать следующее:
- Вывод «скелетного» варианта страницы в процессе загрузки данных клиентом создаёт у пользователя ощущение более быстрой работы сайта, чем в случае, когда пользователь смотрит на пустую страницу во время загрузки данных сервером.
- Gatsby-вариант сайта оказался лишь немного быстрее Next.js-варианта. Однако система плагинов Gatsby и качественная документация проекта повышают удобство этого фреймворка для разработчика.
Уважаемые читатели! Пользуетесь ли вы генераторами статических сайтов или системами серверного рендеринга для ускорения своих проектов?
Автор: ru_vds