Опубліковано
10 хв читання

Серверні компоненти React

Автори
  • Learna
    Ім’я
    Learna

Чому це важливо?

Повільна сторінка погіршує досвід користувача й може коштувати бізнесу грошей. Людина з повільним інтернетом або слабким телефоном може просто закрити застосунок.

Ця стаття описує підходи й експерименти 2024 року. Кешування та API фреймворків залежать від версії; результати прикладів не є універсальними гарантіями.

Для чого потрібні RSC?

Серверні компоненти допомагають:

  • зменшити каскади послідовних мережевих запитів
  • скоротити клієнтський JavaScript, організувати кешування й покращити завантаження
  • полегшити індексацію контенту

Що таке серверні компоненти React?

Серверні компоненти React виконуються на сервері або під час збірки, а їхній результат передається клієнту через протокол RSC. Фреймворк може також згенерувати початковий HTML і надсилати результат потоково. RSC та SSR пов’язані, але не тотожні.

Основні переваги RSC

Завантаження даних:

  • Серверні компоненти можуть звертатися безпосередньо до джерела даних. Це дає змогу уникнути окремих клієнтських запитів для кожного фрагмента інтерфейсу.

Обсяг JavaScript:

  • Менше JavaScript означає менше завантаження, розбору й виконання на пристрої. Код суто серверного компонента лишається на сервері; вкладені клієнтські компоненти все одно потребують JavaScript.

Largest Contentful Paint (LCP):

  • У поєднанні із SSR фреймворк може передати HTML до завершення завантаження клієнтського JavaScript. Це може пришвидшити появу контенту.

Кешування:

  • Дані й результати рендерингу можна кешувати, якщо це підтримує й налаштовано у фреймворку. Автоматичне кешування не є гарантією всіх RSC.

Підходи до рендерингу

Є кілька способів рендерити застосунок. Вибір місця завантаження даних і побудови інтерфейсу впливає на швидкодію. Потрібен баланс між обсягом клієнтського JavaScript, кількістю запитів, свіжістю даних та інтерактивністю.

Клієнтський рендеринг (CSR)

За суто клієнтського рендерингу інтерфейс будується в браузері. Початковий HTML часто майже не містить контенту: потрібно завантажити JavaScript, виконати його й отримати дані. Логіка інтерфейсу та маршрутизація переважно працюють на клієнті. Це зручно для інтерактивних панелей і застосунків реального часу, але має компроміси щодо початкового завантаження та індексації.

Статична генерація (SSG)

Статична генерація створює HTML маршруту заздалегідь, зазвичай під час збірки. Коли приходить запит, його вже можна віддати без повторного рендерингу. Це часто покращує затримку доставки та LCP. TBT й INP також залежать від обсягу клієнтського JavaScript.

Якщо сторінку або значну її частину можна підготувати до запиту користувача, SSG — хороший варіант. Динамічні частини можна доповнювати окремо.

Серверний рендеринг (SSR)

SSR формує HTML під час запиту й надсилає його клієнту. Браузер може показати контент до завантаження JavaScript інтерактивних компонентів. Гідратація пов’язує готовий HTML із клієнтським деревом React та додає інтерактивну поведінку.

Серверні компоненти й клієнтський бандл

Код суто серверних компонентів та їхніх серверних залежностей не потрібно надсилати браузеру. Так можна залишити важку бібліотеку обробки даних на сервері. Але дані, результат RSC та код клієнтських компонентів усе одно передаються мережею. «Нульовий бандл» не означає нульову вартість усієї сторінки.

RSC, CSR та SSR у порівнянні

Щоб краще зрозуміти різницю між RSC, CSR та SSR, розгляньмо завантаження даних і розміри клієнтського коду.

RSC — не один перемикач оптимізації, а архітектура, яку зазвичай використовують через фреймворк. React надає базові можливості, а фреймворк організовує маршрутизацію, збірку й доставку результату.

Приклади RSC, SSR і CSR демонструють:

  1. Відмінності в завантаженні даних.
  2. Відмінності в обсязі клієнтських бандлів.

Використані інструменти:

Приклади завантаження даних і рендерингу

Клієнтський рендеринг (CSR)

import { useEffect, useState } from "react";

const api = {
  fetchUsers(): Promise<DBUser[]> {
    // fetch, handle errors and return DBUser[] | null from API
  },
};

export default function MyComponent() {
  const [users, setUsers] = useState<User[]>([]);

  useEffect(() => {
    const fetchUsers = async () => {
      // Fetch users from api
      const result = await api.fetchUsers();

      // Sort users by name
      const sorted = result.sort((a, b) => a.name.localeCompare(b.name));

      // Replace dateOfBirth property with age
      const users = sorted.map((u) => {
        return {
          id: u.id,
          name: u.name,
          age: new Date().getFullYear() - new Date(u.dateOfBirth).getFullYear() - 1,
        };
      });

      // Set user object to state
      setUsers(users);
    };
    fetchUsers();
  }, []);

  return (
    <main className="main">
      <h3>Hello from client-side rendered components (interactive)</h3>
      {users.length > 0 && (
        <table>
          ... Render data here ...
        </table>
      )}
    </main>
  );
}

Серверний рендеринг (SSR)

const api = {
  fetchUsers(): Promise<DBUser[]> {
    // fetch, handle errors and return DBUser[] | null from API
  },
};

// This gets called on every request
export async function getServerSideProps() {
  // Fetch data from external API
  const result = await api.fetchUsers();

  // Pass data to the page via props
  return { props: { result } };
}

export default function MyComponent({ result }: { result: DBUser[] }) {
  // Sort users by name
  const sorted = result.sort((a, b) => a.name.localeCompare(b.name));

  // Replace dateOfBirth property with age
  const users = sorted.map((u) => {
    return {
      id: u.id,
      name: u.name,
      age: new Date().getFullYear() - new Date(u.dateOfBirth).getFullYear() - 1,
    };
  });

  return (
    <main className="main">
      <h3>Hello from server-side rendered components</h3>
      <table>
        ... Render data here ...
      </table>
    </main>
  );
}

Серверні компоненти React (RSC)

export default async function MyComponent() {
  // Fetch data from DB
  const result = await db.users.findMany();

  // Sort users by name
  const sorted = result.sort((a, b) => a.name.localeCompare(b.name));

  // Replace dateOfBirth property with age
  const users = sorted.map((u) => {
    return {
      id: u.id,
      name: u.name,
      age: new Date().getFullYear() - new Date(u.dateOfBirth).getFullYear() - 1,
    };
  });

  return (
    <main className="main">
      <h3>Hello from react-server-components components (non-interactive)</h3>
      <table>
        ... Render data here ...
      </table>
    </main>
  );
}

Приклади розмірів бандлів

Якщо залежності потрапляють у клієнтський код CSR або SSR, його обсяг може зростати. Додамо дві важкі бібліотеки лише для демонстрації. Це не рекомендація з вибору пакетів: існують інші, зокрема новіші й менші альтернативи.

// Add import statements
import moment from "moment";
import _ from "lodash";

...
// Sort users by name with the help of lodash
const sorted = _.sortBy(result, "name");          // new logic here

...
// Replace dateOfBirth property with age with the help of moment.js
const users = sorted.map((u) => {
  const m = moment(u.dateOfBirth, "MM-DD-YYYY");  // new logic here
  return {
    id: u.id,
    name: u.name,
    age: moment().diff(m, "years", false),        // new logic here
  };
});

Нижче наведено скриншоти оригінальних прикладів 2024 року. Результати залежать від версії фреймворку, залежностей і збірки.

CSR із додатковими бібліотеками

CSR із додатковими бібліотеками: Запитів: 4 · передано: 96.4 kB · ресурси: 281 kB
Запитів: 4 · передано: 96.4 kB · ресурси: 281 kB

CSR без додаткових бібліотек

CSR без додаткових бібліотек: Запитів: 4 · передано: 49.5 kB · ресурси: 146 kB
Запитів: 4 · передано: 49.5 kB · ресурси: 146 kB

SSR із додатковими бібліотеками

SSR із додатковими бібліотеками: Запитів: 12 · передано: 131 kB · ресурси: 387 kB
Запитів: 12 · передано: 131 kB · ресурси: 387 kB

SSR без додаткових бібліотек

SSR без додаткових бібліотек: Запитів: 10 · передано: 85.6 kB · ресурси: 257 kB
Запитів: 10 · передано: 85.6 kB · ресурси: 257 kB

RSC із додатковими бібліотеками

RSC із додатковими бібліотеками: Запитів: 7 · передано: 90.8 kB · ресурси: 295 kB
Запитів: 7 · передано: 90.8 kB · ресурси: 295 kB

RSC без додаткових бібліотек

RSC без додаткових бібліотек: Запитів: 7 · передано: 90.8 kB · ресурси: 295 kB
Запитів: 7 · передано: 90.8 kB · ресурси: 295 kB

Наведені приклади також можна розширити за допомогою Next.js, zod, PostgresDB, drizzle чи Prisma або прямого доступу до бази даних без ORM.

Висновки

  1. Завантаження даних. Клієнтські компоненти зазвичай звертаються до API. Серверний компонент може викликати серверну функцію доступу до бази без клієнтського життєвого циклу або getServerSideProps. Suspense дозволяє показати стан завантаження для частин дерева.
  2. Обсяг JavaScript. Клієнтські залежності мають мережеву й обчислювальну вартість навіть із tree shaking та code splitting. Суто серверні залежності можна залишити поза браузерним бандлом.
  3. Індексація. Змістовний початковий HTML полегшує доступ пошуковиків до контенту. Це не автоматична SEO-гарантія будь-якого RSC-застосунку.

Джерела:

v8.dev/blog/cost-of-javascript-2019
github.com/reactwg/react-18/discussions/37
web.dev/articles/lcp
nextjs.org/docs/app/building-your-application/rendering
Гідратація у веброзробці
plasmic.app/blog/how-react-server-components-work
blog.logrocket.com/deep-dive-react-fiber
react.dev/blog/2023/03/22/react-labs
web.dev/articles/rendering-on-the-web#static_rendering
patterns.dev
nextjs.org/docs/pages/building-your-application/rendering
nextjs.org/docs/app/building-your-application/rendering
nischithbm.medium.com/web
quickbooks-engineering
joshwcomeau.com/react/server-components
debugbear.com/docs/metrics