Улучшение FCP веб-сайта, созданного с помощью React

Вступление

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

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

Оптимизация

Сначала мы перестроили весь веб-сайт как приложение CSR (Client Side Rendered). Первоначальная задача выполнена. Теперь приступим к оптимизации. Мы сделали несколько уровней оптимизации для нашего приложения CSR. Эти оптимизации также хороши для приложения SSR (Server Side Rendered), которое мы создадим позже в этой статье.

Оптимизация для версии с рендерингом на стороне клиента (CSR)

GZip

Мы создали производственную сборку нашего приложения, а также сделали gzip-версию каждого из них, а затем обслужили их с помощью Nginx. Для обслуживания сжатого содержимого нам потребуется следующая конфигурация Nginx.

# Following block is required if you want to redirect all HTTP request to HTTPS
server {
 listen 80 default_server;
 listen [::]:80 default_server;
 server_name _;
 return 301 https://$host$request_uri;
}
# Following block is required to redirect www.example.com to example.com
server {
 listen 443 ssl http2;
 listen [::]:443 ssl http2;
 server_name www.example.com;
 return 301 https://example.com$request_uri;
 ssl_certificate /etc/nginx/ssl/server.crt;
 ssl_certificate_key /etc/nginx/ssl/server.key;
 ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
}
# This is our actual block
server {
 gzip on;
 gzip_static on; 
 gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
 gzip_proxied any;
 gzip_vary on;
 gzip_comp_level 6;
 gzip_buffers 16 8k;
 gzip_http_version 1.1;
 listen 443 ssl http2;
 listen [::]:443 ssl http2;
 Server_name example.com;
 root /usr/share/nginx/html;
error_page 500 502 503 504 /50x.html;
ssl_certificate /etc/nginx/ssl/server.crt;
 ssl_certificate_key /etc/nginx/ssl/server.key;
 ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
# In your react application if you used a router (i.e. react-router) then you have
# to tell the nginx to serve your index.html file for any request.
 location / {
  index index.html index.htm;
  try_files $uri $uri/ /index.html;
 }
 location = /50x.html {
  root /usr/share/nginx/html;
 }
}

Компоненты с отложенной загрузкой

Поскольку у приложения есть несколько разных маршрутов, функция отложенной загрузки response будет действительно полезна. Возможно, вы уже знаете о ленивой загрузке компонентов React. Если нет, то вам следует посмотреть исходную документацию об этом здесь. Использовать это довольно просто. Именно так:

const OtherComponent = React.lazy(() => import(‘./OtherComponent’));
function MyComponent() {
 return (
 <div>
  <Suspense fallback={<div>Loading…</div>}>
   <OtherComponent />
  </Suspense>
 </div>
 );
}

Одно небольшое напоминание, если вы думаете использовать ленивую загрузку. Не слишком увлекайтесь этим и начните лениво загружать все свои компоненты. Если вы не знаете, что делаете, это замедлит загрузку вашего сайта, а не ускорит его. Вот пример сценария, когда ленивая загрузка будет плохой. Предположим, у вас есть MyComponent из нашего предыдущего примера, и он лениво отображает OtherComponent. Если OtherComponent также лениво (!) Загружает «AnotherComponent», то будет цепочка компонентов с отложенной загрузкой, следовательно, цепочка запросов на выборку требуемых сценариев JS, которые будут выполнять дела обстоят хуже.

Вывод версии CSR

С помощью GZip и Lazy Loading Components мы сделали сайт немного быстрее оригинального. Но этого было мало. Нам не удавалось сделать первую содержательную отрисовку и первую значимую отрисовку менее 3,7 секунд! Следовательно, мы стремились к рендерингу на стороне сервера, чтобы улучшить время первой отрисовки содержимого!

Версия с рендерингом на стороне сервера (SSR)

Рендеринг на стороне сервера обычно является устаревшим решением. До появления фреймворков JavaScript, таких как Angular, ReactJS, VueJS, все веб-приложения в основном рендерились на сервере.

Подождите, что? Мы идем назад?

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

Преимущества SSR

Итак, какие преимущества мы получим, если выберем решение SSR. Это только улучшение времени FCP? Или есть какие-то другие преимущества, которые побуждают нас немного изучить решение?

Сначала позвольте мне объяснить здесь реальную ситуацию. Когда браузер запрашивает приложение CSR, он получает минимальную версию HTML. Поэтому, если ваше приложение не сильно изменило макет для нескольких маршрутов и вам действительно не нужны ссылки для совместного использования в социальных сетях для предварительного просмотра содержимого страницы, тогда вам, вероятно, не понадобится решение SSR. Потому что с SSR браузер получит HTML-код большего размера с некоторым содержимым, связанным с запрошенным маршрутом. Но вы также сможете добавить еще несколько мета-тегов, специфичных для запрошенных маршрутов. Таким образом, ваши ссылки для совместного использования в социальных сетях будут лучше отображаться в виде эскизов. Для нашей ситуации это была огромная выгода.

Преобразование приложения CSR React в универсальное приложение

В React уже есть замечательный механизм для этой цели. В ReactDOM помимо метода render есть еще один метод - hydrate. Это метод, указывающий React не повторно отображать то, что уже визуализировано ReactDOMServer. Вместо этого React попытается прикрепить прослушиватели событий к существующей разметке.

Есть несколько хороших и популярных фреймворков реагирования, которые используют эту функцию и поддерживают SSR из коробки. Некоторые из хороших упоминаний включают NextJS, GatsbyJS. Выбираем для себя NextJS. Почему именно NextJS? Это выходит за рамки данной статьи.

К тому времени, когда мы решили сделать наше приложение универсальным, React также выпустил свой Context API, который заставляет нас также избавиться от сторонней библиотеки redux. Таким образом, сохраняется еще 22 КБ. Я не совсем ненавижу Redux. Нам это просто не было нужно, поскольку мы использовали его только как глобальный магазин.

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

Ограничьте использование «окна» и «локального магазина»

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

Получение данных из API

Для приложения реакции CSR мы использовали для выборки данных один из методов жизненного цикла «componentDidMount». Что ж, мы также можем сделать это для приложения SSR, но тогда нам будет не хватать фактического содержимого при первой отрисовке. Следовательно, мы упустим одно важное преимущество, и социальный обмен не будет генерировать миниатюры с значимой информацией.

Итак, что нам нужно сделать, это получить данные на сервере и затем передать их компонентам, которым они необходимы. NextJS (а также AfterJS) использует статическую функцию для каждого компонента страницы для извлечения данных на сервере, а также в браузере, если изменение страницы происходит в браузере. Функция называется «getInitialProps». Следующий сценарий вызывает getInitialProps и затем передает данные, возвращаемые им, компонентам в качестве свойств.

Если вы использовали API выборки браузера, вам также понадобится полифил для узла (сервера). Вы можете использовать что-то вроде isomorphic-unetch, которые имеют одинаковый API для обоих браузеров (полифилл, если в браузере уже нет fetch API) и сервер узла. Если вы использовали axios, то все в порядке, потому что axios поддерживает и то, и другое.

Отсрочка блокировки рендеринга некритического CSS

Вы когда-нибудь слышали о критическом CSS или CSS с блокировкой рендеринга? Когда мы добавляем на страницу таблицу стилей, как показано ниже, браузер запрашивает CSS с запросом на блокировку рендеринга. Это означает, что рендеринг страницы будет задерживаться до тех пор, пока он не получит CSS и не проанализирует его.

<link re=”stylesheet” src=”example.com/example.ccss”/>

Так что первая краска будет отложена. Так как мы это исправим?

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

Привет! Неужели это кому-то хотелось бы увидеть?

К счастью, мы можем преодолеть эту неприятную ситуацию, написав критически важный CSS inline. Критические CSS - это те, которые необходимы при первой отрисовке, чтобы фиксировать макет во время загрузки других ресурсов (данных, некритических CSS, файлов JS и т. Д.).

Мы добавили на страницу наш css, как показано ниже:

<noscript id=”deferred-styles”>
 <link rel=”stylesheet” type=”text/css” href={`/static/styles/all.min.css?${process.env.HASH_VALUE}`} />
</noscript>

Затем добавьте следующие фрагменты в собственный файл _app.js NextJS.

let loadDeferredStyles = function () {
 console.log(‘defering css load’);
 let addStylesNode = document.getElementById(‘deferred-styles’);
 if (addStylesNode) {
  let replacement = document.createElement(‘div’);
  replacement.innerHTML = addStylesNode.textContent;
  document.body.appendChild(replacement);
  addStylesNode.parentElement.removeChild(addStylesNode);
 }
};
if (typeof window !== ‘undefined’) {
 let raf = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
 window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
 if (raf) raf(function () {
  window.setTimeout(loadDeferredStyles, 0);
 });
 else window.addEventListener(‘load’, loadDeferredStyles);
}

Это отложит загрузку CSS, и браузер не будет ждать CSS перед рендерингом.

Использование свойства CSS font-display

Свойство CSS font-display позволяет вам контролировать, как веб-шрифты заменяются системными шрифтами во время / после загрузки. Если вы загружаете большой объем данных шрифта с помощью @ font-face, тогда будет задержка (до нескольких секунд), когда ваш контент будет пустым в ожидании загрузки шрифтов. Мы можем изменить это так, чтобы резервный шрифт загружался сразу же, а затем заменялся вашими веб-шрифтами после их загрузки. (имейте в виду, что ваши шрифты могут иметь разные размеры и заставлять вещи прыгать при загрузке).

Конфигурация Nginx для решения SSR

Нам нужно изменить предыдущую конфигурацию nginx для решения SSR. Раньше у нас было все статическое содержимое. Но теперь у нас есть сервер NodeJS. Итак, нам нужно настроить nginx как обратный прокси.

# Following block is required if you want to redirect all HTTP request to HTTPS
server {
 listen 80 default_server;
 listen [::]:80 default_server;
 server_name _;
 return 301 https://$host$request_uri;
}
# Following block is required to redirect www.example.com to example.com
server {
 listen 443 ssl http2;
 listen [::]:443 ssl http2;
 server_name www.example.com;
 return 301 https://example.com$request_uri;
 ssl_certificate /etc/nginx/ssl/server.crt;
 ssl_certificate_key /etc/nginx/ssl/server.key;
 ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
}
# This is our actual block
server {
 # gzip
 gzip on;
 gzip_static on;
 gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript application/atom+xml image/svg+xml;
 gzip_proxied any;
 gzip_vary on;
 gzip_comp_level 6;
 gzip_buffers 16 8k;
 gzip_http_version 1.1;
# brotli
 brotli on;
 brotli_comp_level 6;
 brotli_types text/xml image/svg+xml application/x-font-ttf image/vnd.microsoft.icon application/x-font-opentype application/json font/eot application/vnd.ms-fontobject application/javascript font/otf application/xml application/xhtml+xml text/javascript application/x-javascript text/plain application/x-font-truetype application/xml+rss image/x-icon font/opentype text/css image/x-win-bitmap;
listen 443 ssl http2;
 listen [::]:443 ssl http2;
 server_name example.com;
 ssl_certificate /etc/nginx/ssl/server.crt;
 ssl_certificate_key /etc/nginx/ssl/server.key;
 ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
location / {
 http2_push_preload on;
 # default port, could be changed if you use next with custom server
 proxy_pass http://example.com:3000;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;
 proxy_set_header Connection ‘upgrade’;
 proxy_set_header Host $host;
 proxy_cache_bypass $http_upgrade;
}
location /service-worker.js {
  add_header Cache-Control “no-store, no-cache, must-revalidate, proxy-revalidate”;
  proxy_pass http://example.com:3000;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection ‘upgrade’;
  proxy_set_header Host $host;
  proxy_cache_bypass $http_upgrade;
 }
location /static/ {
  expires 2d;
  http2_push_preload on;
  # default port, could be changed if you use next with custom server
  proxy_pass http://example.com:3000;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection ‘upgrade’;
  proxy_set_header Host $host;
  proxy_cache_bypass $http_upgrade;
 }
location /_next/ {
  expires 2d;
  http2_push_preload on;
  # default port, could be changed if you use next with custom server
  proxy_pass http://example.com:3000;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection ‘upgrade’;
  proxy_set_header Host $host;
  proxy_cache_bypass $http_upgrade;
 }
}

📝 Прочтите этот рассказ позже в Журнале.

👩‍💻 Просыпайтесь каждое воскресное утро и слушайте самые интересные истории из области технологий, ожидающие вас в вашем почтовом ящике. Прочтите информационный бюллетень« Примечательно в технологиях .