EAS Update в 2026: OTA-обновления React Native и Expo — полное руководство

Практическое руководство по EAS Update в 2026: настройка expo-updates, runtime versions с fingerprint, каналы и ветки, публикация через EAS CLI, откаты, постепенное развёртывание и автоматизация в GitHub Actions.

Обновлено: 21 июля 2026

EAS Update, это сервис Expo для доставки OTA-обновлений JavaScript-бандла и ассетов в React Native приложения без публикации новой сборки в App Store или Google Play. Он работает поверх библиотеки expo-updates, использует runtime versions для защиты от несовместимости с нативным кодом и заменяет закрытый в 2024 году Microsoft CodePush. В этом руководстве я разбираю рабочий процесс EAS Update в 2026 году: настройку, публикацию, откаты, постепенное развёртывание и интеграцию с CI.

  • EAS Update доставляет JS-бандл и ассеты за секунды, но не может обновлять нативный код (для него нужен новый билд).
  • Runtime version решает главную проблему OTA: не даёт отправить бандл, требующий отсутствующего нативного модуля.
  • В 2026 году рекомендуется использовать runtimeVersion.policy = "fingerprint", Expo сам вычисляет хеш нативного слоя.
  • Каналы (channels) связывают билды с ветками (branches) обновлений; одна команда eas update публикует патч всем пользователям канала.
  • Хук useUpdates из expo-updates даёт UI-компоненту полный контроль над проверкой и применением апдейта.
  • Откат делается публикацией нового обновления, помеченного как rollback; старый бандл остаётся в кэше, но не активируется.

Что такое EAS Update и как работает OTA в React Native

EAS Update, это часть экосистемы Expo Application Services, которая обслуживает JavaScript-бандлы, ассеты и метаданные приложения через CDN. Когда я перешёл на React Native из веба, аналогия оказалась почти прямой: React-приложение в вебе получает новую версию бандла при следующем визите на страницу, а EAS Update даёт то же самое для мобильного клиента, новый JS-код доставляется в уже установленное приложение при следующем запуске.

Технически поток выглядит так. Клиент, использующий библиотеку expo-updates, при старте (или по явному вызову) обращается к серверу обновлений, отправляет свою runtime version и текущий channel, получает манифест с URL нового бандла, скачивает его и при следующей загрузке монтирует новую версию JS-кода. Нативный слой приложения (то есть скомпилированный Objective-C/Swift и Java/Kotlin) не меняется. Именно эта граница определяет, что можно доставить через OTA, а что требует нового билда в сторах.

Честно говоря, разница между JS-обновлением и обновлением нативного модуля критична. Если вы поменяли поведение экрана, добавили условие, обновили строки локализации, изменили запрос к API, это OTA. Если вы добавили новый нативный модуль, обновили expo-camera с новой нативной зависимостью, включили новую архитектуру Fabric, нужен новый build. Подробнее про новую архитектуру я писал в отдельном материале про миграцию на Fabric и TurboModules.

EAS Update vs CodePush: почему миграция в 2026 году

Microsoft объявил о прекращении поддержки CodePush в марте 2024 года, и к 2026 году сервис официально отключён. Для команд, которые исторически использовали CodePush, миграция уже не «одна из опций», а обязательный шаг; вопрос лишь в выборе замены. EAS Update занял основную долю рынка, потому что предлагает управляемую инфраструктуру, интегрированную с EAS Build, и не требует поднимать собственный self-hosted сервис.

ХарактеристикаEAS UpdateCodePush (отключён)
Статус в 2026Активно развиваетсяОтключён (март 2025)
Модель совместимостиRuntime version + fingerprintApp version + label
Поддержка Expo SDKНативноТребовал ручной интеграции
Каналы / развёртываниеКаналы + ветки + rolloutDeployment (staging/production)
Интеграция с CIОфициальные GitHub Actionsappcenter-cli
Инкрементные обновленияДельта-ассеты через CDNПолный бандл при каждом апдейте
СтоимостьБесплатно до 1000 MAUБыл бесплатен без лимитов

Если вы всё ещё поддерживаете CodePush-конфигурацию из старой ветки, заведите миграционный план на ближайший квартал. Механизм runtime version в EAS Update концептуально мощнее CodePush-овских label'ов, но требует одноразовой перенастройки CI и клиентского кода. Хорошая новость: миграция полностью обратно совместима на уровне бизнес-логики. Вам не нужно переписывать сами компоненты, только точку входа приложения и pipeline публикации.

Установка и настройка expo-updates

Установка библиотеки одинакова для managed и bare-workflow. В Expo-проекте выполните команду через expo install, чтобы получить версию, совместимую с текущим SDK, а затем добавьте плагин в app.json.

npx expo install expo-updates
npx eas init
npx eas update:configure

Команда eas update:configure делает три вещи: создаёт проект в EAS, добавляет URL сервера обновлений в expo.updates.url и прописывает runtimeVersion policy в app.json. По умолчанию с 2025 года выбирается policy "fingerprint", рекомендованный вариант для большинства проектов.

{
  "expo": {
    "name": "MyApp",
    "slug": "my-app",
    "runtimeVersion": {
      "policy": "fingerprint"
    },
    "updates": {
      "url": "https://u.expo.dev/00000000-0000-0000-0000-000000000000",
      "requestHeaders": {
        "expo-channel-name": "production"
      }
    },
    "plugins": ["expo-updates"]
  }
}

Для React Native CLI-проектов без Expo нужно дополнительно установить expo и expo-modules-autolinking, запустить npx install-expo-modules@latest и вручную инициализировать UpdatesModule в AppDelegate (iOS) и MainApplication (Android). Официальная документация ведёт через каждый шаг, смотрите гайд EAS Update: Getting Started.

Runtime version и совместимость с нативным кодом

Runtime version, это самая тонкая и одновременно самая важная часть EAS Update. Она отвечает за один вопрос: совместим ли этот JS-бандл с нативным слоем, установленным на устройстве пользователя? Ответ должен быть автоматическим, иначе OTA превратится из инструмента ускорения в источник крашей.

С Expo SDK 51 доступна policy "fingerprint". Инструмент @expo/fingerprint вычисляет хеш всех нативных зависимостей, config-плагинов, версий SDK и файлов, влияющих на нативную сборку. Хеш попадает в runtime version автоматически при eas build. Если вы добавили нативный модуль, fingerprint изменится, и старые OTA-обновления не будут приниматься новым билдом (и наоборот).

// app.json, рекомендованная конфигурация для 2026
{
  "expo": {
    "runtimeVersion": {
      "policy": "fingerprint"
    }
  }
}

// Альтернативные policies:
// "sdkVersion" — привязка к версии Expo SDK
// "appVersion" — привязка к версии из app.json (не рекомендуется)
// { "policy": "fingerprint", "fingerprintSources": [...] } — ручное задание источников

Чтобы увидеть, какой fingerprint будет использован для локальной сборки, выполните команду:

npx expo-doctor
npx expo fingerprint:generate --debug

Вывод покажет каждый файл и зависимость, попавшие в хеш. Это полезно, когда вы гадаете, почему EAS Build пересобирает нативную часть; обычно причина в изменении одного из expo.plugins или в новой версии пакета с нативными исходниками. Аналогия из веба: fingerprint работает почти как integrity-хеш в SRI-заголовках для <script>. Если содержимое расходится с ожидаемым, клиент отказывается его исполнять.

Каналы, ветки и релизные потоки в EAS Update

Модель EAS Update строится на трёх сущностях: channel, branch и update. Channel, это то, к чему привязан установленный на устройстве билд (задаётся в eas.json при сборке). Branch, это «поток» обновлений, который вы публикуете из своего репозитория. Update, конкретный опубликованный бандл. Channel маппится на branch, и это соответствие можно менять без пересборки приложения.

// eas.json
{
  "build": {
    "development": {
      "developmentClient": true,
      "channel": "development"
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview"
    },
    "production": {
      "channel": "production"
    }
  }
}

Практический паттерн, который я использую в командах: одна ветка Git, одна branch в EAS Update. Feature-ветки публикуют свои обновления на одноимённую EAS-branch, привязанную к preview-channel через override. Основная main публикует на branch production. Тестировщики ставят один preview-билд и переключаются между feature-обновлениями простой командой:

eas channel:edit preview --branch feature-onboarding-v2

Такая гибкость невозможна с CodePush и была одной из главных причин миграции: QA больше не собирает отдельный TestFlight-билд для каждой ветки, а получает мгновенный переключатель. Похожий подход к параллельному тестированию UI я описывал в статье про тестирование React Native с Maestro.

Как опубликовать OTA-обновление через EAS CLI

Публикация делается одной командой из корня проекта. EAS CLI собирает JS-бандл через Metro, вычисляет runtime version, загружает ассеты в CDN и создаёт новый update в указанной branch.

# Публикация на конкретную branch
eas update --branch production --message "Fix: onboarding button crash"

# Автоматическое определение branch по текущей Git-ветке
eas update --auto

# Публикация только для одной платформы
eas update --branch production --platform ios --message "iOS-only hotfix"

Флаг --auto использует имя текущей Git-ветки как имя EAS-branch и первую строку последнего коммита как message. Это удобно для рабочего процесса, где ветка репозитория и ветка обновлений называются одинаково. Для production-релизов я рекомендую всегда указывать branch явно и писать осмысленный message: он будет виден в EAS dashboard и в аналитике rollout.

После публикации команда выводит QR-код, ведущий на диагностическую страницу с манифестом. Отсканируйте его в приложении Expo Go или в дев-клиенте, вы увидите точный JSON-манифест, который получит клиент. Это спасает от ситуаций типа «обновление опубликовалось, но не приезжает»: если runtime version в манифесте не совпадает с версией на устройстве, будет видно сразу. По моему опыту, около 80% багов «почему OTA не работает» решаются именно на этом шаге, до того как вы полезли смотреть логи expo-updates.

Проверка и применение обновлений в приложении

По умолчанию expo-updates проверяет наличие обновления при каждом холодном старте приложения. Поведение контролируется параметром updates.checkAutomatically в app.json (значения ON_LOAD, ON_ERROR_RECOVERY, WIFI_ONLY, NEVER). Для большинства продуктов автоматической проверки достаточно, но иногда нужен UI-контроль: показать пользователю уведомление «Доступно обновление» и дать нажать «Обновить сейчас».

С 2024 года для этого есть удобный хук useUpdates. Он возвращает реактивное состояние: статус проверки, доступный апдейт, ошибку и метаданные. Ниже, рабочий компонент, который я использую в почти каждом проекте:

import { useEffect } from 'react';
import { Alert } from 'react-native';
import * as Updates from 'expo-updates';

export function UpdatePrompt() {
  const {
    isUpdateAvailable,
    isUpdatePending,
    downloadedUpdate,
  } = Updates.useUpdates();

  // Периодическая проверка (например, при возврате в foreground)
  useEffect(() => {
    if (__DEV__) return;
    const interval = setInterval(() => {
      Updates.checkForUpdateAsync().catch(() => {});
    }, 15 * 60 * 1000);
    return () => clearInterval(interval);
  }, []);

  useEffect(() => {
    if (!isUpdatePending) return;
    Alert.alert(
      'Обновление готово',
      'Приложение перезапустится, чтобы применить изменения.',
      [{ text: 'Перезапустить', onPress: () => Updates.reloadAsync() }],
    );
  }, [isUpdatePending]);

  return null;
}

Обратите внимание на разделение состояний. isUpdateAvailable означает, что сервер сказал «есть новее», а isUpdatePending сигнализирует, что бандл уже скачан и готов к применению. Обычно скачивание происходит автоматически после проверки, но если вы отключили auto-download через fetchAutomatically: false, придётся вызвать Updates.fetchUpdateAsync() вручную. Такое разделение полезно, когда пользователь на мобильной сети и вы хотите скачивать бандл только по Wi-Fi.

Откаты и постепенное развёртывание (rollout)

OTA снижает риск релиза, но не устраняет его. В 2026 году EAS Update даёт два инструмента для управления рисками: rollback и rollout. Rollout публикует обновление процентной доле пользователей канала: начните с 5%, следите за crash-free rate в Sentry, увеличивайте до 100% или откатывайте.

# Публикация с rollout на 10% пользователей канала production
eas update --branch production --rollout-percentage 10 --message "New feed layout"

# Постепенное увеличение доли
eas update:edit --rollout-percentage 50
eas update:edit --rollout-percentage 100

# Полный откат: обновление, которое возвращает клиентов на предыдущий бандл
eas update:rollback --branch production

Механика rollback принципиально важна. EAS не «удаляет» обновление с CDN, а публикует новый update, помеченный как rollback-to-embedded (или к явно указанному предыдущему update). Клиенты получают этот rollback как обычное обновление, и expo-updates откатывается к встроенному в билд бандлу или к более старому кэшированному. Никакого магического «удалить с устройств» не существует, только новый апдейт может отменить предыдущий.

Для команд с высоким SLA я советую связку EAS Update + Sentry Release Health: настройте автоматическое уведомление в Slack, если crash-free session rate падает ниже 99.5% после публикации, и держите rollback-скрипт в CI. Про интеграцию Sentry-подобных инструментов я подробнее писал в материале про отладку и профилирование React Native.

Автоматизация EAS Update через GitHub Actions

В 2026 году официальный action expo/expo-github-action покрывает 95% сценариев. Ниже, workflow, который я использую для двух потоков: preview для feature-веток, production для merge в main.

# .github/workflows/eas-update.yml
name: EAS Update

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}

      - run: npm ci

      - name: Publish preview update
        if: github.event_name == 'pull_request'
        run: eas update --branch pr-${{ github.event.number }} --message "PR #${{ github.event.number }}: ${{ github.event.pull_request.title }}" --non-interactive

      - name: Publish production update
        if: github.ref == 'refs/heads/main'
        run: eas update --branch production --message "${{ github.event.head_commit.message }}" --non-interactive

Ключевой момент, это EXPO_TOKEN. Сгенерируйте его в EAS dashboard (Access Tokens) с минимальными правами: только update, без прав на build или submit. Так вы ограничите blast radius, если токен утечёт.

Для PR-веток удобно ставить на канал preview policy «always fetch latest» и постить в PR комментарий с QR-кодом обновления. Тестировщик сканирует, переключает preview-билд на нужную ветку и проверяет фичу без пересборки. Экшн expo/expo-github-action/preview-comment делает это одной строкой. Полный список параметров action-а описан на его странице в GitHub.

Типичные ошибки и как их избежать

За последние два года я собрал список ошибок, которые повторяются в почти каждом проекте, впервые внедряющем EAS Update. Разберём самые болезненные.

Пуш нативного изменения через OTA. Классика: разработчик обновил пакет с нативной зависимостью, локально всё работает (потому что expo prebuild регенерировал нативные файлы), и он пушит бандл через eas update. У пользователей приложение крашится на старте, модуля-то нет. Fingerprint runtime version предотвращает это, но только если вы его не отключили вручную. Я лично словил эту грабельку, когда выкатывал обновление pdf-вьювера и забыл, что новая версия тянет native binding.

Забытая пересборка после обновления Expo SDK. При апгрейде SDK меняется runtime version, и уже установленные приложения перестают получать обновления. Не забудьте выпустить новый билд в сторы одновременно с апгрейдом, иначе аудитория окажется на «замороженной» версии, пока не обновит приложение через сторы.

Отсутствие мониторинга размера бандла. OTA-обновления скачиваются по мобильной сети. Если бандл вырос с 3 до 12 МБ из-за случайно импортированной библиотеки, часть пользователей на медленном 3G просто не докачает его. Добавьте проверку размера в CI, например через expo export плюс du -sh dist.

Публикация с ошибкой в коде. В отличие от билда в стор, OTA-обновление применяется всем пользователям канала в течение часов. Всегда тестируйте на preview-канале перед production, используйте rollout ≤ 10% для рискованных изменений и держите наготове команду отката.

Игнорирование политики Apple. Apple App Store Review Guidelines разрешают OTA-обновления, но с ограничением: обновление не должно менять «основное назначение» приложения. Изменение UI, багфиксы, обновление контента, всё это легально. Добавление нового платного контента, изменение продукта под другую целевую аудиторию, обход App Review, это прямое нарушение и повод для бана. Смотрите пункт 3.2.2 в App Store Review Guidelines.

Часто задаваемые вопросы

Можно ли использовать EAS Update без Expo?

Да. С 2025 года EAS Update официально поддерживает React Native CLI-проекты. Достаточно установить expo, expo-modules-autolinking и expo-updates, выполнить npx install-expo-modules@latest и настроить нативные модули согласно документации. Managed workflow не требуется.

Сколько стоит EAS Update в 2026 году?

Free-план даёт 1000 месячных активных пользователей (MAU) без ограничения на количество обновлений. Тариф Production включён в подписки Expo от $19/месяц и покрывает до 200 000 MAU; сверх этого $0.005 за MAU. Приватные CDN-инстансы доступны на Enterprise-плане.

Как откатить неудачное OTA-обновление?

Выполните eas update:rollback --branch <channel-branch>. Команда публикует новый update с флагом rollback, и клиенты при следующей проверке вернутся к предыдущему кэшированному бандлу либо к встроенному в билд. Физического удаления обновления с CDN не происходит: только новый апдейт может отменить предыдущий.

В чём разница между EAS Update и обычным обновлением через App Store?

EAS Update доставляет только JavaScript-бандл и ассеты (обычно 1–5 МБ), применяется за секунды и не требует апрува. Обновление через App Store доставляет весь нативный код и требует ревью 1–3 дня. OTA годится для JS-фиксов, стор-релиз для нативных изменений: новые модули, апгрейд Expo SDK, изменения архитектуры.

Что такое runtime version в EAS Update?

Runtime version, это идентификатор совместимости между JS-бандлом и нативным слоем. Клиент получает только те обновления, у которых runtime version совпадает с версией установленного билда. Рекомендованная policy fingerprint вычисляет хеш всех нативных зависимостей автоматически и не даёт отправить несовместимый бандл.

Anita Iyer
Об авторе Anita Iyer

Cross-platform mobile developer who came to RN from web. Bridges the two worlds and explains the seams.