Резюме, WTF?

Может быть, вы слышали термин Resumability, о котором недавно говорили. Может быть, кто-то восхищался новым фреймворком Qwik от Miško Hevery. Может быть, вы слышали, как я упоминал его в нашей работе над грядущим Marko 6. Может быть, вы слышали, что это как-то связано с гидратацией. Но вы даже не знаете, что такое гидратация.

Эта статья для вас.


Почему разработчики JavaScript так хотят пить?

🧁 Марк Далглиш
@markdalgleish
Я слышал, как некоторые JavaScript-разработчики говорили, что гидратация — это чистая накладность, поэтому я решил перестать пить воду.
03:30 AM — 18 Jun 2022

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

Гидратация — это процесс восстановления приложения, отрендеренного на сервере, до состояния, в котором оно находилось бы, если бы было отрендерено на клиенте. (Кредит: @mlrawlings)

Почему это вообще происходит? Так было не всегда. В прежние времена, мы делали серверный рендеринг страницы в бэкенде по нашему выбору, затем добавляли немного JavaScript для обработки взаимодействия. Возможно, немного jQuery.

По мере роста требований к интерактивности мы добавляли в код все больше структуры. И императивные посыпки превратились в декларативные фреймворки, которые вы знаете и любите сегодня, такие как React, Angular и Vue. Все представление пользовательского интерфейса теперь живет в JavaScript.

Чтобы вернуть возможность серверного рендеринга, эти фреймворки также работают на сервере для генерации HTML. Мы получаем возможность создавать и поддерживать одно приложение на одном языке. Когда наше приложение запускается в браузере, эти фреймворки повторно запускают тот же код, добавляя обработчики событий и обеспечивая правильное состояние приложения. И эта «повторная гидратация» (позже сокращенная до гидратации) позволяет приложению быть интерактивным.

Звучит пока неплохо? Но есть проблема.


Входим в долину сверхъестественного

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

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

Вы можете сделать страницу без JavaScript, но она не будет предлагать тот же пользовательский опыт, пока JavaScript не будет выполнен. Кто-то может нажать на кнопку, а ничего не произойдет, и при этом не будет никакой индикации. Или кто-то может вызвать полную перезагрузку страницы, просто чтобы подождать, пока все это произойдет снова. Если бы они подождали на полсекунды дольше, это было бы плавное взаимодействие на стороне клиента.

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

Не самый лучший опыт.


Так что же такое возобновляемость?

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

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

Звучит знакомо? Не это ли мы делали в свое время с Vanilla JavaScript или jQuery?

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

Итак…, почему гидратация — это вещь? Почему не все можно возобновить?

Это довольно сложно сделать. Современные декларативные фреймворки управляются данными. По событию вы обновляете некоторое состояние, и некоторые компоненты перерисовываются. И в этом кроется сложность.

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

function Counter() {
  const [count, setCount] = useState(0);

  // How can I call `increment` without ever running
  // Counter once? Where does `count` and `setCount`
  // come from?
  const increment = () => setCount(count + 1);

  return <button
      className="counter-button"
      onClick={increment}
    >
      {count}
    </button>;
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

// Over-simplified example:

// global scope
const lookup = [
  ...,
  Counter,
  increment,
  ...,
  {
    count: createReactiveStateWhenAccessedFirstTime({
      value: 0,
      watchers: [Counter]
    })
  }
]

// global event handler
function increment(event, ctx) {
  ctx.count++; // update value and trigger watchers
  // ie.. only run the component for the first time now.
}

// global event listener
document.addEventListener("click", (event) => {
  // find the element we care about
  if (event.target.className === "counter-button") {
    // find the location of its handler and data from it
    const fn = lookup[event.target.$$handlerID];
    const context = lookup[event.target.$$handlerContext];
    fn(event, context);
  }
});
Вход в полноэкранный режим Выход из полноэкранного режима

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

Это нетривиальная реализация, и она не обходится без компромиссов.


Сериализация

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

// in the code
const [count, setState] = useState(0); 


// in JSON
<script id="__NEXT_DATA__" type="application/json">
  ...
</script>
Вход в полноэкранный режим Выйти из полноэкранного режима

Это нужно нам потому, что когда мы пробуждаем приложение в браузере, наш JavaScript-код должен быть в том же состоянии, что и отображаемый в данный момент HTML.

Вы можете подумать: «Разве мы не можем просто взять эту информацию из HTML?».

Можем и не можем. Окончательный вывод содержит только окончательно отформатированные данные. Это может быть с потерями.

const [date, setDate] = useState(Date.now());
const [dateFormat, setDateFormat] = useState("MM/DD/YYYY")

return <time>{format(date, dataFormat)}</time>

// in HTML - how do I get the timestamp?
<time>08/19/2022</time>
Вход в полноэкранный режим Выход из полноэкранного режима

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

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


Три мушкетера

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

Одна оптимизация известна как Progressive Hydration (или иногда Selective Hydration). В Resumable-фреймворках вы не совсем гидратируете, но вы все равно можете отложить загрузку кода до тех пор, пока он вам не понадобится. Это может значительно сократить пакет и достичь «0Kb JavaScript по умолчанию». Это помогает улучшить показатели загрузки страницы, но может отложить работу до тех пор, пока вы не начнете взаимодействовать. К выгоде Resumability, это только затраты на загрузку и разбор, поскольку ему не нужно, чтобы этот JavaScript выполнялся с нетерпением. Но это все равно нужно делать аккуратно, и рекомендуется предварительно загружать все критические взаимодействия со страницей.

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

Это известно как частичная гидратация; возможно, вы видели версию этой техники, Islands, в таких фреймворках, как Marko, Astro или Fresh. Разница в том, что возобновляемые фреймворки могут делать это на уровне субкомпонентов, гораздо меньше, чем Islands, и они могут делать это автоматически.

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


Жизнь в возобновляемом мире

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

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

Но если он оправдает себя, то, наконец, объединит старый Web императивного jQuery с механизмами современных декларативных JavaScript-фреймворков. И, возможно, даже исчезнет необходимость в таких словах, как resumability и hydration в нашем веб-словаре.

И это то, к чему стоит стремиться.


Особая благодарность @tigt & @t3dotgg за обзор этой статьи.

Оцените статью
devanswers.ru
Добавить комментарий