- PVSM.RU - https://www.pvsm.ru -
Сегодня мы публикуем перевод рассказа о том, как переход с React Boilerplate [1] на Next.js [2], фреймворк для разработки прогрессивных веб-приложений, основанный на React, позволил ускорить загрузку домашней страницы [3] проекта manifold.co в 7.5 раз. Другие изменения в проект не вносили, и этот переход, в общем-то, оказался совершенно незаметным для других частей системы. То, что получилось в итоге, оказалось даже лучшим, чем ожидалось.
Фактически, мы можем говорить о том, что переход на Next.js дал нам нечто вроде «взявшегося из ниоткуда увеличения производительности проекта». Вот как выглядит время загрузки проекта при использовании различных аппаратных ресурсов и сетевых соединений.
Соединение | Процессор | До, секунд | После, секунд | Улучшение, % |
Быстрое (200 Мбит/с) | Быстрый | 1.5 | 0.2 | 750 |
Среднее (3G) | Быстрый | 5.6 | 1.1 | 500 |
Среднее (3G) | Средний | 7.5 | 1.3 | 570 |
Медленное (медленное 3G-соединение) | Средний | 22 | 4 | 550 |
При использовании быстрого соединения и устройства с быстрым процессором время загрузки сайта упало с 1.5 с. до 0.2 с., то есть этот показатель улучшился в 7.5 раз. На соединении среднего качества и на устройстве со средней производительностью время загрузки сайта упало с 7.5 с. до 1.3 с.
Для того чтобы понять особенности работы прогрессивных веб-приложений (Progressive Web App, PWA), нужно сначала разобраться с тем, что происходит между моментом, когда пользователь переходит по URL (по адресу нашего сайта), и моментом, когда он видит что-то в окне браузера (в данном случае — наше React-приложение).
Стадии работы с приложением
Рассмотрим 5 стадий работы с приложением, схема которых приведена выше.
Так как прогрессивное веб-приложение берёт React-код и выдаёт статический HTML и CSS-код, это означает, что пользователь видит React-приложение уже на шаге 3 вышеприведённой схемы, а не на шаге 5. В наших тестах это занимает 0.2-4 секунды, что зависит от скорости соединения пользователя с интернетом и от его устройства. Это куда лучше, чем предыдущие 1.5-22 секунды. Прогрессивные веб-приложения — это надёжный способ быстрее доставлять пользователю React-приложения.
Причина, по которой прогрессивные веб-приложения и соответствующие фреймворки наподобие Next.js всё ещё не пользуются широчайшей популярностью, заключается в том, что, традиционно, JS-фреймворки не особенно преуспевают в генерировании статического HTML-кода. Сегодня же всё очень сильно изменилось благодаря тому, что такие фреймворки, как React, Vue и Angular, да и другие, обладают отличной поддержкой средств серверного рендеринга. Однако для того, чтобы этими средствами воспользоваться, всё ещё нужно глубокое понимание особенностей работы бандлеров и средств сборки проектов. Работа со всем этим не лишена проблемных моментов.
Недавнее появление PWA-фреймворков, вроде Next.js и Gatsby (оба появились в конце 2016 — начале 2017) стало серьёзным шагом в сторону широкого принятия PWA благодаря снижению входных барьеров и благодаря тому, что это сделало использование подобных фреймворков простым и приятным занятием.
Хотя не каждое приложение можно перевести на Next.js, для многих React-приложений такой переход означает ту самую «производительность из ниоткуда», о которой мы тут говорим, дополненную ещё и более эффективным использованием сетевых ресурсов.
В целом можно отметить, что перевод нашей домашней страницы на Next.js не был очень уж тяжёлым. Однако мы столкнулись с некоторыми сложностями, которые были вызваны особенностями архитектуры нашего приложения.
Нам пришлось отказаться от маршрутизатора React так как в Next.js имеется собственный встроенный маршрутизатор, который лучше сочетается с оптимизациями, касающимися разделения кода, выполняемыми поверх архитектуры PWA. Это позволяет данному маршрутизатору обеспечить гораздо более быструю загрузку страниц, чем можно ожидать от любого маршрутизатора, работающего на стороне клиента.
Маршрутизатор Next.js — это нечто вроде высокоскоростного маршрутизатора React, но это, всё таки, не маршрутизатор React.
На практике, так как мы не пользовались особенно продвинутыми возможностями, которые предлагает маршрутизатор React, для нас переход на маршрутизатор Next.js заключался в простой замене стандартного компонента маршрутизатора React на соответствующий компонент Next.js:
/* Старый код (Маршрутизатор React) */
<Link to="/my/page">
A link
</Link>
/* Новый код (Маршрутизатор Next.js) */
<Link href="/my/page" passHref>
<a>
A link
</a>
</Link>
В общем-то, всё оказалось не так уж и плохо. Нам пришлось переименовать свойство и добавить тег для целей серверного рендеринга. Так как мы, кроме того, пользовались библиотекой styled-components
, оказалось, что нам, в большинстве экземпляров, понадобилось добавить свойство passHref
для того, чтобы обеспечить такое поведение системы, при котором href
всегда указывает на сгенерированный тег.
Сетевые запросы для manifold.co
Для того чтобы своими глазами увидеть оптимизации маршрутизатора Next.js в действии, откройте вкладку Network инструментов разработчика браузера, просматривая страницу manifold.co [3], и щёлкните по какой-нибудь ссылке. На предыдущем рисунке показан результат щелчка по ссылке /services
. Как видно, он приводит к выполнению запроса на загрузку services.js
вместо выполнения обычного запроса.
Я не говорю только о маршрутизации на стороне клиента, для решения этой задачи подходит и маршрутизатор React. Я говорю о реальном фрагменте JavaScript-кода, который был выделен из остального кода и загружен по запросу. Это делается стандартными средствами Next.js. И это — гораздо лучше, чем то, что у нас было раньше. А именно, речь идёт о большом пакете JS-кода размером в 1.7 Мб, который клиенту, прежде чем он мог что-то увидеть, нужно было загрузить и обработать.
Хотя представленное здесь решение и не идеально, оно гораздо ближе, чем предыдущее, к идее, в соответствии с которой пользователи загружают код только для тех страниц, которые они просматривают.
Продолжая тему сложностей, связанных с переходом на Next.js, можно отметить, что все те интересные оптимизации, которым Next.js подвергает приложение, оказывают определённое влияние на это приложение. А именно, так как Next.js выполняет разделение кода на уровне страниц, он не позволяет разработчику обращаться к корневому компоненту React
или к методу render()
библиотеки react-dom
. Если вы уже занимались настройкой Redux, то вы можете отметить, что всё это говорит нам о том, что для нормальной работы с Redux нам нужно решить проблему, которая заключается в том, что неясно, где именно нужно искать Redux.
В Next.js предусмотрена специальный компонент высшего порядка, withRedux
, играющий роль обёртки для всех компонентов верхнего уровня на каждой странице:
export default withRedux(HomePage);
Хотя всё это не так уж и плохо, но если вам нужны методы createStore()
, как, например, при использовании redux-reducer-injectors [6], рассчитывайте на то, что вам понадобится дополнительное время на отладку обёртки (и, кстати, постарайтесь никогда не пользоваться чем-то вроде redux-reducer-injectors
).
Кроме того, из-за того, что теперь Redux представляет собой «чёрный ящик», использование с ним библиотеки Immutable [7] становится проблематичным. Хотя то, что Immutable будет работать с Redux, кажется вполне очевидным, я столкнулся с проблемой. Так, либо состояние верхнего уровня не было иммутабельным (ошибка get is not a function
), либо компонент-обёртка пытался использовать точечную нотацию для работы с JS-объектами вместо метода .get()
(ошибка Can’t get catalog of undefined
). Для отладки этой проблемы мне пришлось обращаться к исходному коду. В конце концов, Next.js заставляет разработчика пользоваться его собственными механизмами не без причины.
В целом же можно отметить, что главной проблемой, связанной с Next.js является то, что очень немногое в этом фреймворке хорошо документировано. В документации [8] имеется множество примеров, на основе которых можно создать что-то своё, но если среди них нет такого, который отражает особенности вашего проекта, вам можно лишь пожелать удачи.
Мы использовали библиотеку react-inlinesvg [9], которая предлагает возможности по стилизации встроенных SVG-изображений и по кэшированию запросов. Но тут у нас возникла одна проблема: при выполнении серверного рендеринга нет такого понятия, как XHR-запросы (по крайней мере, не в смысле URL, генерируемых Webpack, как того можно было бы ожидать). Попытки выполнения таких запросов препятствуют серверному рендерингу.
Хотя существуют и другие библиотеки для работы со встроенными SVG-данными, которые поддерживают SSR, я решил отказаться от этой возможности, так как SVG-файлы, всё равно, использовались редко. Я либо заменил их обычными изображениями, тегами <img>
, в том случае, если при выводе соответствующих изображений не нужна была стилизация, либо встроил их в код в виде React JSX. Вероятно, так всё стало только лучше, так как JSX-иллюстрации теперь попадали в браузер при первоначальной загрузке страницы и в JS-бандле, отправляемом клиенту, было на 1 библиотеку меньше.
Если же вам необходимо пользоваться механизмами загрузки данных (мне эта возможность понадобилась для другой библиотеки), то вы можете это настроить с помощью next.config.js
, используя whatwg-fetch
и node-fetch
:
module.exports = {
webpack: (config, options) =>
Object.assign(config, {
plugins: config.plugins.concat([
new webpack.ProvidePlugin(
config.isServer
? {}
: { fetch: 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch' }
),
]),
resolve: Object.assign(config.resolve, {
alias: Object.assign(
config.resolve.alias,
config.isServer ? {} : { fetch: 'node-fetch' }
),
}),
}),
};
Последняя особенность Next.js, о которой тут хотелось бы упомянуть, заключается в том, что этот фреймворк запускается дважды — один раз для сервера, и ещё раз — для клиента. Это немного размывает границу между клиентским JavaScript и Node.js-кодом в одной и той же кодовой базе, вызывая необычные ошибки, наподобие fs is undefined
при попытке воспользоваться возможностями Node.js на клиенте.
В результате приходится сооружать такие вот конструкции в next.js.config
:
module.exports = {
webpack: (config, options) =>
Object.assign(config, {
node: config.isServer ? undefined : { fs: 'empty' },
}),
};
Флаг config.isServer
в Webpack станет вашим лучшим другом в том случае, если один и тот же код нужно запускать в разных окружениях.
Кроме того, Next.js поддерживает, в дополнение к стандартным методам жизненного цикла компонентов React, метод getInitialProps()
, который вызывается только при работе кода в серверном режиме:
class HomePage extends React.Component {
static getInitialProps() {
// Это вызывается только при первом проходе серверного рендеринга
}
componentDidMount() {
// Это вызывается только на клиенте, при монтировании компонента
}
…
}
Да, и не будем забывать о том, что наш хороший друг, объект window
, необходимый для организации прослушивания событий, для определения размеров окна браузера и дающий доступ к множеству полезных функций, в Node.js недоступен:
if (typeof window !== 'undefined') {
// Пожалуйста, позволь мне работать с `window` не устроив тут полный беспорядок
}
Надо отметить, что даже Next.js не способен избавить разработчика от необходимости решения проблем, связанных с выполнением одного и того же кода и на сервере и на клиенте. Но при решении подобных задач весьма полезными оказываются config.isServer
и getInitialProps()
.
В краткосрочной перспективе фреймворк Next.js отлично соответствует, в плане производительности, нашим требованиям к серверному рендерингу и к возможности просматривать наш сайт на устройствах, на которых отключён JavaScript. К тому же, теперь он позволяет использовать расширенные (rich) мета-теги [10].
Возможно, в будущем мы рассмотрим другие варианты, в том случае, если наше приложение будет нуждаться и в серверном рендеринге и в более сложной серверной логике (например, мы присматриваемся к возможности реализовать технологию единого входа на сайтах manifold.co и dashboard.manifold.co). Но до тех пор мы будем пользоваться Next.js, так как этот фреймворк, при небольших временных затратах, принёс нам огромные выгоды.
Уважаемые читатели! Используете ли вы Next.js в своих проектах?
Автор: ru_vds
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/311354
Ссылки в тексте:
[1] React Boilerplate: https://github.com/react-boilerplate/react-boilerplate
[2] Next.js: https://github.com/zeit/next.js/
[3] домашней страницы: https://manifold.co/
[4] Image: https://habr.com/ru/company/ruvds/blog/442654/
[5] асинхронно: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#Notes
[6] redux-reducer-injectors: https://github.com/GuillaumeCisco/redux-reducers-injector
[7] Immutable: https://facebook.github.io/immutable-js/
[8] документации: https://github.com/zeit/next.js/tree/canary/examples/
[9] react-inlinesvg: https://github.com/gilbarbara/react-inlinesvg
[10] мета-теги: https://css-tricks.com/essential-meta-tags-social-media/
[11] Image: https://ruvds.com/ru-rub/#order
[12] Источник: https://habr.com/ru/post/442654/?utm_source=habrahabr&utm_medium=rss&utm_campaign=442654
Нажмите здесь для печати.