Оптимизация использования тегов

Система тегов в RTK Query представляет собой механизм логической связи между запросами (query) и мутациями (mutation). Теги используются для автоматической инвалидации кэша и повторного обновления данных после изменений на сервере.

При неправильной организации тегов приложение начинает выполнять избыточные сетевые запросы, создавать каскадные перезагрузки данных и перегружать клиентский кэш. В крупных приложениях это приводит к ухудшению производительности, росту количества ререндеров и повышенной нагрузке на API.

Оптимизация тегов направлена на решение нескольких задач:

  • уменьшение количества ненужных рефетчей;
  • сокращение сетевой нагрузки;
  • повышение отзывчивости интерфейса;
  • уменьшение размеров кэша;
  • предотвращение повторных обновлений одинаковых данных;
  • локализация инвалидации только для изменённых сущностей.

Проблема чрезмерной инвалидации

Одна из самых распространённых ошибок — использование одного общего тега для всех запросов.

Пример неоптимальной конфигурации:

tagTypes: ['Posts']

getPosts: build.query({
    query: () => '/posts',
    providesTags: ['Posts']
})

getPost: build.query({
    query: (id) => `/posts/${id}`,
    providesTags: ['Posts']
})

updatePost: build.mutation({
    query: ({ id, ...body }) => ({
        url: `/posts/${id}`,
        method: 'PUT',
        body
    }),
    invalidatesTags: ['Posts']
})

После обновления одной записи будут инвалидированы:

  • список постов;
  • все отдельные посты;
  • все компоненты, использующие эти данные.

Если приложение содержит десятки открытых компонентов, количество повторных запросов может резко увеличиться.


Использование granular tags

Наиболее эффективный подход — разделение тегов по идентификаторам сущностей.

Инвалидация конкретного элемента

getPost: build.query({
    query: (id) => `/posts/${id}`,
    providesTags: (result, error, id) => [
        { type: 'Posts', id }
    ]
})

updatePost: build.mutation({
    query: ({ id, ...body }) => ({
        url: `/posts/${id}`,
        method: 'PUT',
        body
    }),
    invalidatesTags: (result, error, { id }) => [
        { type: 'Posts', id }
    ]
})

Теперь RTK Query обновит только изменённый объект.

Преимущества:

  • минимизация сетевой активности;
  • отсутствие массовых рефетчей;
  • уменьшение нагрузки на React;
  • снижение количества повторных ререндеров.

Комбинация LIST и entity tags

Наиболее распространённая архитектура в RTK Query — использование специальных тегов списка.

Проблема обновления списка

Если инвалидировать только конкретный элемент:

invalidatesTags: [{ type: 'Posts', id: 5 }]

то список постов (getPosts) не обновится.

Это приводит к рассинхронизации:

  • детальная карточка обновлена;
  • список показывает старые данные.

Шаблон LIST

RTK Query рекомендует разделять:

  • тег списка;
  • теги отдельных сущностей.

Оптимальная схема

getPosts: build.query({
    query: () => '/posts',
    providesTags: (result) =>
        result
            ? [
                  ...result.map(post => ({
                      type: 'Posts',
                      id: post.id
                  })),
                  { type: 'Posts', id: 'LIST' }
              ]
            : [{ type: 'Posts', id: 'LIST' }]
})

Мутация создания:

createPost: build.mutation({
    query: (body) => ({
        url: '/posts',
        method: 'POST',
        body
    }),
    invalidatesTags: [
        { type: 'Posts', id: 'LIST' }
    ]
})

Мутация обновления:

updatePost: build.mutation({
    query: ({ id, ...body }) => ({
        url: `/posts/${id}`,
        method: 'PUT',
        body
    }),
    invalidatesTags: (result, error, { id }) => [
        { type: 'Posts', id }
    ]
})

Мутация удаления:

deletePost: build.mutation({
    query: (id) => ({
        url: `/posts/${id}`,
        method: 'DELETE'
    }),
    invalidatesTags: (result, error, id) => [
        { type: 'Posts', id },
        { type: 'Posts', id: 'LIST' }
    ]
})

Минимизация повторных запросов

Не инвалидировать LIST без необходимости

Очень частая ошибка:

invalidatesTags: [
    { type: 'Posts', id: 'LIST' }
]

для любой мутации.

Даже обновление одного поля поста вызовет:

  • полную перезагрузку списка;
  • обновление пагинации;
  • повторную загрузку связанных компонентов.

Если список содержит сотни элементов, это становится дорогостоящей операцией.


Разделение тегов по типам данных

Крупные приложения не должны использовать один tagType для всех сущностей.

Плохой пример:

tagTypes: ['Data']

Правильный пример:

tagTypes: [
    'Posts',
    'Users',
    'Comments',
    'Categories',
    'Notifications'
]

Преимущества:

  • локальная инвалидация;
  • изоляция кэша;
  • отсутствие каскадных обновлений;
  • упрощение поддержки.

Избежание избыточного providesTags

Ошибка полного перечисления

Иногда разработчики создают теги для каждого вложенного объекта:

providesTags: (result) =>
    result.flatMap(post => [
        { type: 'Posts', id: post.id },
        ...post.comments.map(comment => ({
            type: 'Comments',
            id: comment.id
        }))
    ])

Если список содержит тысячи комментариев:

  • увеличивается размер внутреннего state RTK Query;
  • растёт стоимость сравнения тегов;
  • повышается нагрузка на garbage collector;
  • замедляется инвалидация.

Когда детализация становится вредной

Слишком granular tags также ухудшают производительность.

Например:

{ type: 'Posts', id: 'title-5' }
{ type: 'Posts', id: 'content-5' }
{ type: 'Posts', id: 'author-5' }

Такой уровень детализации редко оправдан.

RTK Query оптимален при работе:

  • с сущностями;
  • коллекциями;
  • страницами;
  • агрегатами.

Использование page-based tags

Для пагинации эффективнее использовать теги страниц.

Пример

getPosts: build.query({
    query: (page) => `/posts?page=${page}`,
    providesTags: (result, error, page) => [
        { type: 'Posts', id: `PAGE-${page}` }
    ]
})

Инвалидация:

invalidatesTags: [
    { type: 'Posts', id: 'PAGE-2' }
]

Преимущества:

  • обновляется только нужная страница;
  • уменьшается сетевой трафик;
  • отсутствует полная перезагрузка пагинации.

Гибридные стратегии тегов

В сложных системах используется комбинация:

  • LIST;
  • PAGE;
  • ENTITY.

Пример архитектуры

providesTags: (result, error, page) =>
    result
        ? [
              ...result.items.map(post => ({
                  type: 'Posts',
                  id: post.id
              })),
              { type: 'Posts', id: `PAGE-${page}` },
              { type: 'Posts', id: 'LIST' }
          ]
        : [
              { type: 'Posts', id: `PAGE-${page}` },
              { type: 'Posts', id: 'LIST' }
          ]

Такая архитектура позволяет:

  • обновлять конкретный пост;
  • обновлять одну страницу;
  • обновлять весь список.

Оптимизация infinite scroll

При бесконечной прокрутке неправильная инвалидация особенно опасна.

Проблема

Если инвалидировать:

{ type: 'Posts', id: 'LIST' }

то RTK Query может повторно загрузить:

  • десятки страниц;
  • уже просмотренные данные;
  • старые сегменты ленты.

Сегментированные теги

Для infinite scroll лучше разделять данные:

{ type: 'Feed', id: 'CHUNK-1' }
{ type: 'Feed', id: 'CHUNK-2' }
{ type: 'Feed', id: 'CHUNK-3' }

Это позволяет обновлять только изменённые сегменты.


Исключение тегов для статических данных

Не все запросы нуждаются в тегах.

Пример:

getCountries: build.query({
    query: () => '/countries'
})

Если список стран:

  • почти никогда не меняется;
  • загружается один раз;
  • не обновляется через mutation,

теги становятся лишними.

Избыточное использование тегов:

  • усложняет систему;
  • увеличивает объём metadata;
  • создаёт ненужные зависимости.

Оптимизация nested APIs

В сложных API часто встречаются вложенные структуры.

Пример

/users/5/posts
/users/5/comments
/users/5/friends

Ошибка:

tagTypes: ['Users']

Правильнее:

tagTypes: [
    'Users',
    'UserPosts',
    'UserComments',
    'UserFriends'
]

Это уменьшает область инвалидации.


Оптимизация при нормализации данных

Если используется createEntityAdapter, система тегов должна учитывать нормализованную структуру.

Пример

providesTags: (result) =>
    result
        ? [
              ...result.ids.map(id => ({
                  type: 'Posts',
                  id
              })),
              { type: 'Posts', id: 'LIST' }
          ]
        : [{ type: 'Posts', id: 'LIST' }]

Такой подход хорошо масштабируется на большие объёмы данных.


Снижение количества refetchOnFocus

Частая ошибка — сочетание агрессивной инвалидации с автоматическим рефетчем:

refetchOnFocus: true
refetchOnReconnect: true

Если теги настроены неоптимально:

  • приложение будет постоянно выполнять запросы;
  • увеличится нагрузка на backend;
  • появятся скачки интерфейса.

Оптимизация optimistic updates

При использовании optimistic updates часть инвалидаций можно исключить полностью.

Пример

async onQueryStarted(arg, { dispatch, queryFulfilled }) {
    const patch = dispatch(
        api.util.updateQueryData(
            'getPost',
            arg.id,
            draft => {
                draft.title = arg.title
            }
        )
    )

    try {
        await queryFulfilled
    } catch {
        patch.undo()
    }
}

Если optimistic upd ate полностью синхронизирует данные:

invalidatesTags: []

может быть достаточным решением.

Это особенно полезно:

  • в realtime-интерфейсах;
  • в чатах;
  • в live dashboards;
  • в collaborative systems.

Дедупликация тегов

Иногда providesTags случайно создаёт дубликаты.

Плохой пример:

providesTags: (result) =>
    result.map(item => ({
        type: 'Posts',
        id: item.id
    }))

если API возвращает повторяющиеся элементы.

Оптимизация:

providesTags: (result) => {
    const uniqueIds = [...new Se t(
        result.map(item => item.id)
    )]

    return uniqueIds.map(id => ({
        type: 'Posts',
        id
    }))
}

Оптимизация memory footprint

Каждый тег хранится внутри RTK Query state.

Тысячи тегов могут приводить к:

  • росту памяти;
  • увеличению snapshot Redux DevTools;
  • замедлению сериализации;
  • ухудшению производительности Redux middleware.

Использование abstract tags

Иногда эффективнее использовать логические группы.

Пример

{ type: 'Posts', id: 'TRENDING' }
{ type: 'Posts', id: 'LATEST' }
{ type: 'Posts', id: 'ARCHIVE' }

Это полезно:

  • для фильтров;
  • категорий;
  • сегментов данных;
  • тематических лент.

Инвалидация нескольких сущностей

RTK Query позволяет комбинировать теги.

Пример

invalidatesTags: (result, error, post) => [
    { type: 'Posts', id: post.id },
    { type: 'Users', id: post.userId },
    { type: 'Statistics', id: 'POSTS_COUNT' }
]

Подобная архитектура должна использоваться осторожно.

Слишком большое количество зависимостей:

  • усложняет предсказуемость;
  • увеличивает количество рефетчей;
  • создаёт скрытые цепочки обновлений.

Анализ производительности тегов

При масштабировании приложения полезно анализировать:

  • количество инвалидируемых запросов;
  • размер RTK Query state;
  • число активных subscriptions;
  • частоту повторных запросов;
  • глубину каскадных обновлений.

Основные признаки проблем:

  • одинаковые запросы выполняются слишком часто;
  • один mutation обновляет половину приложения;
  • Redux store быстро растёт;
  • появляются задержки при dispatch;
  • DevTools начинают тормозить.

Практические рекомендации

Для небольших приложений

Подход:

LIST + ENTITY

обычно полностью достаточен.


Для средних приложений

Рекомендуется:

  • разделение tagTypes;
  • page-based tags;
  • selective invalidation;
  • optimistic updates.

Для крупных систем

Эффективны:

  • сегментированные теги;
  • доменные tagTypes;
  • гибридные стратегии;
  • локальная инвалидация;
  • частичный manual cache update;
  • отказ от глобальных LIST invalidation.

Наиболее эффективная стратегия

Для большинства production-приложений оптимальной считается следующая архитектура:

tagTypes: [
    'Posts',
    'Users',
    'Comments'
]

Схема тегов:

LIST
PAGE-x
ENTITY_ID
FILTER-x
CATEGORY-x

Принципы:

  • LIST обновляется только при изменении состава коллекции;
  • ENTITY обновляется при изменении конкретной сущности;
  • PAGE используется для пагинации;
  • FILTER применяется для отдельных выборок;
  • optimistic updates уменьшают количество refetch;
  • статические данные не используют теги без необходимости.