Улучшайте производительность вашего сайта с помощью отложенной загрузки и распределения кода

1656644178 uluchshajte proizvoditelnost vashego sajta s pomoshhyu otlozhennoj zagruzki i raspredeleniya

Хосе М. Перес

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

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

1*QbeGpWdpFKZLxJsbCFVfjw
Рисунок Марвина Ронсдорфа

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

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

В статье будет использован Preact/React, но идеи можно применить к любой другой библиотеке компонентов.

Мы собираемся охватить несколько тем.

Давайте начнем!

Композиционные закономерности

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

Обычно это достигается с помощью компонентов высокого порядка (HOC). Эти компоненты получают еще один компонент и добавляют некоторые функции, например, поведение.

Если вы использовали redux, connect Функция – это HOC, который получает ваш неподключенный компонент. Вы можете найти больше примеров в «Глубокой реакции на компоненты высшего порядка» Френа Гиджарро.

const MyComponent = props => (  <div>    {props.id} - {props.name}  </div>);
// ...
const ConnectedComponent = connect(mapStateToProps, mapDispatchToProps)( MyComponent );

Функция как дочерний компонент (также известная как Render Callback) — это другой шаблон, используемый в подобных сценариях. В наши дни он становится достаточно популярным. Возможно, вы натыкались на них в React-media или в неизвестных.

Посмотрите на этот пример, взятый из react-media:

const MyComponent = () => (  <Media query="(max-width: 599px)">    {matches =>      matches ? (        <p>The document is less than 600px wide.</p>      ) : ( <p>The document is at least 600px wide.&lt;/p>      )    }  </Media>);

The Media компонент вызывает своих детей, передавая a matches аргумент. Таким образом, дочерним компонентам не нужно знать о медиа-запросе. Компонентирование в целом облегчает тестирование и обслуживание.

Улучшение производительности наших сайтов путем загрузки только необходимого

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

  • заголовок (в эти дни большое изображение героя занимает всю область в верхней части страницы)
  • раздел с несколькими изображениями
  • другой раздел с тяжелым компонентом, таким как карта
  • нижний колонтитул
0*OwMzSYIliOOuMnAs
Базовую структуру страницы мы будем использовать в качестве примера.

Это, отраженное в компонентах React, будет примерно следующим:

const Page = () => {  <div>    <Header />    <Gallery />    <Map />    <Footer />  </div>};

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

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

Много лет назад, еще до ES6, крупные компании придумали свои решения для определения зависимостей и загрузки их при необходимости. Yahoo создал YUI Loader, а Facebook написал Haste, Bootloader и Primer.

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

Какой смысл запрашивать ресурсы, которые пользователю не потребуются, например, изображения, до которых пользователь не сможет добраться? Или загрузка постороннего компонента, например Google Map, со всеми его дополнительными ресурсами, необходимыми для визуализации?

Отчет о покрытии кода, как предоставляемый Google Chrome нам не очень поможет. Код JS будет выполнен, а CSS применим к невидимым элементам.

0*aaC71xdnTv1M90uj
Вкладка покрытия кода в Google Chrome (источник)

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

  • Не ленитесь загружать выше свертки. В большинстве случаев мы хотим, чтобы содержимое в верхней части страницы было отображено как можно скорее. Каждая техника отложенной загрузки приведет к задержке. Браузер должен запустить JS, который вставляет HTML в документ, проанализировать его и начать запрашивать ресурсы, на которые ссылаются.
0*Og7qV0WrbJC8Thtl

Где установить изгиб? Это сложно, и это будет зависеть от устройства очень сильно различающегося и вашего макета.

  • Ленивая загрузка немного раньше, чем это необходимо. Вы хотите избежать отображения пустых областей пользователю. Для этого вы можете скачать нужный ресурс, когда он находится достаточно близко к видимой области. К примеру, пользователь прокручивает вниз и если изображение для загрузки находится, скажем, на 100 пикселей ниже нижней части окна просмотра, начинает спрашивать его.
0*SauGmZ_Equ1vw89o
  • Невидимое содержимое в некоторых сценариях. Следует учесть, что отложенное загруженное содержимое не будет отображаться в некоторых ситуациях:

1) Если загруженное содержимое не было загружено, оно не отображается при печати страницы.

2) То же может произойти, когда страница отображается в программах чтения RSS, которые могут не выполнять Javascript, необходимый для загрузки содержимого.

3) Что касается SEO, у вас могут возникнуть проблемы с индексированием отложенной загрузки содержимого в Google. К моменту написания этой статьи Googlebot поддерживает IntersectionObserver. Он вызывает обратный вызов с изменениями в окне просмотра над сверткой. однако, это не будет запускать обратный вызов для содержимого в нижней части страницы. таким образом, Google не будет просматривать и не индексировать это содержимое. Если ваше содержимое важно, вы можете, например, отобразить текст и отложено загружать такие компоненты, как изображения и другие виджеты (например, карты).

Здесь я визуализирую тестовую страницу (вы можете просмотреть источник здесь) с помощью инструментов Google для веб-мастеров «Просмотреть как Google». Робот Googlebot отображает содержимое в открывшемся окне просмотра, но не содержимое под ним.

Небольшой компонент для обнаружения видимой области

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

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

class Observer extends Component {  constructor() {    super();    this.state = { isVisible: false };    this.io = null;    this.container = null;  }  componentDidMount() {    this.io = new IntersectionObserver([entry] => {      this.setState({ isVisible: entry.isIntersecting });    }, {});    this.io.observe(this.container);  }  componentWillUnmount() {    if (this.io) {      this.io.disconnect();    }  }  render() {    return (      // we create a div to get a reference.      // It's possible to use findDOMNode() to avoid      // creating extra elements, but findDOMNode is discouraged      <div        ref={div => {          this.container = div;        }}      >        {Array.isArray(this.props.children)          ? this.props.children.map(child => child(this.state.isVisible))          : this.props.children(this.state.isVisible)}      </div>    );  }}

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

Этот базовый компонент можно расширить дополнительными свойствами, переданными в качестве параметров IntersectionObserver, таких как поля или пороговые значения. Это позволяет нам обнаруживать элементы вблизи окна просмотра, но не пересекаются с ним. Параметры устанавливаются в конструкторе и доступны только для чтения. Таким образом, добавление поддержки параметров означает, что нам нужно будет восстановить IntersectionObserver с новыми параметрами, когда они изменятся, добавляя дополнительную логику в componentWillReceiveProps которые мы не собираемся здесь освещать.

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

const Page = () => {    <div>        <Header />        <Observer>          {isVisible => <Gallery isVisible />}        </Observer>        <Observer>          {isVisible => <Map isVisible />}        </Observer>        <Footer />    </div>}

В приведенном выше коде я просто передаю isVisible собственность до Gallery и Map компоненты, чтобы они с этим справлялись. Кроме того, мы можем вернуть компонент, если он видим, или пустой элемент в противном случае.

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

Как сделать Map и Gallery компоненты используют isVisible собственность? Давайте посмотрим на Map:

class Map extends Component {  constructor() {    super();    this.state = { initialized: false };    this.map = null;  }
initializeMap() {    this.setState({ initialized: true });    // loadScript loads an external script, its definition is not included here.    loadScript(" () => {      const latlng = new google.maps.LatLng(38.34, -0.48);      const myOptions = { zoom: 15, center: latlng };      const map = new google.maps.Map(this.map, myOptions);    });  }
componentDidMount() {    if (this.props.isVisible) {      this.initializeMap();    }  }
componentWillReceiveProps(nextProps) {    if (!this.state.initialized && nextProps.isVisible) {      this.initializeMap();    }  }
render() {    return (      <div        ref={div => {          this.map = div;        }}      />    );  }}

Когда контейнер отображается в окне просмотра, мы посылаем запрос на использование сценария Google Map. После загрузки мы создаем карту. Это хороший пример JavaScript с отложенной загрузкой, который не нужен с самого начала, а остальные ресурсы нужны для отображения карты.

Компонент имеет возможность избежать повторного ввода сценария Карты Google.

Давайте посмотрим на Gallery компонент:

class Gallery extends Component {  constructor() {    super();    this.state = { hasBeenVisible: false };  }  componentDidMount() {    if (this.props.isVisible) {      this.setState({ hasBeenVisible: true });    }  }  componentWillReceiveProps(nextProps) {    if (!this.state.hasBeenVisible && nextProps.isVisible) {      this.setState({ hasBeenVisible: true });    }  }  render() {    return (      <div>        <h1>Some pictures</h1>        Picture 1        {this.state.hasBeenVisible ? (          <img src=" width="300" height="300" />        ) : (          <div className="placeholder" />        )}        Picture 2        {this.state.hasBeenVisible ? (          <img src=" width="300" height="300" />        ) : (          <div className="placeholder" />        )}      </div>    );  }}

Приведенный выше пример определяет другой компонент с сохранением состояния. На самом деле мы сохраняем в состоянии ту же информацию, что и с Map.

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

Дочерние компоненты без гражданства

Компонент без состояния также может быть интересен. Это позволит нам выгрузить изображения, которые больше не видны, показывая заполнители:

const Gallery = ({ isVisible }) => (  <div>    <h1>Some pictures</h1>;    Picture 1    {isVisible ? (      <img src=" width="300" height="300" />    ) : (      <div className="placeholder" />    )}    Picture 2    {isVisible ? (      <img src=" width="300" height="300" />    ) : (      <div className="placeholder" />    )}  </div>);

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

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

const Page = () => {  ...  <Observer>    {(isVisible, hasBeenVisible) =>      <Gallery hasBeenVisible /> // Gallery can be now stateless    }  &lt;/Observer>  ...}

Другой вариант – иметь вариант Observer компонент, передающий только prop like hasBeenVisible. Это имеет преимущество в том, что мы можем отключить IntersectionObserver, как только элемент будет виден, поскольку мы не собираемся изменять его значение. Назовем этот компонент ObserverOnce:

class ObserverOnce extends Component {  constructor() {    super();    this.state = { hasBeenVisible: false };    this.io = null;    this.container = null;  }  componentDidMount() {    this.io = new IntersectionObserver(entries => {      entries.forEach(entry => {        if (entry.isIntersecting) {          this.setState({ hasBeenVisible: true });          this.io.disconnect();        }      });    }, {});    this.io.observe(this.container);  }  componentWillUnmount() {    if (this.io) {      this.io.disconnect();    }  }  render() {    return (      <div        ref={div => {          this.container = div;        }}      >        {Array.isArray(this.props.children)          ? this.props.children.map(child => child(this.state.hasBeenVisible))          : this.props.children(this.state.hasBeenVisible)}      &lt;/div>    );  }}

Больше случаев использования

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

Вот пример с веб-сайта React Alicante. Он анимирует некоторые номера конференции, как только пользователь переходит в этот раздел.

Мы могли бы воспроизвести это так (см. пример Codepen):

class ConferenceData extends Component {  constructor() {    super();    this.state = { progress: 0 };    this.interval = null;    this.animationDuration = 2000;    this.startAnimation = null;  }  componentWillReceiveProps(nextProps) {    if (      !this.props.isVisible &&      nextProps.isVisible &&      this.state.progress !== 1    ) {      this.startAnimation = Date.now();      const tick = () => {        const progress = Math.min(          1,          (Date.now() - this.startAnimation) / this.animationDuration        );        this.setState({ progress: progress });        if (progress < 1) {          requestAnimationFrame(tick);        }      };      tick();    }  }  render() {    return (      <div>        {Math.floor(this.state.progress * 3)} days ·        {Math.floor(this.state.progress * 21)} talks ·        {Math.floor(this.state.progress * 4)} workshops ·        {Math.floor(this.state.progress * 350)} attendees      </div>    );  }}

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

Полизаполнение IntersectionObserver по требованию

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

Вариантом будет установить isVisible к true когда IntersectionObserver недоступен. На практике это отключит загрузку. В определенном смысле мы бы рассматривали отложенную загрузку как прогрессивное усовершенствование:

class Observer extends Component {  constructor() {    super();    // isVisible is initialized to true if the browser    // does not support IntersectionObserver API    this.state = { isVisible: !(window.IntersectionObserver) };    this.io = null;    this.container = null;  }  componentDidMount() {    // only initialize the IntersectionObserver if supported    if (window.IntersectionObserver) {      this.io = new IntersectionObserver(entries => {        ...      }    }  }}

Другой вариант, который я предпочитаю, состоит в том, чтобы включить полизаполнение, например полизаполнение IntersectionObserver w3c. Таким образом, IntersectionObserver будет работать во всех браузерах.

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

class Observer extends Component {  ...  componentDidMount() {    (window.IntersectionObserver      ? Promise.resolve()      : import('intersection-observer')    ).then(() => {      this.io = new window.IntersectionObserver(entries => {        entries.forEach(entry => {          this.setState({ isVisible: entry.isIntersecting });        });      }, {});      this.io.observe(this.container);    });  }  ...}

Вы можете просмотреть демо здесь (проверьте источник кода). Safari сделает дополнительный запрос для загрузки intersection-observer пакет npm, потому что он не поддерживает IntersectionObserver.

0*vNqwYA3I3hEASrCG
Safari запрашивает полизаполнение для перекрестка-наблюдателя по требованию. Нет необходимости посылать его в поддерживающие его браузеры.

Это достигается благодаря разделению кода. Существуют такие инструменты, как Parcel или Webpack, которые создают пакет для импортируемого пакета и логику, необходимую для запроса файла.

Разбиение кода и CSS-in-JS

Пока мы видели, как использовать HOC, чтобы обнаружить, что элемент находится в окне просмотра. Мы также видели, как при необходимости загрузить дополнительный JavaScript.

Разделение кода достаточно распространено и просто для реализации на уровне маршрута. Браузер загружает дополнительные пакеты, когда пользователь переходит между разными URL-адресами на сайте. Такие инструменты как react-router и Next.js сделали это простым в реализации.

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

Компонент может ссылаться на другие ресурсы или даже встраивать их. Подумайте о стилях SVG или CSS.

Нет смысла запрашивать стили, которые не будут применены ни к одному элементу. Динамический запрос и использование CSS вызывает FOUC (Flash of Unstyled Content). В браузере отображаются элементы HTML с существующим стилем. После ввода дополнительных стилей он изменяет стили содержимого. С появлением решений CSS-in-JS (или JSS) это больше не проблема. CSS встроен в компонент, и мы получаем подлинное распределение кода для наших компонентов. Благодаря CSS-in-JS мы улучшаем разделение кода, загружая CSS по требованию.

Полезные реализации

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

Я однозначно рекомендую вам проверить эти 2 библиотеки:

Вывод

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

Я хотел бы поблагодарить @alexjoverm, @aarongarciah и @FlavioCorpa за просмотр публикации, исследование подобных тем и рекомендацию инструментов для представления примеров на странице.

Вы видели ошибку или неправильную информацию? В таком случае напишите мне.

Читайте больше от меня на моем сайте.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *