Skip to main content
Назад к блогу
5 мин чтения

Прогрессивное улучшение в эпоху RSC: что работает, что нет

Server Components изменили, какие паттерны прогрессивного улучшения жизнеспособны. Формы стали лучше, чем когда-либо. Клиентская интерактивность требует осознанных решений. Вот что я вынес из релиза RSC-приложений для реальных пользователей.

next.jsux

Прогрессивное улучшение — идея о том, что страница должна работать без JavaScript и становиться лучше с ним — раньше была простой. Рендери HTML на сервере, добавляй JS для интерактивности. Если JS не загрузился, страница всё равно работает.

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

Что работает лучше, чем раньше

Формы

Это главная победа. Server Actions превращают формы в прогрессивно улучшаемые компоненты по умолчанию.

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 route (лишний файл, лишний код), либо fetch на клиенте (нет JS — нет формы). Теперь прогрессивное улучшение встроено.

Контентные страницы

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

HTML всегда так и делал, но теперь это поведение по умолчанию в React, а не то, за что нужно бороться.

Навигация

Next.js App Router автоматически предзагружает ссылки и обрабатывает навигацию на стороне клиента. Но под капотом ссылки — обычные теги <a>. Если JavaScript упал, клик по ссылке делает полный переход — и это работает нормально.

Что требует осознанных решений

Интерактивные компоненты в серверно-рендеренных страницах

Страница может быть 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 пользователь видит информацию о продукте (хорошо), но кнопка «Add to Cart» не работает (плохо). Вопрос: что должно происходить, когда JS недоступен?

Вариант 1: Fallback-форма. Оберни кнопку в <form> с Server Action. Нет JS — форма отправляется. JS есть — клиентский обработчик берёт управление. Это самое чистое прогрессивное улучшение, но оно работает только для действий, которые можно выразить как отправку формы.

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

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

Я использую Вариант 1 для любого действия, которое можно выразить как форму (добавить в корзину, подписаться, написать, переключить настройки). Вариант 2 — для по-настоящему сложных интерфейсов.

Состояния загрузки

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

Это реальный шаг назад. Пользователь на медленном соединении без JS видит пустую страницу дольше, чем при традиционном серверном рендеринге (который ждёт всех данных перед отправкой ответа).

Митигация: держи Suspense-границы точечными. Не оборачивай всю страницу в один Suspense — оборачивай отдельные медленные секции. Части, не ожидающие данных, рендерятся немедленно как 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

Будь честен с собой — не делай вид, что всё можно прогрессивно улучшить:

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

Для каждой страницы перед релизом я запускаю такой тест: отключаю JavaScript в браузере и загружаю страницу.

  1. Может ли пользователь прочитать контент? Если нет — что-то пошло не так. Контент всегда должен быть HTML.
  2. Может ли пользователь перейти на другие страницы? Ссылки должны работать. Если навигация зависит от JS — это баг.
  3. Может ли пользователь выполнить основное действие? Для страницы контактов: может ли он отправить форму? Для страницы продукта: может ли он хотя бы увидеть детали?
  4. Деградируют ли интерактивные элементы gracefully? Вкладки должны показывать весь контент (не спрятанный за JS-only вкладками). Аккордеоны должны быть раскрыты по умолчанию.

Этот тест занимает 2 минуты на страницу. Он ловит реальные проблемы — не гипотетические «а что если JS упадёт», а настоящие UX-проблемы для пользователей на медленных соединениях, где JS таймаутит.

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

Прогрессивное улучшение — это не про поддержку пользователей, намеренно отключающих JavaScript. Это про устойчивость для пользователей на плохих соединениях, старых устройствах или в ситуациях, когда JS не загружается (ошибка CDN, конфликт с блокировщиком рекламы, корпоративный прокси).

В эпоху RSC дефолтное поведение лучше, чем когда-либо, для контента и форм. Работа — в том, чтобы заставить интерактивные компоненты деградировать gracefully, и честно признавать, какие из них не могут.


Строишь на RSC и хочешь, чтобы страницы работали для всех? Давай поговорим — я делаю прогрессивно улучшаемые приложения, где контент работает даже когда JavaScript не работает.