Lazy-load Яндекс.Карт через IntersectionObserver

Веб Штурм
  • производительность
  • IntersectionObserver
  • Core Web Vitals
  • Яндекс.Карты

Виджет Яндекс.Карт — это около 600 КБ JS и 200 КБ CSS на каждую страницу, где она встроена. На типовой странице «Контакты» с одной картой PSI Mobile падает с 90+ до 30-40, TBT превышает 1500 мс. И всё это — ещё до того как пользователь увидел карту глазами. Решение — IntersectionObserver и ленивая инициализация. На ozsm.ru дало Mobile PSI 44 → 99 на главной.

Проблема

Стандартный код подключения Яндекс.Карт у большинства сайтов выглядит так:

<script src="https://api-maps.yandex.ru/2.1/?lang=ru_RU&apikey=YOUR_KEY"></script>
<script>
  ymaps.ready(function () {
    var map = new ymaps.Map('map', {
      center: [53.2415, 50.2212],
      zoom: 14
    });
  });
</script>
<div id="map" style="height: 400px;"></div>

Что не так: скрипт api-maps.yandex.ru/2.1/?lang=ru_RU грузится сразу при загрузке страницы. Это:

Если карта показывается в подвале страницы или на отдельной вкладке — этот вес лежит на каждой странице, где есть <script src="api-maps">. Пользователь до карты может вообще не доскроллить.

Решение: IntersectionObserver + ленивая инициализация

Не подгружаем api-maps.yandex.ru пока пользователь не подскроллил до карты. IntersectionObserver с rootMargin: 200px начинает загрузку за экран до появления — пользователь не видит «пустого места».

<!-- На странице — placeholder без подключения API -->
<div id="ymap-placeholder"
     class="ymap-placeholder"
     style="height: 400px; background: #1a1a1a; position: relative;"
     data-coords="53.2415,50.2212"
     data-zoom="14">
  <noscript>
    <a href="https://yandex.ru/maps/?ll=50.2212,53.2415&z=14">
      Открыть карту в Яндексе
    </a>
  </noscript>
</div>

<script>
  (function () {
    var mapEl = document.getElementById('ymap-placeholder');
    if (!mapEl) return;

    var observer = new IntersectionObserver(function (entries) {
      if (!entries[0].isIntersecting) return;
      observer.disconnect();
      loadYandexMaps();
    }, { rootMargin: '200px' });

    observer.observe(mapEl);

    function loadYandexMaps() {
      var s = document.createElement('script');
      s.src = 'https://api-maps.yandex.ru/2.1/?lang=ru_RU&apikey=YOUR_KEY';
      s.async = true;
      s.onload = function () {
        ymaps.ready(function () {
          var coords = mapEl.dataset.coords.split(',').map(parseFloat);
          var zoom = parseInt(mapEl.dataset.zoom, 10);

          mapEl.id = 'ymap';
          mapEl.style.background = '';

          var map = new ymaps.Map('ymap', {
            center: coords,
            zoom: zoom,
            controls: ['zoomControl', 'fullscreenControl']
          });

          map.geoObjects.add(
            new ymaps.Placemark(coords, {}, { preset: 'islands#redIcon' })
          );
        });
      };
      document.head.appendChild(s);
    }
  })();
</script>

Тридцать строк vanilla JavaScript. Никаких зависимостей.

Что даёт rootMargin: 200px

Опция rootMargin: '200px' говорит IntersectionObserver: «начни загружать карту, когда до её появления остаётся 200 пикселей скролла». На типовой мобильной странице это около половины экрана — пользователь не успевает заметить, как карта появилась.

Без rootMargin карта начинает загружаться в момент попадания в viewport — есть шанс увидеть «пустой блок 1-2 секунды», пока скрипт парсится.

Дополнительный fix: prefetch DNS

Можно ещё ускорить — за момент до подгрузки сделать DNS-prefetch для api-maps.yandex.ru:

<!-- В <head> — без блокировки -->
<link rel="dns-prefetch" href="https://api-maps.yandex.ru">
<link rel="preconnect" href="https://api-maps.yandex.ru" crossorigin>

DNS-prefetch — это уже разрешённый адрес в кэше браузера, а preconnect — открытое TCP-соединение. Когда IntersectionObserver сработает, скрипт начнёт скачиваться сразу, без 100-300 мс на handshake.

Результаты на ozsm.ru

На главной ozsm.ru карта стояла в самом подвале (контактный блок). Цифры до и после:

Метрика на главнойДоПосле
PSI Mobile Performance4499
TBT1 108 мс89 мс (-92%)
Bundle JS на старте~520 КБ~50 КБ (gzip)
Bootup time2,4 с0,6 с
Время до карты при скроллемгновенно+300 мс (незаметно)

Прибавка к PSI на 55 пунктов — не одна эта правка, конечно. Но lazy-карты — самый дешёвый из десятка фиксов спринта (полтора часа кода против десяти дней на критический CSS, дедуп jQuery, WebP и прочее).

Гочи

Несколько вещей, на которые потратили время и которые стоит знать заранее:

Что мы из этого вынесли

  1. Не грузите то, до чего пользователь не доскроллит. Это базовое правило для всех тяжёлых виджетов: карты, видео-плееры, чаты, embed’ы соцсетей.
  2. IntersectionObserver работает в проде. Поддержка >97% браузеров, никаких полифиллов не требуется.
  3. Один маленький фикс может дать 30-50 пунктов PSI — если виджет действительно тяжёлый, а место в DOM подходит для отложенной загрузки.

Ссылки