RTK Query поставляется как часть @reduxjs/toolkit, и его
архитектура изначально ориентирована на tree-shaking и
ESM-совместимость. Однако итоговый размер bundle в реальных проектах
зависит не столько от самой библиотеки, сколько от способа импорта и
организации API-слоёв.
Ключевой момент — различие между пакетами:
@reduxjs/toolkit/query@reduxjs/toolkit/query/reactПервый содержит только ядро логики: создание API, middleware, кэширование и базовые утилиты. Второй добавляет React-адаптер: хуки, интеграцию с жизненным циклом компонентов, дополнительные обёртки.
Использование react-версии автоматически увеличивает
размер сборки, но позволяет использовать useQuery,
useMutation и связанные хуки. В проектах без React или с
гибридной архитектурой имеет смысл ограничиваться
@reduxjs/toolkit/query.
Tree-shaking в RTK Query работает только при соблюдении условий сборщика (Webpack, Vite, Rollup):
requiresideEffects: false в
зависимостиRedux Toolkit уже поставляется с корректной ESM-структурой, но проблемы возникают на уровне пользовательского кода:
import { createApi } from '@reduxjs/toolkit/query/react'
Такой импорт подтягивает React-слой полностью. Если React не используется, это приводит к избыточному коду.
Оптимальный вариант при отсутствии React:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
Один из главных факторов роста bundle — монолитный API-слайс.
Когда в одном createApi описаны десятки эндпоинтов
разных доменов (users, auth, orders, analytics), сборщик вынужден
включать весь файл целиком, даже если используется только часть.
Разделение API на логические домены снижает объем финального бандла за счёт:
Пример структуры:
export const userApi = createApi({
reducerPath: 'userApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getUser: builder.query({ query: (id) => `user/${id}` })
})
})
export const orderApi = createApi({
reducerPath: 'orderApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getOrders: builder.query({ query: () => 'orders' })
})
})
Каждый слайс может быть подключён только в тех частях приложения, где он реально используется.
RTK Query поддерживает инъекцию эндпоинтов через
injectEndpoints. Это один из наиболее эффективных способов
уменьшить initial bundle size.
Базовый API определяется отдельно:
export const baseApi = createApi({
reducerPath: 'baseApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: () => ({})
})
Функциональные модули добавляют эндпоинты только при загрузке соответствующего кода:
const extendedApi = baseApi.injectEndpoints({
endpoints: (builder) => ({
getProfile: builder.query({
query: () => 'profile'
})
})
})
Преимущество этого подхода:
В связке с динамическими импортами ES Modules RTK Query позволяет переносить загрузку API-логики на уровень маршрутов:
const OrdersPage = React.lazy(() => import('./pages/OrdersPage'))
Внутри модуля OrdersPage импортируется только нужный
API:
import { orderApi } from '../services/orderApi'
Такой подход гарантирует, что код RTK Query для конкретного домена не попадёт в initial bundle.
Особенно эффективно в SPA с роутингом:
/users загружает userApi/orders загружает orderApiПакет @reduxjs/toolkit/query/react добавляет
значительный объём кода за счёт:
Если архитектура позволяет, можно разделить слой запросов и UI:
import { createApi } from '@reduxjs/toolkit/query'
и затем вручную обрабатывать dispatch:
dispatch(api.endpoints.getUser.initiate(id))
Это снижает bundle size, но увеличивает сложность. Такой подход оправдан:
Частая проблема — неявное подтягивание всего
@reduxjs/toolkit через неправильные импорты.
Плохая практика:
import { configureStore } from '@reduxjs/toolkit'
сама по себе допустима, но опасность возникает при добавлении дополнительных утилит, если библиотека импортируется целиком в нескольких местах.
Для RTK Query важно избегать:
Правильный подход:
import { createApi } from '@reduxjs/toolkit/query'
или строго React-ориентированный:
import { createApi } from '@reduxjs/toolkit/query/react'
но не смешивать их без необходимости.
baseQuery часто недооценивается как источник увеличения
bundle size.
Использование готовых адаптеров может подтянуть лишние зависимости:
fetchBaseQuery — минимальный и предпочтительный
вариантaxiosBaseQuery могут добавлять axios в
bundleПример минимального решения:
import { fetchBaseQuery } from '@reduxjs/toolkit/query'
Альтернативы с axios:
import axios from 'axios'
в большинстве случаев увеличивают итоговый размер бандла без необходимости.
RTK Query middleware добавляется в store автоматически:
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)
При наличии нескольких API слайсов middleware также растёт.
Оптимизация достигается через:
createApiКаждый endpoint внутри createApi остаётся частью AST
модуля, и даже если он не используется в рантайме, он может попасть в
bundle.
Оптимизация:
Пример условной компиляции:
const endpoints = (builder) => {
const base = {
getUser: builder.query({ query: (id) => `user/${id}` })
}
if (process.env.ENABLE_ADMIN_API) {
base.getAdmin = builder.query({ query: () => 'admin' })
}
return base
}
Redux Toolkit корректно помечает пакеты как ESM, но ошибки появляются при неправильной конфигурации bundler’а.
Критически важно:
"sideEffects": false в package.json проектаimport '@reduxjs/toolkit'Если side effects не настроены, tree-shaking может быть полностью отключён, и RTK Query будет тянуться целиком.
Хотя selectFromResult напрямую не влияет на bundle size,
он влияет на то, какие части кода подтягиваются в зависимости от
использования.
Избыточные селекторы увеличивают связность модулей:
useGetUserQuery(id, {
selectFromResult: ({ data }) => ({ user: data?.user })
})
Чем меньше логики внутри API слоя, тем легче сборщику разделять зависимости.
Типовая оптимизированная архитектура выглядит как:
baseApi без endpointsinjectEndpointsfetchBaseQueryreact-обёртки при
необходимостиТакой подход обеспечивает предсказуемый tree-shaking, уменьшает initial bundle и позволяет масштабировать API без линейного роста размера сборки.