Как лениво загружать клиентские компоненты и библиотеки

Ленивая загрузка в Next.js помогает улучшить начальную производительность загрузки приложения, уменьшая количество JavaScript, необходимого для рендеринга маршрута.

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

В Next.js есть два способа реализации ленивой загрузки:

  1. Использование динамических импортов с next/dynamic
  2. Использование React.lazy() с Suspense

По умолчанию серверные компоненты (Server Components) автоматически подвергаются разделению кода (code splitting), и вы можете использовать стриминг для постепенной отправки частей UI с сервера на клиент. Ленивая загрузка применяется к клиентским компонентам.

next/dynamic

next/dynamic представляет собой комбинацию React.lazy() и Suspense. Он работает одинаково в директориях app и pages, что позволяет постепенно мигрировать.

Примеры

Импорт клиентских компонентов

app/page.js
'use client'

import { useState } from 'react'
import dynamic from 'next/dynamic'

// Клиентские компоненты:
const ComponentA = dynamic(() => import('../components/A'))
const ComponentB = dynamic(() => import('../components/B'))
const ComponentC = dynamic(() => import('../components/C'), { ssr: false })

export default function ClientComponentExample() {
  const [showMore, setShowMore] = useState(false)

  return (
    <div>
      {/* Загружается сразу, но в отдельном клиентском бандле */}
      <ComponentA />

      {/* Загружается по требованию, только при выполнении условия */}
      {showMore && <ComponentB />}
      <button onClick={() => setShowMore(!showMore)}>Переключить</button>

      {/* Загружается только на стороне клиента */}
      <ComponentC />
    </div>
  )
}

Примечание: Когда серверный компонент динамически импортирует клиентский компонент, автоматическое разделение кода (code splitting) в настоящее время не поддерживается.

Отключение SSR

При использовании React.lazy() и Suspense клиентские компоненты по умолчанию будут пререндериться (SSR).

Примечание: Опция ssr: false работает только для клиентских компонентов. Для корректной работы разделения кода перенесите её в клиентские компоненты.

Если вы хотите отключить пререндеринг для клиентского компонента, вы можете использовать опцию ssr со значением false:

const ComponentC = dynamic(() => import('../components/C'), { ssr: false })

Импорт серверных компонентов

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

app/page.js
import dynamic from 'next/dynamic'

// Серверный компонент:
const ServerComponent = dynamic(() => import('../components/ServerComponent'))

export default function ServerComponentExample() {
  return (
    <div>
      <ServerComponent />
    </div>
  )
}

Примечание: Опция ssr: false не поддерживается в серверных компонентах. Вы увидите ошибку, если попытаетесь использовать её в серверных компонентах. ssr: false не разрешён с next/dynamic в серверных компонентах. Пожалуйста, перенесите его в клиентский компонент.

Загрузка внешних библиотек

Внешние библиотеки можно загружать по требованию с помощью функции import(). В этом примере используется внешняя библиотека fuse.js для нечёткого поиска. Модуль загружается на клиенте только после того, как пользователь введёт текст в поле поиска.

app/page.js
'use client'

import { useState } from 'react'

const names = ['Tim', 'Joe', 'Bel', 'Lee']

export default function Page() {
  const [results, setResults] = useState()

  return (
    <div>
      <input
        type="text"
        placeholder="Поиск"
        onChange={async (e) => {
          const { value } = e.currentTarget
          // Динамическая загрузка fuse.js
          const Fuse = (await import('fuse.js')).default
          const fuse = new Fuse(names)

          setResults(fuse.search(value))
        }}
      />
      <pre>Результаты: {JSON.stringify(results, null, 2)}</pre>
    </div>
  )
}

Добавление кастомного компонента загрузки

app/page.js
'use client'

import dynamic from 'next/dynamic'

const WithCustomLoading = dynamic(
  () => import('../components/WithCustomLoading'),
  {
    loading: () => <p>Загрузка...</p>,
  }
)

export default function Page() {
  return (
    <div>
      {/* Компонент загрузки будет отображаться во время загрузки <WithCustomLoading/> */}
      <WithCustomLoading />
    </div>
  )
}

Импорт именованных экспортов

Для динамического импорта именованного экспорта вы можете вернуть его из Promise, возвращаемого функцией import():

components/hello.js
'use client'

export function Hello() {
  return <p>Привет!</p>
}
app/page.js
import dynamic from 'next/dynamic'

const ClientComponent = dynamic(() =>
  import('../components/hello').then((mod) => mod.Hello)
)