Оптимизация производительности через ленивый(отложенный) рендеринг/гидратацию

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

Дабы избежать этого был добавлен фикс, подробнее

Async-components

Обычные асинхронные компоненты все-равно будут грузиться и на клиенте и на сервере(исправляется фиксом выше), ЕСЛИ v-if=true, в случае v-if=false грузиться они не будут. С v-show в любом значении компонент будет загружен.

Имеет смысл для элементов которые меняются между мобильным/десктопным интерфейсом, не важны для seo и отображаются по действию (модальные окна, мобильные сайдбары)

<template>
  <div>
    <AsyncComponent />
  </div>
</template>

<script>
export default {
  components: {
    AsyncComponent: () => import('foo/bar'),
  },
};
</script>

Преимущества и недостатки

  • ✓ Отложенная загрузка если компонент находится за v-if или <Component :is=""/>
  • ✓ Работает для seo (рендерится при ssr)
  • × В остальных случаях порой хуже статичных импортов т.к. грузится и на клиенте и на сервере и маунтиться сразу, но получается лишний запрос
  • × Возможно размазывание long-task при первом рендере и увеличение FID/INP

Область применения

  • Компоненты в <Component :is="component" />
  • Компоненты за v-if (лучше с исходным значением false)

Vue-lazy-hydration

C v-if компонент грузится лишь при v-if="true". С v-show гидрируется и грузится по триггеру (when-visible итд) (для ssr уточнение)

<template>
  <div>
    <AsyncComponent />
  </div>
</template>

<script>
import { hydrateNever } from 'vue-lazy-hydration';

export default {
  components: {
    AsyncComponent: hydrateNever(() => import('foo/bar')),
  },
};
</script>

Преимущества и недостатки

  • ✓ Логика компонента, загрузка (для ssr fix) и привязывание к DOM выполняется по триггеру
  • ✓ Работает для seo (рендерится при ssr)
  • × При слишком коротких триггерах возможно размазывание long-task при первом рендере и увеличение FID/INP
  • × При неточных триггерах или их отсутствие, потеря интерактивности
  • × Теряет смысл при spa переходах

Область применения

  • Тяжелые компоненты НУЖНЫЕ при ssr (SfCarousel, разнообразные списки)
  • Формы при правильном триггере

Lazy-component

Сочетание ClientOnly и триггеров на манер vue-lazy-hydration, при указании loadingComponent лоадер отображается при ssr и до маунта компонента.

<template>
  <div>
    <LazyComponent />
  </div>
</template>

<script>
import { defineLazyComponentWhenObserve } from 'shared-front/lib/composition/lazy-component';

export default {
  components: {
    LazyComponent: defineLazyComponentWhenObserve({
      loader: () => import('foo/bar'),
      loadingComponent: 'SfSpinner',
    }),
  },
};
</script>

Преимущества и недостатки

  • ✓ Клиентский код не исполняется на сервере, полезно для библиотек или если нужен browser api
  • ✓ Первый рендер и загрузка откладываются до триггера
  • ✓ Работает при spa переходах
  • × Не работает для seo (при ssr отображается лоадер если есть)
  • × При слишком коротких триггерах возможно размазывание long-task при первом рендере и увеличение FID/INP
  • × При неточных триггерах или их отсутствие, потеря интерактивности

Область применения

  • Тяжелые компоненты НЕ нужные при ssr (SfRichEditor)
  • Модальные окна и выезжающие панели которых нет на экране изначально (MobileUserMenu)

ClientOnly

Преимущества и недостатки

  • ✓ Клиентский код не исполняется на сервере, полезно для библиотек или если нужен browser api
  • × Для первого рендера модифицируется dom и этим вызывает дополнительную нагрузку на браузер, которой бы не было при ssr

Область применения

  • Компоненты в которых нужно browser API и которые нужны как можно скорее (прим. SfColorThemeSwitcher, FcmSubscribed)
  • Страницы которые не нужны при ssr (формы spravochnik/redactor)

async-components

Обычные асинхронные компоненты все-равно будут грузиться и на клиенте и на сервере даже с v-if (тем более с v-show)

<template>
  <div>
    <ClientOnly>
      <AsyncComponent />
    </ClientOnly>
  </div>
</template>

<script>
export default {
  components: {
    AsyncComponent: () => import('foo/bar'),
  },
};
</script>

Оборачивание vue-lazy-hydration в ClientOnly

При оборачивании vue-lazy-hydration в ClientOnly и , гидратация не откладывается т.к. её нет. Компонент сразу загружается и маунтиться.

Полностью идентично обычным/асинхронным компонентам в ClientOnly, лишено смысла.

<template>
  <div>
    <ClientOnly>
      <AsyncComponent />
    </ClientOnly>
  </div>
</template>

<script>
import { hydrateNever } from 'vue-lazy-hydration';

export default {
  components: {
    AsyncComponent: hydrateNever(() => import('foo/bar')),
  },
};
</script>

lazy-component

Внутри lazyComponent и так применяется логика ClientOnly, нет нужды в еще одной обертке

<template>
  <div>
    <ClientOnly>
      <LazyComponent />
    </ClientOnly>
  </div>
</template>

<script>
import { defineLazyComponentWhenObserve } from 'shared-front/lib/composition/lazy-component';

export default {
  components: {
    LazyComponent: defineLazyComponentWhenObserve({
      loader: () => import('foo/bar'),
      loadingComponent: 'SfSpinner',
    }),
  },
};
</script>

Ssr and async-components

По умолчанию vue-server-renderer (vue2) используемый в nuxt2 добавляет асинхронные компоненты в <body>. Даже если фактическая их загрузка на клиенте никогда не стриггерится.

Для исправления этого, был написан хук к наксту, который вырезает <script> с определенным названием файла.

Исходно решение было взято из vuejs/vue#9847

hooks: {
  render: {
    // Хук после рендера, но до отправки клиенту
    route: (_, page) => {
      // html ответа
      page.html = page.html.replace(
        /<script[\w"= ]*src="(.*?)".*?><\/script>/gm,
        (match, src) => {
          // Удаление скриптов по src
          if (src.includes('_lazy')) return '';

          return match;
        },
      );
    },
  },
},

Помимо вырезания, нужно, чтобы вебпак отдавал эти названия файлов, поэтому необходимо чтобы config.build.filenames.chunk всегда содержал название чанка ([name])

build: {
  filenames: {
    chunk: () => '[id]-[name]-[chunkhash].js',
  },
},

Для использования асинхронных чанков необходимо добавлять webpackChunkName с _lazy в названии

const ShopGuestPurchaseModal = () =>
  import(
    /* webpackChunkName: "shop-id_lazy" */ './-components/shop-guest-purchase-modal'
  );

Итоги

  • !!! Для асинхронных импортов компонентов нужно добавление webpackChunkName с постфиксом _lazy
  • Для тяжелых компонентов не нужных при первом рендере использовать lazyComponent
  • Для тяжелых компонентов нужных при ssr, но с которыми юзер не будет сразу работать vue-lazy-hydration
  • Для компонентов в <Component :is="component" /> обычные async-component или vue-lazy-hydration