JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Расширенные сценарии: A/B тестирование и интернационализа...

Расширенные сценарии: A/B тестирование и интернационализация (i18n) в Next.js

Модуль 4: Node.js, Next.js и Angular
11 уровень , 8 лекция
Открыта

1. A/B тестирование через middleware

В Next.js 15 middleware работает на уровне edge — максимально быстро, прямо на границе CDN, что делает его отличным инструментом для задач, где важна скорость принятия решения: например, определять, к какому варианту страницы отправить пользователя (A/B тест), или на каком языке ему показать сайт (i18n).

Что такое A/B тестирование?

A/B тестирование — это способ сравнить две (или больше) версии сайта или фичи, чтобы понять, какая из них эффективнее. Обычно часть пользователей видит вариант A, другая — вариант B. Дальше вы собираете статистику: кто больше кликает, покупает, остаётся дольше и т.д.

В Next.js A/B тестирование можно реализовать на уровне middleware, потому что оно позволяет:

  • Делить пользователей на группы до того, как они увидят страницу.
  • Сохранять выбранный вариант в cookie, чтобы пользователь всегда видел одну и ту же версию.
  • Делать выборку случайной или по какому-то признаку (например, по географии, браузеру и т.д.).

Пример: Реализация простого A/B теста

Представим, что у нас есть две версии главной страницы: / (вариант A) и /variant-b (вариант B). Мы хотим, чтобы 50% пользователей видели вариант B, остальные — вариант A.

Структура файлов


/app
  /page.tsx           // Вариант A
  /variant-b/page.tsx // Вариант B
/middleware.ts        // Наш middleware

Пример кода middleware для A/B теста


// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Проверяем, что это запрос к корню сайта ("/")
  if (request.nextUrl.pathname === '/') {
    // Проверяем, есть ли уже cookie с вариантом
    const abCookie = request.cookies.get('ab-variant');
    let variant = abCookie?.value;

    if (!variant) {
      // Если cookie нет — определяем вариант случайно
      variant = Math.random() < 0.5 ? 'A' : 'B';
    }

    // Сохраняем выбор в cookie (чтобы пользователь всегда видел одну версию)
    const response =
      variant === 'B'
        ? NextResponse.redirect(new URL('/variant-b', request.url))
        : NextResponse.next();

    response.cookies.set('ab-variant', variant, { path: '/', maxAge: 60 * 60 * 24 * 30 });

    return response;
  }

  // Для остальных маршрутов ничего не делаем
  return NextResponse.next();
}

Как это работает?

  • Пользователь заходит на /.
  • Middleware проверяет, есть ли cookie ab-variant.
  • Если нет — случайно выбирает вариант (A или B).
  • Если выбран вариант B — делает редирект на /variant-b, иначе пропускает дальше.
  • Устанавливает cookie, чтобы при следующем заходе пользователь попал в ту же группу.
  • Всё это происходит мгновенно, ещё до рендера страницы.

Пример страницы B


// app/variant-b/page.tsx
export default function VariantBPage() {
  return <h1>Это страница B (экспериментальная версия)</h1>;
}

А если вариантов больше?

Можно хранить в cookie не только "A" или "B", а, например, номер варианта (1, 2, 3), или даже делать выборку по сложным правилам.

Как собирать аналитику?

Обычно вы отправляете событие в свою систему аналитики (например, Google Analytics, Яндекс.Метрика или что-то кастомное) — например, вместе с событием "Открыта страница" передаёте значение cookie ab-variant. Это позволяет анализировать поведение каждой группы отдельно.

2. Интернационализация (i18n) через middleware

i18n (от английского Internationalization) — это "международализация", т.е. поддержка нескольких языков/регионов в вашем приложении. В Next.js есть встроенная поддержка i18n, но middleware позволяет реализовать более гибкие сценарии — например, автоматически перенаправлять пользователя на нужную языковую версию сайта в зависимости от его браузера, IP или предпочтений.

Пример: Автоматическое определение языка

Предположим, у нас есть две версии сайта: /ru и /en. Мы хотим, чтобы:

  • Если пользователь заходит на /, его автоматически редиректило на /ru или /en в зависимости от его языка.
  • Если пользователь уже зашёл на /ru или /en, ничего не делать.
  • Язык сохранялся в cookie, чтобы при следующем заходе пользователь попадал на свой язык.

Структура файлов


/app
  /ru/page.tsx
  /en/page.tsx
/middleware.ts

Пример кода middleware для i18n


// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const SUPPORTED_LOCALES = ['en', 'ru'];
const DEFAULT_LOCALE = 'en';

function getPreferredLocale(request: NextRequest) {
  // 1. Читаем из cookie
  const localeCookie = request.cookies.get('locale')?.value;
  if (localeCookie && SUPPORTED_LOCALES.includes(localeCookie)) {
    return localeCookie;
  }

  // 2. Пробуем из заголовка Accept-Language
  const acceptLang = request.headers.get('accept-language');
  if (acceptLang) {
    const found = SUPPORTED_LOCALES.find((loc) =>
      acceptLang.toLowerCase().startsWith(loc)
    );
    if (found) return found;
  }

  // 3. По умолчанию
  return DEFAULT_LOCALE;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Если уже на /ru или /en — ничего не делаем
  if (SUPPORTED_LOCALES.some((loc) => pathname.startsWith(`/${loc}`))) {
    return NextResponse.next();
  }

  // Определяем нужный язык
  const locale = getPreferredLocale(request);

  // Сохраняем язык в cookie и делаем редирект
  const response = NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
  response.cookies.set('locale', locale, { path: '/', maxAge: 60 * 60 * 24 * 30 });

  return response;
}

Как это работает?

  • Пользователь заходит на / или на любой маршрут без языка.
  • Middleware определяет язык: сначала по cookie, потом по заголовку браузера, потом дефолт.
  • Делаем редирект на нужный язык (/ru или /en).
  • Устанавливаем cookie, чтобы не определять язык заново при каждом заходе.

Пример страницы


// app/ru/page.tsx
export default function RussianPage() {
  return <h1>Добро пожаловать!</h1>;
}

// app/en/page.tsx
export default function EnglishPage() {
  return <h1>Welcome!</h1>;
}

Как добавить выбор языка вручную?

На сайте можно сделать переключатель языка, который меняет cookie locale и делает редирект на нужную версию. Middleware автоматически подхватит этот cookie при следующих заходах.

3. Комбинированные сценарии: A/B + i18n

А что если хочется и A/B тест, и интернационализацию? Например, чтобы в каждой языковой версии были свои варианты A/B теста. В middleware можно реализовать оба сценария: сначала определять язык, потом — вариант теста.


export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const SUPPORTED_LOCALES = ['en', 'ru'];
  const DEFAULT_LOCALE = 'en';

  // 1. Проверяем язык
  let locale = request.cookies.get('locale')?.value;
  if (!locale || !SUPPORTED_LOCALES.includes(locale)) {
    const acceptLang = request.headers.get('accept-language');
    locale = SUPPORTED_LOCALES.find((loc) =>
      acceptLang?.toLowerCase().startsWith(loc)
    ) || DEFAULT_LOCALE;
    // Редиректим на нужный язык, если не на /ru или /en
    if (!SUPPORTED_LOCALES.some((loc) => pathname.startsWith(`/${loc}`))) {
      const response = NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
      response.cookies.set('locale', locale, { path: '/', maxAge: 60 * 60 * 24 * 30 });
      return response;
    }
  }

  // 2. A/B тестирование только на главной странице каждого языка
  for (const loc of SUPPORTED_LOCALES) {
    if (pathname === `/${loc}`) {
      let variant = request.cookies.get('ab-variant')?.value;
      if (!variant) {
        variant = Math.random() < 0.5 ? 'A' : 'B';
      }
      if (variant === 'B') {
        const response = NextResponse.redirect(new URL(`/${loc}/variant-b`, request.url));
        response.cookies.set('ab-variant', variant, { path: '/', maxAge: 60 * 60 * 24 * 30 });
        return response;
      }
      // Для варианта A ничего не делаем, просто сохраняем cookie
      const response = NextResponse.next();
      response.cookies.set('ab-variant', variant, { path: '/', maxAge: 60 * 60 * 24 * 30 });
      return response;
    }
  }

  // Остальные маршруты не трогаем
  return NextResponse.next();
}

4. Практические нюансы и особенности

  • Middleware не может работать с динамическими данными из базы: Всё, что нужно для принятия решения, должно быть доступно быстро (cookie, заголовки, путь, query-параметры).
  • Не злоупотребляйте сложной логикой: Middleware должен быть быстрым! Не делайте в нём тяжёлых вычислений или долгих запросов.
  • Edge middleware работает очень быстро, но есть ограничения: Например, нельзя использовать некоторые Node.js API (например, fs, net и т.п.)
  • Редиректы в middleware — это обычная практика: Пользователь даже не заметит, что его перенаправили — всё происходит на edge-уровне и очень быстро.

5. Типичные ошибки при реализации A/B тестов и i18n в middleware

Ошибка №1: Не сохраняется вариант в cookie.
Если вы не сохраняете выбранный вариант A/B теста в cookie, пользователь может каждый раз попадать в новую группу при каждом заходе. Это испортит статистику и опыт пользователя. Всегда сохраняйте выбор!

Ошибка №2: Редирект зацикливается.
Если вы не проверяете, что пользователь уже находится на нужной странице (/variant-b или /ru), можно попасть в бесконечный цикл редиректов. Перед редиректом всегда проверяйте текущий путь!

Ошибка №3: Неправильная обработка языков.
Если вы не проверяете, что путь уже содержит язык (/ru, /en), middleware будет постоянно редиректить пользователя, даже если он уже на нужной странице.

Ошибка №4: Использование тяжёлых операций в middleware.
Middleware должен быть лёгким и быстрым. Не делайте в нём запросы в базу или сторонние API — это замедлит ответы всем пользователям.

Ошибка №5: Неочевидная логика выбора варианта.
Если вы используете слишком сложные условия для выбора A/B варианта или языка, это затруднит отладку и может привести к неожиданным результатам. Старайтесь держать логику максимально прозрачной и тестируемой.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ