Оптимизация бандла

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

Символы vs Чанки

Символы - это отдельные лениво загружаемые фрагменты в Qwik. Символ создается всякий раз, когда вы используете __$(__) в своём исходном коде.

Например, приведённый ниже код создает два символа: от component$ и onClick$.

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
 
  return (
    <button onClick$={() => count.value++}>
      Прибавить к {count.value}
    </button>
  );
});

Оптимизатор переписывает приведённый выше код в нечто подобное:

Файл: chunk-1.js

export default componentQRL(qrl('./chunk-1.js', 's_ABC123'));
 
export const s_ABC123 = () => {
  const count = useSignal(0);
 
  return (
    <button onClickQRL={qrl('/.chunk-1.js', 's_XYZ342')}>
      Прибавить к {count.value}
    </button>
  );
};
 
export const s_XYZ432 = () => {
  const [count] = useLexicalScope();
  return count.value++;
}

В приведённом выше примере все символы (sABC123 и s_XYZ432) находятся в одном чанке (./chunk-1.js).

Чанки - это бандлы JavaScript, которые могут содержать один или несколько символов.

Оптимальное распределение символов

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

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

Проблема с одним чанком заключается в следующем:

  • Он будет содержать много символов, которые не нужны клиенту (зря потраченная полоса пропускания);
  • Клиент не может выполнять никакие символы, пока не будет загружен весь чанк.

Проблема с отдельным чанком для каждого символа заключается в следующем:

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

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

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

qwikVite() Plugin

Плагин qwikVite() в вашем файле vite.config.ts управляет распределением символов. Обычно опция entryStrategy устанавливается в значение smart, что позволяет Qwik делать эвристические предположения о том, как символы должны быть лениво загружены. Но можно переопределить эвристику, указав объект конфигурации manual в файле vite.config.ts:

export default defineConfig(() => {
  const routesDir = resolve('src', 'routes');
  return {
    // ...
    qwikVite({
      entryStrategy: {
        type: 'smart',
        manual: {
          ...bundle('bundleA', [
              's_I5CyQjO9FjQ',
              's_NsnidK2eXPg',
              's_kDw0latGeM0',
          ]),
          ...bundle('bundleB', [
              's_vXb90XKAnjE',
              's_hYpp40gCb60',
          ]),
          ...bundle('bundleC', [
              's_AqHBIVNKf34',
              's_oEksvFPgMEM',
              's_eePwnt3YTI8',
          ]),
        },
      },
    }),
  };
});
 
function bundle(bundleName: string, symbols: string[]) {
  return symbols.reduce((obj, key) => {
    // Иногда символы имеют префикс `s_`, удаляем его.
    obj[key.replace('s_', '')] = obj[key] = bundleName;
    return obj;
  }, {} as Record<string, string>);
}

Поэтому возникает вопрос, как получить имена символов, такие как s_I5CyQjO9FjQ? См. следующий раздел Аналитика времени выполнения.

Аналитика времени выполнения

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

Для сбора данных об использовании символов в запущенном приложении необходимо:

  1. Вставьте этот код в ваше приложение:
    <script>
      window.symbols = [];
      document.addEventListener('qsymbol', (e) => window.symbols.push(e.detail));
    </script>
  2. Выполните некоторый набор операций, имитирующих поведение пользователя.
  3. Откройте консоль и введите symbols, чтобы увидеть список используемых символов. Используйте эту информацию для обновления файла vite.config.ts.

ПРИМЕЧАНИЕ: Мы рассматриваем возможность создания более эффективных способов сбора этой информации в будущем (см. Insights).

ПРИМЕЧАНИЕ: Хеши символов разработаны так, чтобы быть стабильными даже при многочисленных компиляциях приложения. Однако, если код подвергается сложному рефакторингу, хэш может измениться. Это не приведет к поломке приложения, но может привести к перемещению символа в другой, неоптимальный чанк до тех пор, пока аналитика времени выполнения не будет собрана снова.

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

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

См. спекулятивное получение модулей.

Обратите внимание, что функция Service Worker доступна только в защищённом контексте (HTTPS), в некоторых или во всех поддерживающих браузерах. См. спецификацию serviceWorker property API specs.

События

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

Детали пользовательского события qprefetch

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

Обычно сервис-воркер слушает событие qprefetch и загружает требуемый символ в кэш браузера. Сервис-воркер имеет карту отношений символов и бандлов, и использует её, чтобы определить, какие бандлы следует заранее получить на основе символа.

export interface QPrefetchDetail {
  /// Список символов для предварительной выборки.
  symbols: string[];
}

Детали пользовательского события qsymbol

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

export interface QSymbolDetail {
  /// Необязательное событие DOM, которое вызвало разрешение символа.
  element?: Element;
  /// Время запроса, когда символ был разрешён.
  reqTime: number;
  /// Разрешаемый символ.
  symbol: string;
}

Водопад

Сервис-воркер пытается минимизировать водопад запросов путём предварительной выборки бандлов. Чтобы иметь возможность делать это, сервис-воркер имеет manifest символов и чанков. manifest представляет собой полный граф всех символов и соответствующих им чанков. Он также знает граф импорта, поэтому, если символ предварительно загружается, то сервисный работник также предварительно загрузит и все другие символы, которые необходимы как часть графа импорта.

Участники

Спасибо всем участникам, которые помогли сделать эту документацию лучше!

  • mhevery
  • the-r3aper7
  • mrhoodz
  • Craiqser
  • literalpie
  • antoinepairet
  • hamatoyogi