В инструментах разработчика браузера хром есть вкладка «Audit». На ней расположился инструмент который называется Lighthouse, служит он для анализа насколько хорошо сделано веб приложение.

image

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

Если у вас нету возможности установить хром, то можно поставить lighthouse из npm и работать с ним из консоли.

В статье я не стал сопоставлять каждую рекомендацию с конкретным разделом, вместо этого я разбил разделы по решениям которые я применил и которые понравились Ligthouse. Это далеко не все что он рекомендует, это только самое интересное. Остальные рекомендации очень просты, а такие как SEO всем уже давно хорошо знакомы.

Performance


Выбор сервера


Это самый банальный совет, но именно это является фундаментом для всей производительности. К счастью найти хорошее решение просто, это любой ЦОД уровня Tier 3 или Tier 4. Сам этот статус ничего не говорит о скорости, он говорит о том что владельцы позаботились о качестве.

Инициализация приложения


Когда то в браузерах был только html. Потом появился javascript и бизнес логика. Сегодня логики на клиенте стало настолько много, что html с ней не справляется и стал вовсе не нужен. Но, т.к. браузер не может начать загружаться из JavaScript файла, нам придется разместить небольшой кусок html для запуска нашего приложения.

В идеале он должен выглядеть примерно так:

<!DOCTYPE html>
<html lang="ru">
<head>
    <title>Название сайта</title>

    <link rel="manifest" href="./manifest.webmanifest">
    <link rel="shortcut icon" href="content/images/favicon.ico" type="image/x-icon">

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width" />
    <meta name="theme-color" content="#425566">
    <meta name="Description" content="описание сайта">
</head>
<body>
    <div id="loader">
        loading
    </div>
    <script async>
        // todo: попозже добавим скрипты загрузки
    </script>
</body>
</html>

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

В данной статье не рассматриваем оптимизацию для ботов, но скажу что проще всего отловить конкретного бота и отдать то что конкретному боту нужно. Бот гугла сам всё поймет из контента который подгрузится позже.

Использовать сплеш скрин


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

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

image

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

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

И сразу спешу расстроить тех кто думает что сплеш скрином можно обмануть Lighthouse и разместить тяжелое приложение за ним. Он все видит, и не выдаст вам хорошую оценку за тяжелое приложение.

Инициализация приложения


Теперь, когда мы отвлекаем внимание пользователя картинками, настало время загрузить приложение. Для этого мы внутрь блока script вставим следующий скрипт.

// 1. Подключаем ServiceWorker, подробнее рассмотрим в блоке PWA
if (navigator.serviceWorker && !navigator.serviceWorker.controller) {
    navigator.serviceWorker.register('pwabuider-sw.js', { scope: './' });
}

// 2. Подключаем необходимые стили
[
    "./content/font.css",
    "./content/grid.css"
].forEach(function(url){
    var style = document.createElement("link");
    style.href = url;
    style.rel = "stylesheet";
    document.head.appendChild(style);
});

// 3. Подключаем необходимые скрипты
[
    "./scripts/polyfills.min.js", // или vendors.min.js
    "./scripts/main.min.js" // spa приложение
].forEach(function(url){
    const script = document.createElement("script");
    script.src = url;
    script.async = false;
    document.head.appendChild(script);
});

Из чего он состоит:

  1. Подключение PWA — рассмотрим в соответствующем разделе ниже. Подключать его надо как можно раньше, потому что возможно в pwa уже будет все необходимое для работы сайта и запросов на сервер больше не будет.
  2. Подключение стилей — по мере необходимости подключаем стили. В идеале этого кода вообще не должно быть и стили должны подключать ваши компоненты по мере необходимости.
  3. Подключение скриптов — подключаем программу. Состоять он должен всего из двух этих скриптов. Все остальные скрипты (карты, аналитика, библиотеки) не влияющие на отображение первого экрана (не всей страницы) загружаются уже после отрисовки первого экрана приложения. Аналитику уже должен подгрузить компонент аналитики после загрузки программы. Качество аналитики от этого не пострадает, а системы аналитики поддерживают загрузку после загрузки программы. Карты должны погрузиться только после того как пользователь до них доскролит и они попадут в экран. Со сторонними библиотеками, необходимых для работы конкретных компонентов, аналогично.

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

Ленивая подгрузка и отрисовка


Очень важным параметром является то насколько быстро отрисуется первый экран и пользователь сможет начать взаимодействовать с этой страницей. И здесь стоит использовать следующие оптимизации:

1. Ленивая отрисовка. Необходимо отрисовывать только ту часть страницы куда смотрит пользователь, а отрисовку тяжелых компонентов или картинок уже делать когда пользователь к ним доскролился.

Хорошим решением тут может служить компоненты lazy-block и lazy-img:

<div>
   <p>текст</p>
   <lazy-img src="..."/>
</div>
<lazy-block>
   тяжелые блоки
</lazy-block>
<lazy-block>
   тяжелые блоки
</lazy-block>
<lazy-block>
   тяжелые блоки
</lazy-block>

Смысл в том что они буду следить за скролом пользователя и в случае если компонент попадает в область экрана будет отрисовываться. Это можно сравнить с техникой виртуального скрола (пример) который всем хорошо знаком по стенам социальных сетей. Мы можем скролить вечно, а они никогда не тормозят.

Но не стоит забывать и о гугл боте, который видит spa, но не скролит всю страницу. Поэтому если вы не позаботитесь, то он не увидит ваш контент.

2. Если какой либо из компонентов использует внешнюю зависимость, он должен будет загрузить ее сам по мере необходимости. Например это может быть блок с картами, графиками или 3D графикой. А с недавних пор в JS появился способ сделать это очень просто:

class Demo {
    constructor() {
        this.init();
    }
    private async init() {
        const module = await import('./external.mjs'); // динамический импорт
        module.default();
        module.doStuff();
    }
}

В результате пользователь подгружает только то что ему надо, что сильно экономит ресурсы пользователя и сервера.

Минимизация бандла


И… да, вы подумали не о том, речь не о минификации в Terser (UglifyJS), а о том что бы конкретному браузеру отдавать только то что ему нужно.

Дело в том что браузеры постоянно развиваются, у них появляется новое API, разработчики начинают использовать его, а для совместимости со старыми браузерами подключают полифиллы и транспиллеры. В итоге образуется проблема что пользователи с новейшими браузерами, которых около 80%, получают код который предназначен для пользователей IE11, транспиленный и с полифилами.

Проблема этого кода в том что он содержит много лишнего текста, а производительность его в 3 раза меньше (по моим субъективным оценкам) чем оригинала. Гораздо логичнее делать несколько бандлов для разных версий браузеров. Бандл с ES2017 кодом для Chrome 73 с минимумом полифилов, бандл с ES5 для IE11 с максимум полифилов и т.д.

О том как за один раз собрать бандлы разных версий я писал в предыдущей статье. А для выбора правильной версии в браузере немного модифицируем скрипт подключения программы:

var esVersion = ".es2017";
try{
    eval('"use strict"; class foo {}');
}catch(e){
    esVersion = ".es5";
}

[
    "./scripts/polyfills" + esVersion + ".min.js",
    "./scripts/main" + esVersion + ".min.js"
].forEach(function(url){
    const script = document.createElement("script");
    script.src = url;
    script.async = false;
    document.head.appendChild(script);
});

В итоге пользователи современных браузеров получат максимально легкую и производительную программу, а пользователи IE11 получат то что они заслужили.

Еще один интересный способ минификации
Очень интересная библиотека для сокращения бандла на 50%, к сожалению с непредсказуемым результатом.

Минимизация кода


Очень популярна проблема когда разработчики начинают подключать всё на что упадет их взгляд. В результате иногда можно наблюдать программы которые весят по 5-15 мб и даже больше. Поэтому к выбору библиотек надо подходить с умом.

Вместо тяжелых фреймворков вроде Angular или React, лучше выбрать их более легковесные аналоги: vue, preact, mithril и т.п. Они нисколько не уступают своим именитым аналогам, а вот экономия на размере бандла может составить разы.

Избегайте использования тяжелых библиотек. Вместо использование таких библиотек как jquery, lodash, moment, rxjs и любая другая в минифицированном размере >100кб, постарайтесь поглубже изучить алгоритмы и найти решение на нативном JS. Как правило на нативном скрипте можно написать проще, а вы избавляетесь от лишней тяжелой зависимости.

Минификация картинок


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

По этому здесь я приведу очень простой рецепт решения всех проблем с картинками. В основе этого рецепта лежит инструмент обработки и конвертирования изображений Sharp. Выделяется он очень продуманным пайплайном, за счет которого скорость обработки изображений в 30-40 раз выше чем у аналогов. А само время сборки сотен изображений из огромных исходников в разные размеры и форматы сопоставимы со скоростью сборки современного фронтенда.

Для использования Sharp необходимо написать скрипт, я использую его в связке с glob для рекурсивного поиска изображений в директории с исходными картинками, а сам скрипт прячу за утилитой запуск задач gulp. Пример моей сборки:

gulp.task('core-min-images', async () => {
    const fs = require('fs');
    const path = require('path');
    const glob = require('glob');
    const sharp = require('sharp');

    // 1. Рекурсивно получаем список файлов для обработки при помощи утилиты glob
    const files = await new Promise((resolve, reject) => {
        glob('src/content/**/*.{jpeg,jpg,png}', {}, async (er, files) => {
            !er ? resolve(files) : reject(er);
        });
    });

    // 2. Запускаем процесс обработки списка изображений
    let completed = 1;
    await Promise.all(files.map(async (file) => {

        const outFile = file.replace(/^src/, 'www');
        const outDir = path.dirname(outFile);

        // 2.1. Проверяем наличие дирректории для обработанных файлов
        if (!fs.existsSync(outDir)) {
            fs.mkdirSync(outDir, { recursive: true });
        }

        // 2.2. Считываем исходное изображение
        const origin = sharp(file);

        // 2.3. Генерируем изображение в разрешении 1920 по горизонтали с сохранением
        // пропорций и сохраняет в формат в jpg/png и webp с дефолтным качеством (80%)
        const size1920 = origin.resize({ width: 1920 });
        await size1920.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-1920w.$1'));
        await size1920.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-1920w.webp'));

        // 2.4. Аналогично для разрешения 480 по горизонтали
        const size480 = origin.resize({ width: 480 });
        await size480.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-480w.$1'));
        await size480.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-480w.webp'));

        // 2.5. Аналогично для разрешения 120 по горизонтали
        const size120 = origin.resize({ width: 120 });
        await size120.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-120w.$1'));
        await size120.toFile(outFile.replace(/\.(jpeg|jpg|png)$/, '-120w.webp'));

        // 2.6. Это для более интересных логов
        console.log(`Complete image ${completed++} of ${files.length}:`, file);
    }));

});

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

<img src="sample.jpg"/>

То теперь надо писать так:

<picture>
    <source srcset="img/sample-480w.webp" type="image/webp">
    <source srcset="img/sample-480w.jpg" type="image/jpeg"> 
    <img src="img/sample-480w.jpg" alt="Альтернативный текст!">
</picture>

И тогда браузер сам выберет наиболее удобный для него формат. Также этот вариант можно дополнить отзывчивыми картинками:

<picture>
    <source srcset="img/sample-480w.webp, img/sample-960w.webp 2x" type="image/webp">
    <source srcset="img/sample-480w.jpg, img/sample-960w.webp 2x" type="image/jpeg"> 
    <img src="img/sample-480w.jpg" alt="Альтернативный текст!">
</picture>

А с учетом того что теперь можно генерировать картинки на этапе сборки приложения, получается на все картинки будет одинаковый набор форматов и разрешений, а значит мы можете унифицировать эту логику и скрыть за каким нибудь компонентом, например тем же <lazy-img src="img/sample.jpg">.

Минификация стилей


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

Минимизируйте название классов. Огромная длинна вложенных или БЭМ селекторов в стилях плохо влияют на размер вашего приложения. В настоящее время полно инструментов которые на генерируют стили с уникальными селекторами: JSS, Styled Components, CSS Modules.

Минификация дома


Все мы знакомы с html, но мало кто задумывался что это всего лишь простая абстракция над деревом очень сложных объектов. Цепочка наследования для элемента div выглядит следующим образом:

HTMLDivElement -> HTMLElement -> Element -> Node -> EventTarget

И у каждого объекта в этой цепочки от 10 до 100 свойств и методов которые потребляют много памяти. И все это богатство должен учитывать движок DOM для построения картинки которую мы видим. Поэтому старайтесь не использовать лишние элементы в доме.

Минифицируйте HTML. Удаляйте все что вы используете для форматирования html на этапе написания. Дело в том что пробелы которые используются при написании кода, в браузере также превращаются в объекты дома:

TextNode -> Node -> EventTarget

Удаляйте комментарии. Они также являются элементом дома и потребляют много ресурсов:

Comment -> CharacterData -> Node -> EventTarget

Хорошей практикой может служить использование шаблонизаторов на jsx. Дело в том что при компиляции он превращается в нативный js код, который не генерирует пробелов, комментариев и никогда не ошибается в открытии и закрытии тэгов.

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

Фрагмент страницы html
<!-- Фрагмент 1 -->
<div class="">
  <div class="_42ef">
    <div class="_25-w">
      <div class="_17pg">
        <div class="_1rwk">
          <form class=" _129h">
            <div class=" _3d2q _65tb _7c_r _4w79">
              <div class="_5rp7">
                <div class="_1p1t">
                  <div class="_1p1v" id="placeholder-77m1n" style="white-space: pre-wrap;">
                    Напишите комментарий...
                  </div>
                </div>
              </div>
            </div>
            <ul class="_1obb">
              ...li...
            </ul>
          </form>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- Фрагмент 2 -->
<div>
  <div>
    <div class="_3nd0">
      <div class="_1mwp navigationFocus _395 _4c_p _5bu_ _34nd _21mu _5yk1" role="presentation" style="" id="js_u">
        <div class="_5yk2" tabindex="-1">
          <div class="_5rp7">
            <div class="_1p1t" style="">
              <div class="_1p1v" id="placeholder-6t6up" style="white-space: pre-wrap;">
                Что у вас нового?
              </div>
            </div>
            <div class="_5rpb">
              <div aria-autocomplete="list" aria-controls="js_1" aria-describedby="placeholder-6t6up"
                aria-multiline="true" class="notranslate _5rpu" contenteditable="true"
                data-testid="status-attachment-mentions-input" role="textbox" spellcheck="true"
                style="outline: none; user-select: text; white-space: pre-wrap; overflow-wrap: break-word;">
                <div data-contents="true">
                  <div class="" data-block="true" data-editor="6t6up" data-offset-key="6b02n-0-0">
                    <div data-offset-key="6b02n-0-0" class="_1mf _1mj">
                      <span data-offset-key="6b02n-0-0">
                        <br data-text="true">
                      </span>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>


Как видим используется вложенность из десяти элементов, но эта вложенность не выполняет никакой работы. Первый фрагмент всего лишь выводит текст «Напишите комментарий...» и иконки, второй «Что у вас нового?». В результате такого не рационального использования DOM вся производительность шаблонизатора React просто сводится на нет, и сайт становится одним из самых медленных что я знаю.

Progressive Web App


Файл манифеста


PWA позволяет пользоваться вашим веб приложение как нативным приложением. При включении поддержки на сайте в меню браузера появляется кнопка инсталляции вашего сайта на устройство (Windows, Android, iOS), после чего оно начинает вести себя как нативное и работать в режиме оффлайн, и всё это в обход магазинов приложений.

Включить на сайте поддержку PWA на самом деле очень просто. Достаточно включить в html страницы ссылку на файл манифеста. Файл манифеста можно сгенерировать на сайте pwabuilder.com.

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

Сервис воркер


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

Пример кода можно посмотреть там же на pwabuilder.com:

// This is the service worker with the Cache-first network

const CACHE = "pwabuilder-precache";
const precacheFiles = [
  /* Add an array of files to precache for your app */
];

self.addEventListener("install", function (event) {
  console.log("[PWA Builder] Install Event processing");

  console.log("[PWA Builder] Skip waiting on install");
  self.skipWaiting();

  event.waitUntil(
    caches.open(CACHE).then(function (cache) {
      console.log("[PWA Builder] Caching pages during install");
      return cache.addAll(precacheFiles);
    })
  );
});

// Allow sw to control of current page
self.addEventListener("activate", function (event) {
  console.log("[PWA Builder] Claiming clients for current page");
  event.waitUntil(self.clients.claim());
});

// If any fetch fails, it will look for the request in the cache and serve it from there first
self.addEventListener("fetch", function (event) { 
  if (event.request.method !== "GET") return;

  event.respondWith(
    fromCache(event.request).then(
      function (response) {
        // The response was found in the cache so we responde with it and update the entry

        // This is where we call the server to get the newest version of the
        // file to use the next time we show view
        event.waitUntil(
          fetch(event.request).then(function (response) {
            return updateCache(event.request, response);
          })
        );

        return response;
      },
      function () {
        // The response was not found in the cache so we look for it on the server
        return fetch(event.request)
          .then(function (response) {
            // If request was success, add or update it in the cache
            event.waitUntil(updateCache(event.request, response.clone()));

            return response;
          })
          .catch(function (error) {
            console.log("[PWA Builder] Network request failed and no cache." + error);
          });
      }
    )
  );
});

function fromCache(request) {
  // Check to see if you have it in the cache
  // Return response
  // If not in the cache, then return
  return caches.open(CACHE).then(function (cache) {
    return cache.match(request).then(function (matching) {
      if (!matching || matching.status === 404) {
        return Promise.reject("no-match");
      }

      return matching;
    });
  });
}

function updateCache(request, response) {
  return caches.open(CACHE).then(function (cache) {
    return cache.put(request, response);
  });
}

Как видим из кода все ответы от сервера кешируются, но в режиме онлайн кеш не используется. А использоваться они начинают уже когда соединение с сервером пропало. Тем самым пользователь перемещаясь по сайту может и не заметить кратковременного исчезновения интернета, и даже если интернет пропал надолго, у пользователя сохраняется возможность передвигаться по уже закешированным данным.

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

Accessibility


Корректность атрибутов для людей с особыми потребностями


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

  • Используйте достаточно контрастные цвета. По статистике Минздрава 20% людей имеют проблемы со зрением. А плохой контраст сайтов только усложняет им жизнь, а здоровым людям увеличивает утомляемость.
  • Расставте tabindex. Позвольте пользоваться сайтом без мышки и сенсорных устройств. Грамотное расположением переходов с помощью клавиатуры сильно упрощает процесс заполнения форм.
  • Атрибут aria-label на ссылках. Позволяет экранным дикторам зачитывать текст внутри атрибута.
  • Атрибут alt на картинках. Аналогично предыдущему. Кроме того отобразит текст в случае невозможности загрузки картинки.
  • Язык документа. Пометьте тег html атрибутом с языком lang=«код языка». Это поможет вспомогательным инструментам правильно настроиться на работу.

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

Best Practices


Отделите фронтенд приложение от серверного приложения


Во первых, если вы все еще рендерите html на сервере, то перестаньте уже это делать. Перенос процесса рендеринга на клиента на два порядка сокращает нагрузку на сервер и как результат стоимость поддержки серверного приложения. А клиенты получают приложение с мгновенной реакцией на их действия.

Во вторых отделите ваше клиентское SPA приложение от бекенд приложения. Вы же не держите вместе серверное приложение и windows приложение, и андройд приложение, и iOS приложение. Вот и веб приложение уже давно является самодостаточным приложением, которое может работать и без сервера и даже в режиме оффлайн. Самая популярная ошибка что я вижу это когда бекенд фреймворк вроде Spring или Asp.Net занимаются раздачей статики, в том числе собранным SPA приложением. Давно пора перестать так делать и вынести статику и SPA в отдельный микросервис и спрятать за специализированным веб сервером для раздачи статики, например nginx.

image

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

Настройка прокси сервера, HTTP/2, gzip, cache


Ваше бекенд приложение не должно напрямую общаться с клиентом, лучше всего спрятать его за специализированными воротами, например прокси сервером Nginx. И на нем уже настроить всё необходимое для комфортного общения клиентского устройства с сервером.

  • Настройка сертификата SSL. На самом деле установка SSL соединения относительно дорогое удовольствие, но необходимое, так что сделать его лучше на легковесном и быстром прокси сервер, например Nginx. А вот Nginx и Asp.Net Core умеют переиспользовать между собой имеющиеся соединения, что сильно экономит ресурсы.
  • GZIP Включите и настройте сжатие ответов клиенту. Текст хорошо сжимается в десятки раз что экономит время и трафик клиенту.
  • Cache Закешируйте все сжатые ответы. Также можно кешировать все Get, Head запросы к бекенду, что на порядок снижает на него нагрузку.
  • Настройте роутинг и балансировку Грамотно распределите нагрузку между бекенд приложениями.

Опять из-за того что эта часть достойна отдельной большой статьи, не описываю детально весь процесс настройки, но порекомендую сайта для генерации конфига nginx nginxconfig.io.

SEO


Создайте мета теги в html и используйте семантическую разметку


Про это уже все знают, и как правило используют. Поэтому чтобы исправить достаточно посмотреть на список замечаний Lighthouse и поправить.

Конец


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

В этой статье не описывается как с оптимизировать админки, формы и прочий энтерпрайз, но об этом будет вторая часть.

Комментарии (39)


  1. easyman
    17.03.2019 20:44

    когда бекенд фреймворк вроде Spring
    или Asp.Net занимаются раздачей статики

    Результаты тестов производительности dotnet не сходятся с этим утверждением.
    https://www.ageofascent.com/2019/02/04/asp-net-core-saturating-10gbe-at-7-million-requests-per-second/


    1. LabEG Автор
      17.03.2019 21:30

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


      1. easyman
        17.03.2019 23:01

        Вообще-то весь перечисленный Вами функционал есть в коробке.
        Соединения устанавливаются :)
        Файлы читаются, сжимаются, кэшируются и отдаются.
        Для этого надо отконфигурировать приложение в startup.cs


        Насчёт производительность поменьше: на чём именно поменьше?


        1. LabEG Автор
          18.03.2019 07:59

          Только это не конфигурируется, а программируется. А что бы повторить функционал nginx надо много попотеть.
          Производительность в том что С++ программы работают быстрее C# программ, а nginx годами затачивался на производительность отдачи статики.


          1. tuxi
            18.03.2019 09:36

            Неее, для отдачи статики нужна обычно настройка. Nginx прекрасен в другом: кеширование (с кучей настроек), балансер запросов, виртуальные хосты, подмена хидеров при проксировании, плюс некоторые вещи по части ограничения доступа.


    1. tuxi
      17.03.2019 22:31

      Со стороны java оставлю тут это
      Как раз сравнение отдачи статики аппликейшен сервера vs nginx. Тесты не свежие, но тенденция сохраняется и сейчас. Nginx мы любим не только за скорость отдачи статики, но еще и за бесплатную возможность балансировки.


  1. copyhold
    17.03.2019 21:11
    +3

    Прекрасные рекомендации подходящие, к сожалению, только для посадочных страниц не нуждающихся в индексации. Ибо гуглобот имеет версию хрома 42, а значит не загрузит ничего.
    Не говоря уже о попытке поделиться такой страницей в социальных сетях у которых вообще NOSCRIPT.
    Так что серверный рендер все еще ого-го. Впрочем ничто не мешает сделать очень упрощенный серверный рендер — только метатеги и минимальный контент для ботов.


    1. LabEG Автор
      17.03.2019 21:33
      -3

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


      1. Klenov_s
        17.03.2019 22:35
        +3

        А с чего вы взяли, что пользователю нужна ваша анимация и тонны каких-то скриптов вместо простого текста, содержащего нужную ему инфу?


        1. w0den
          18.03.2019 01:01

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


        1. LabEG Автор
          18.03.2019 08:10

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

          Вот для примера два сайта вики и промо. И как вы думаете какой из них выберет пользователь? Там где скучный текст, или там где можно трейлеры посмотреть, музыку послушать, но все на скриптах…


          1. Tenebrius
            18.03.2019 10:13

            Конечно же вики =)
            Если мне интересен фильм, как видео, то я его целиком посмотрю, а не на трейлеры любоваться буду.
            А если я ищу информацию. то вики намного удобнее (и грузится быстрее).


            1. manifest1
              18.03.2019 14:46

              Начинающие программисты как правило делают акцент на разработке разного рода красивостей (анимация, много графики), более опытные программисты акцентируют на функциональности и контенте.
              Где встречал рекомендации по разработке сайтов, в котором говорилось, что главное — это контент. Если сайт будет красивым, то на него зайдут, скажут прикольно, покликают и уйдут навсегда. Также со временем все эти анимации и красивости надоедают пользователям, т.к. мешают сосредоточится на контенте и функциональности


          1. kolyan222
            18.03.2019 11:48
            +2

            Второй мне сперва дольше ждать загрузки пришлось, а потом ещё и скролить, чтоб получить хоть минимальную информацию о фильме.
            Так что определённо первый.


          1. iproger
            20.03.2019 07:04

            Вы не том сайте спрашиваете мнение. Думаю что среди молодежи будет выбор марвела, а среди бородатых программистов вики, тем более таких как на хабре.


        1. LabEG Автор
          18.03.2019 22:15
          -1

          Судя по рейтингу интернет тотально сломан, и вместо html с анимашками надо возвращать epub =)


  1. tuxi
    17.03.2019 22:08
    +2

    Непрерывно используем Lighthouse/PageSpeed Insights для оптимизации. С большинством постулатов соглашусь, особенно по части кол-ва узлов в DOM. Также много профита принесла выдача webp вместо jpg/png (кстати фаерфокс уже тоже поддерживает webp)

    Но вот с этим не соглашусь:

    если вы все еще рендерите html на сервере, то перестаньте уже это делать. Перенос процесса рендеринга на клиента на два порядка сокращает нагрузку на сервер и как результат стоимость поддержки серверного приложения. А клиенты получают приложение с мгновенной реакцией на их действия.
    Во-первых, не очень понятно, что включает в себя «стоимость поддержки серверного приложения», во-вторых: почему она растет то, если рендеринг происходит на стороне сервера перед отдачей клиенту и занимает в худшем случае 20% от всего времени обработки реквеста, а чаще 5%… ну 10% на очень сложных страницах. И это под нагрузкой, и хорошо видно в jprofiler.

    И еще одна из рекомендаций, на которую имхо не стоит обращать внимание по причине невозможности исправлений
    Задайте правила эффективного использования кеша для статических объектов
    image


    1. ainu
      17.03.2019 23:48

      > И еще одна из рекомендаций, на которую имхо не стоит обращать внимание по причине невозможности исправлений

      А вот это как раз решается проще, чем глобальная переопитимизация дерева DOM и сложности JS.

      Скрипты яндекса и гугла загружаются кроном в локальную директорию по ночам и отдаются с нужным кешем (pragma/last-modified etc).

      Ютуб iframe-ы (в нижних частях страницы) скрываются и грузятся в момент прокрутки страницы (таймауты всё равно нагрузят браузер через какое-то время и все равно гуглбот лайтхаусом ловит).

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


      1. tuxi
        18.03.2019 00:13

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

        Насчет отдачи своих копий всего и вся. Нехорошая идея. Теряем профит от использования 304 у новых посетителей.


    1. LabEG Автор
      18.03.2019 08:20

      Серверный рендеринг увеличивает время до Time to Interact. С начало сервер тратит время на рендеринг, потом нарендереное передается клиенту (клиенты любуются белым экраном), потом клиенту передается SPA (что бы у пользователей не мелькал экран между переходами), потом происходит расчет нового дома и гидрация…
      В случае отсутствия серверного рендеринга время до окончания запуска программы сильно меньше, а при повторном заходе вся программа уже будет на клиенте.

      Время жизни кеша тоже полезная фича, хоть и не привычная. Lighthouse рекомендует выставить год. По хорошему для сброса такого кеша надо использовать хеш файла (как делает ангуляр), но я просто использую версии /scripts/main.min.js?v={version} которые расставляются при компиляции статики.


      1. tuxi
        18.03.2019 09:25

        А какое значение tti считается допустимым? Пусть это будет страница продукта, как наиболее подходящая по понятие «лендинг страница»

        /scripts/main.min.js?v={version}

        Речь шла про сторонние ресурсы, а не свои собственные. Хотя пейдж спид ругается и на время жизни 90 дней.


        1. LabEG Автор
          18.03.2019 22:16

          Да, чужие сайты к сожалению с оптимизировать нельзя.


          1. tuxi
            18.03.2019 22:23

            Да не сайты :) А апи яндекса или гугла. На скриншоте видно же на что ругань. Если вы повесите на свой лендинг яндекс.метрику, яндекс.икоммерц, гугл.аналитикс, и не дай Бог еще какие нибудь маркетинговые пиксели — гугл пейдж спид начнет на них ругаться. Даже на родственный гугл.аналитикс


      1. NerVik
        18.03.2019 14:43

        сам гугл рекомендует SSR в своих видео по SEO, заодно там же рассказывают про то, почему и как хром обходит SPA(пусть и очень коротко, но примерно понять его логику становится возможно)
        https://www.youtube.com/user/GoogleWebmasterHelp/playlists?view=50&sort=dd&shelf_id=10


  1. ainu
    17.03.2019 23:51

    Автор, снимаю шляпу. Можете сказать адрес сайта (или в личку кинуть)?


  1. balsoft
    18.03.2019 02:43
    +1

    Я как разработчик (не веб) понимаю, что технологии не стоят на месте, «html не нужен», «сервер-рендеринг не нужен» и всё в таком духе, что это упрощает разработку и развёртывание сайта, что это на 3.5% ускоряет загрузку и что это best practices.

    Но как пользователь — я вас ненавижу. Я не хочу смотреть на глюкавую анимацию загрузки, я хочу получить контент (пускай без css и скриптов, которые всё равно зарезаны noscript). Я хочу получить контент, даже если javascript не работает. Я хочу получить контент, даже если сижу в emacs из tty, и пускай контент будет уродливым — но он должен быть! Показывайте мне рекламу в текстовом виде или картинкой — я не против, но дайте мне контент без javascript и css!

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

    Извините, накопилось.


    1. LabEG Автор
      18.03.2019 08:31

      Ничего страшного, всё то вы описали имеет отношение к кривым рукам, и никак не связано с использованием js вместо html. Просто на js гораздо проще бездумнее выжрать всю память, чем на html =)


      1. dollar
        18.03.2019 09:32

        Пусть сайт хотя бы работает без JS — это решает проблему кривых рук.


      1. balsoft
        18.03.2019 11:27

        Я не против расклада, когда желающие получают контент с помощью прогрессивного приложения на JS с заставками, анимациями и клиент-рендерингом. Только дайте же вы возможность получать контент в чистом HTML! Мне плевать, что он кривовато выглядит, зато меня выбешивает, когда его нет.


  1. iproger
    18.03.2019 05:39

    Я не соглашусь с выражением что необходимо переносить рендер html на клиент. Особенно если в первую очередь подразумевается экономия ресурсов сервера.
    В 2012 на i7-3770 можно было открывать сотни сайтов без какой-либо загрузки cpu + было достаточно 4gb памяти, а если на борту было 8gb то даже не было мыслей что память может закончится.
    В 2019 уже i9-9900k с 16gb при открытии gmail загружается на 20%. Можно создать ситуацию когда открытие сразу нескольких тяжелых сайтов доведет загрузку почти до 100%. А все из-за огромного количества логики которая стала лишней на сервере. Кстати, тот же gmail на raspberry pi 3b+ уже едва можно открыть: загрузка и инициализация занимает более 30 секунд, забирая почти всю доступную память на устройстве. Так что нет, лучше не надо переносить логику с сервера если того не требуют обстоятельства.
    Так же мало кто задумывается про fps в браузере. Да, да, он так же важен как и в играх. Например, сайт Uber Eats использует React (если не ошибаюсь), все рендерится на фронте. Открываешь такой сайт чтобы заказать поесть, выбираешь ресторан, но то и дело картинка какая-то дерганная, глаз постоянно цепляется. А все дело в том что компоненты рендерятся налету и, видимо, без перерыва, что заставляет браузер постоянно перерисовывать страницу. На 165гц мониторе особенно заметно, будто смотришь на кинескоп, как в девяностые. Уверен что рано или поздно разработчики станут обращать внимание на провалы в fps и станут относиться к этому так же как к оптимизации много-мегабайтных картинок.

    Остальные советы как по мне — годные. Отметил бы что начинать надо с сервера. Слабая машина не сможет выполнять работу с любыми оптимизациями. Далее нужно более-менее грамотно настроить nginx (apache стараться избегать). Два этих действия вместе уже дадут 80% оптимизации которой очень не хватает половине всех сайтов.

    Решил добавить пример сайта где соблюден баланс в фичах и быстродействии. DTF.


    1. LabEG Автор
      18.03.2019 08:39

      Сильный жор некоторых программ не связан с js напрямую, а связан с ленью и неграмотностью разработчиков, которые вместо того чтобы подумать 5 минут начинают втыкать тонны библиотек которые делают всю работу за разработчиков.


  1. sinneren
    18.03.2019 08:22

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


    1. LabEG Автор
      18.03.2019 08:42

      Здесь рядом должен быть комментарий. «Зачем мне ждать загрузку подвала, если мне нужен только телефон в шапке» =)
      И ленивая отрисовка на js не выдает белых блоков, белые блоки выдают ленивая подгрузка.


      1. sinneren
        18.03.2019 11:56

        Под белым блоком я имел ввиду белая пустота снизу)


  1. dollar
    18.03.2019 09:51

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


    1. b00
      18.03.2019 22:17

      И даже если свистелки не подтянулись (или сознательно отключены) — с материалом можно работать, в т.ч. поиском. А загрузится он за секунду или за три — плевать.


  1. Lexicon
    18.03.2019 13:50

    Чтобы приблизиться к нестабильной сотне ( потому, что эта штука даже не может генерировать одинаковый результат ), уходит слишком много усилий и многие — на какую-то ерунду.


    Приложение, делающее акцент на дизайн (никаких жутких подзагрузок), фоновые картинки и шрифты обязательны, SSR необходим, как на зло Offline-first используется и полезен.


    На входе:



    Серверный рендеринг с кешированием всего, что "толкается" — делает свое дело.
    Скрипты подгружаются асинхронно; по HTTP/2; сжаты. Бандл поделен на множество модулей.
    CDN отдает изображения в WebP, в 3 размерах и ориентациях.
    Offline-first после первой загрузки.


    Приходится бороться с снижением рейтинга за низкую скорость загрузки шрифтов…
    Гуглом. С CDN гугла. Перенося все на слоупок-локальный сервер.


    Проблемами с кешированием SW (дикий рост TTI), с интерпретацией defer скриптов как обязательных.


    По итогу, в борьбе за последние 10-15 рейтинга(mobile) приходится использовать оптимизации и технологии, которые не сдались даже крупнейшим проектам. Буквально кровью, потом и перебором.


    И вроде все это круто. А приятель рядом открывает страничку с идеальной сотней…
    (у посетителей нормально рендерится)


    Заголовок спойлера



  1. sergeiss
    18.03.2019 16:42

    «В нем не должно быть никакого контента, только код необходимый для инициализации приложения, который подгрузит уже само приложение и контент.» — очень спорное утверждение, без указания условий применимости. Ибо если контента овер-дохрена, и грузится он каждый раз, да еще грузятся реакт и другие прибамбасы, то до отрисовки реальной страницы может проходить несколько секунд — на скоростном интернете (!!!). И это с учетом кэша браузера, т.е. при не первой загрузке. А на первой загрузке вообще бардак полный.

    Это не мои фантазии, а то, с чем я сейчас борюсь :) Хотя, правда, благодаря всем предыдущим наработкам, приведшим к такой проблеме, у меня есть работа… «Нет худа без добра» (с), как гласит народная мудрость :)

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

    ПыСы. На англоязычных сайтах утверждается (если надо, найду ссылки), что будущее за «универсальными приложениями», SPA + SSR (Single Page Application + Server Site Rendering). А именно, это когда начальная версия страницы рендерится на сервере. Затем, пока юзер читает полученное, туда подтягиваются все файлы и данные, после чего начинает работает уже JS. При этом юзер может даже кликать по ссылкам и переходить на другие страницы сайта, не дожидаясь полной загрузки всего и вся. Ну и что также существенно, вся эта конструкция будет работать даже при выключенном JS, пусть бы даже и не столь «красиво», как предполагалось.


  1. paulmann
    18.03.2019 21:56
    +1

    GZIP Включите и настройте сжатие ответов клиенту.

    Кроме GZIP еще включаю Brotli — скорость быстрее, компрессия больше. Ну и прекомпрессию с максимальным уровнем сжатия для статичных файлов. Прекомпрессию для GZIP лучше всего делать через Zopfli c предварительной минификацией кода. image
    Минификация картинок

    Для PNG лучший компрессор в WebP на базе Zopfli — zopflipng.
    Для jpeg остановился на cwebp (меньше всего сбоев и размер файлов получается чуть меньше, чем у других компрессоров). Если жать с потерей качества, то нужно делать проверку на размер исходного файла и если размер меньше 100кб, жать с меньшими потерями или без них, а если больше, то можно поиграться с настройками.