Skip to main content
Назад до блогу
5 хв читання

Progressive enhancement в епоху RSC: що працює, що — ні

Server Components змінили, які патерни progressive enhancement взагалі реальні. Форми — краще, ніж будь-коли. Клієнтська інтерактивність потребує більше думання. Ось що я виніс зі shipped RSC-застосунків на реальних юзерах.

next.jsux

Progressive enhancement — ідея, що сторінка має працювати без JavaScript і ставати кращою з ним — колись була простою. Рендери HTML на сервері, трохи JS для взаємодій. Якщо JS не завантажився — сторінка все одно працює.

React Server Components змінюють рівняння. Одні речі стали простішими, ніж будь-коли. Інші вимагають усвідомленого вибору. Ось що я виніс із shipped RSC-застосунків на Next.js на реальних юзерах.

Що стало кращим

Форми

Це найбільший виграш. Server Actions перетворюють форми на progressively enhanced компоненти за замовчуванням.

async function subscribe(formData: FormData) {
  'use server'
  const email = formData.get('email') as string
  await db.subscribers.create({ data: { email } })
}

export default function NewsletterForm() {
  return (
    <form action={subscribe}>
      <input type="email" name="email" required />
      <button type="submit">Subscribe</button>
    </form>
  )
}

Ця форма працює з вимкненим JavaScript. Браузер відправляє стандартний POST-запит, server action виконується, сторінка перезавантажується. З увімкненим JavaScript React обробляє все на клієнті — без перезавантаження, миттєвий фідбек.

До Server Actions форма потребувала або окремого API-роуту (зайвий файл, зайвий код), або клієнтського fetch (нема JS — нема форми). Тепер progressive enhancement вбудований.

Контентні сторінки

Server Components рендеряться в HTML на сервері. Пости блогу, документація, маркетингові сторінки — приходять як повноцінний HTML. Гідрація не потрібна для статичного контенту. Браузер рендерить їх миттєво, і вони відмінно працюють без JavaScript.

Це те, що HTML завжди і робив, але тепер це дефолт у React, а не щось, за що треба боротися.

Навігація

Next.js App Router prefetch-ить посилання і обробляє клієнтську навігацію автоматично. Але під капотом посилання — це звичайні теги <a>. Якщо JavaScript не працює, клік по посиланню робить повне page navigation — і це нормально.

Що потребує думання

Інтерактивні компоненти в серверно-рендерених сторінках

Сторінка може бути Server Component із кишенями клієнтської інтерактивності. Типовий патерн:

// Server Component — рендериться в HTML, без JS
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id)

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {/* Client Component — потребує JS */}
      <AddToCartButton productId={product.id} />
    </main>
  )
}

Без JavaScript юзер бачить інформацію про продукт (добре), але кнопка «Додати до кошика» не працює (погано). Питання: що має відбуватись, коли JS недоступний?

Варіант 1: Fallback-форма. Обгорни кнопку у <form> із Server Action. Немає JS? Форма сабмітиться. JS є? Клієнтський обробник бере управління. Це найчистіший progressive enhancement, але він працює тільки для дій, які можна виразити як form submission.

Варіант 2: Прийняти обмеження. Для складних взаємодій (drag-and-drop, real-time оновлення, canvas-based UI) HTML-фолбеку не існує. Задокументуй вимогу до JS і переконайся, що сторінка деградує коректно — показує контент, ховає нефункціональні елементи.

Варіант 3: Повідомлення через <noscript>. Показуй повідомлення при вимкненому JS: «Увімкніть JavaScript для повного функціоналу.» Чесно, але виглядає як відмова від відповідальності для простих взаємодій.

Я використовую Варіант 1 для будь-якої дії, яку можна виразити як форму (додати до кошика, підписатись, контакт, toggle налаштувань). Варіант 2 — для справді складних UI.

Стани завантаження

Server Components стримують — shell рендериться миттєво, повільні дані приходять пізніше. З JavaScript юзер бачить Suspense-межі і стани завантаження. Без JavaScript — нічого, поки вся сторінка не завантажиться.

Це справжнє погіршення. Юзер на повільному з'єднанні без JS бачить порожню сторінку довше, ніж на традиційній серверно-рендереній сторінці (яка чекає всі дані перед відправкою відповіді).

Митигація: тримай Suspense-межі вузькими. Не обгортай всю сторінку в один Suspense — обгортай окремі повільні секції. Частини, які не await-ять дані, рендеряться одразу як HTML незалежно від підтримки JS.

Клієнтський стан

Будь-який стан в useState, useReducer або бібліотеці управління станом потребує JavaScript. Кошики, фільтри, табки, акордеони — якщо вони існують лише в клієнтському стані, вони зникають без JS.

Патерн, що працює: кодуй стан в URL.

// Замість useState для фільтрів
// URL: /products?category=shoes&sort=price

export default async function Products({ searchParams }) {
  const products = await getProducts({
    category: searchParams.category,
    sort: searchParams.sort,
  })

  return (
    <form method="get">
      <select name="category">
        <option value="shoes">Shoes</option>
        <option value="shirts">Shirts</option>
      </select>
      <button type="submit">Filter</button>
    </form>
  )
}

URL-based стан працює без JavaScript (форма сабмітиться, сторінка перезавантажується з новими параметрами). З JavaScript можна перехоплювати форму і оновлювати клієнтський стан через router.push.

Що не працює без JavaScript

Будь чесним щодо цього — не вдавай, що можна progressively enhanced все:

Практичний тест

Для кожної сторінки, яку я шипаю, я проводжу такий тест: вимикаю JavaScript у браузері і завантажую сторінку.

  1. Чи може юзер читати контент? Якщо ні — щось не так: контент завжди має бути HTML.
  2. Чи може юзер переходити на інші сторінки? Посилання мають працювати. Якщо навігація залежить від JS — це баг.
  3. Чи може юзер виконати основну дію? Для контактної сторінки: чи може він відправити форму? Для сторінки продукту: чи може він хоча б побачити деталі продукту?
  4. Чи деградують інтерактивні елементи коректно? Табки мають показувати весь контент (а не ховати його за JS-only-табками). Акордеони мають бути розкриті за замовчуванням.

Цей тест займає 2 хвилини на сторінку. Він виловлює реальні проблеми — не гіпотетичні сценарії «а що як JS зламається», а справжні UX-проблеми для юзерів на повільних з'єднаннях, де JS вичерпує таймаут.

Чесна позиція

Progressive enhancement — не про підтримку юзерів, які навмисно вимикають JavaScript. Це про стійкість для юзерів на поганих з'єднаннях, старих пристроях або в ситуаціях, де JS не завантажується (помилка CDN, конфлікт з блокувальником реклами, корпоративний проксі).

В епоху RSC дефолт кращий, ніж будь-коли — для контенту і форм. Робота полягає в тому, щоб інтерактивні компоненти деградували коректно — і щоб бути чесним щодо тих, які не можуть.


Будуєш із RSC і хочеш, щоб твої сторінки працювали для всіх? Пиши — я шипаю progressively enhanced застосунки, де контент працює навіть коли JavaScript не завантажується.