Ссылка на оригинал

Паттерн заключается в отложенной загрузке ресурсов, то есть только тогда, когда пользователю нужна какая-либо часть интерфейса.

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

Этими частями интерфейса могут являться видео плееры, чаты, либо часть интерфейса, которая появляется по клику.

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

Это может негативно отразиться на таких метриках как:

  • FID (First Input Delay)

  • TBT (Total Blocking Time)

  • TTI (Time To Interactive)

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

  • Пользователь впервые кликает на компоненте

  • Компонент попал во viewport

  • Или отложить загрузку компонента до того момента, пока браузер не будет бездействовать ( через requestIdleCallback )

Различные варианты как мы можем загружать ресурсы:

  • Сразу - обычный способ для загрузки скриптов

  • Lazy (для роутера) - загружать, когда пользователь посещает страницы

  • Lazy (при взаимодействии) - пользователь кликнул на элементе

  • Lazy (viewport) - загружать когда юзер доскроллил до компонента

  • Prefetch - предварительная загрузка, но после загрузки critical resources

  • Preload - загрузка сразу

Отложенный импорт при взаимодействии очень часто встречается и мы разберем дальше несколько примеров. Вы могли его видеть например в Google Docs, где применение этого паттерна позволяет экономить около 500кб:

Другие ситуации где пригодится этот паттерн - это всевозможные сторонние виджеты.

Загрузка youtube видео по клику:

А вот на сайте android.com :

Аутентификация

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

Виджеты для чатов

Calibre app улучшили производительность их виджета на 30% через использование подобного "фасадного подхода". Они сделали кнопку чата на CSS и HTML, и по клику на нее подгружают нужный бандл.

Postmark также отметили, что всегда загружали свой help виджет сразу, хотя не всем пользователям требовался данный функционал. Причем весь виджета составляем 314кб - больше чем вся их главная страница. Для улучшения UX они заменили виджет на CSS + HTML компонент, и грузили всю библиотеку только после клика по кнопке. Таким образом они смогли уменьшить метрику TTI (Time To Interactive) с 7.7 до 3.7 секунд.

Другие примеры

Компания NE Digital использует react-scroll для анимированного скролла к началу страницы. Вместо того чтобы сразу загружать всю библиотеку, они загружают ее по клику на кнопке, экономя около 7КБ.

handleScrollToTop() {
    import('react-scroll').then(scroll => {
      scroll.animateScroll.scrollToTop({
      })
    })
}

Как мы можем применять этот паттерн ?

Чистый JS

Динамический импорт использует отложенную загрузку модулей и возвращает Promise, это может отличным решением при правильном использовании.

Ниже приведен пример, в котором динамический импорт используется в листенере кнопки для импорта модуля lodash.sortby, а затем его использования.

const btn = document.querySelector('button');

btn.addEventListener('click', e => {
  e.preventDefault();
  import('lodash.sortby')
    .then(module => module.default)
    .then(sortInput()) // use the imported dependency
    .catch(err => { console.log(err) });
});

Также можно динамически инлайнить скрипты:

const loginBtn = document.querySelector('#login');

loginBtn.addEventListener('click', () => {
  const loader = new scriptLoader();
  loader.load([
      '//apis.google.com/js/client:platform.js?onload=showLoginScreen'
  ]).then(({length}) => {
      console.log(${length} scripts loaded!);
  });
});

React

Давайте представим что у нас есть приложение чата, у которого есть три компонента:

  • <MessageList>

  • <MessageInput>

  • <EmojiPicker> (использует emoji-mart весом 98кб - минифицированный и gzip'нутый)

Часто бывает так, что все эти компоненты загружаются при стартовой загрузке страницы:

import MessageList from './MessageList';
import MessageInput from './MessageInput';
import EmojiPicker from './EmojiPicker';

const Channel = () => {
  ...
  return (
    <div>
      <MessageList />
      <MessageInput />
      {emojiPickerOpen && <EmojiPicker />}
    </div>
  );
};

Можно декомпозировать такую загрузку с помощью code-splitting.

Метод React.lazy позволяет легко сделать code-splitting на компонентом уровне с помощью динамического импорта. Функция React.lazy предоставляет легкую возможность разделить компоненты в приложении на отдельные чанки. С помощью Suspense можно добавить состояние загрузки, пока грузится EmojiPicker:

import React, { lazy, Suspense } from 'react';
import MessageList from './MessageList';
import MessageInput from './MessageInput';

const EmojiPicker = lazy(
  () => import('./EmojiPicker')
);
const Channel = () => {
  ...
  return (
    <div>
      <MessageList />
      <MessageInput />
      {emojiPickerOpen && (
        <Suspense fallback={<div>Loading...</div>}>
          <EmojiPicker />
        </Suspense>
      )}
    </div>
  );
};

Мы можем пойти дальше и загружать код для EmojiPicker только при щелчке значка эмодзи в <MessageInput>, а не сразу при загрузке приложения :

import React, { useState, createElement } from 'react';
import MessageList from './MessageList';
import MessageInput from './MessageInput';
import ErrorBoundary from './ErrorBoundary';

const Channel = () => {
  const [emojiPickerEl, setEmojiPickerEl] = useState(null);
  const openEmojiPicker = () => {
    import(/* webpackChunkName: "emoji-picker" */ './EmojiPicker')
      .then(module => module.default)
      .then(emojiPicker => {
        setEmojiPickerEl(createElement(emojiPicker));
      });
  };
  const closeEmojiPickerHandler = () => {
    setEmojiPickerEl(null);
  };
  return (
    <ErrorBoundary>
      <div>
        <MessageList />
        <MessageInput onClick={openEmojiPicker} />
        {emojiPickerEl}
      </div>
    </ErrorBoundary>
  );
};

Vue

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

С помощью v-if="show" мы можем управлять отображением компонента EmojiPicker по клику на кнопке:

<template>
  <div>
    <button @click="show = true">Load Emoji Picker</button>
    <div v-if="show">
      <emojipicker></emojipicker>
    </div>
  </div>
</template>

<script>
export default {
  data: () => ({ show: false }),
  components: {
    Emojipicker: () => import('./Emojipicker')
  }
};
</script>

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

Продолжение следует...