Олег Марков
Недостатки Feature Sliced Design - подробный разбор проблем и подводных камней
Введение
Feature-Sliced Design (FSD) часто подается как современный и удобный подход к архитектуре фронтенда, особенно в React-проектах. Он обещает масштабируемость, модульность, независимое развитие фич, удобную навигацию по коду. Но любая архитектура имеет стоимость и ограничения, и FSD не исключение.
В этой статье вы увидите, какие реальные проблемы и недостатки возникают при использовании Feature-Sliced Design, в каких ситуациях он скорее вредит, чем помогает, и какие типичные ошибки совершают команды при его внедрении. Я буду опираться на практические сценарии, разбирать куски кода и структуры папок, чтобы вам было проще сопоставить это со своими проектами.
Переусложнение для маленьких и средних проектов
Когда FSD стреляют из пушки по воробьям
Для небольших приложений FSD часто приводит к архитектурному «перегреву». Структура становится слишком сложной по сравнению с реальной сложностью домена.
Посмотрите на условный ToDo-приложение, организованное по FSD:
src/
app/
providers/
routing/
processes/
auth/
pages/
todos/
ui/
model/
lib/
features/
add-todo/
ui/
model/
toggle-todo/
ui/
model/
entities/
todo/
ui/
model/
shared/
ui/
lib/
api/
Для простого ToDo здесь уже слишком много уровней. Чтобы отследить путь данных, вам нужно прыгать между entities/todo, features/add-todo, features/toggle-todo, pages/todos. Это замедляет работу, хотя логики немного.
Как это выглядит в коде
Допустим, у вас есть небольшая форма добавления задачи. В классическом «упрощенном» подходе она могла бы выглядеть так:
// components/AddTodoForm.tsx
import { useState } from "react";
export function AddTodoForm({ onAdd }) {
const [title, setTitle] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
// Вызываем пропс-коллбек
onAdd(title);
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="Новая задача"
/>
<button type="submit">Добавить</button>
</form>
);
}
Теперь давайте посмотрим, как это может «разъехаться» в FSD при чрезмерном следовании слоям:
// features/add-todo/model/useAddTodo.ts
import { useState } from "react";
import { useTodosStore } from "entities/todo/model/store";
export function useAddTodo() {
const [title, setTitle] = useState("");
const { addTodo } = useTodosStore();
const changeTitle = (value: string) => {
// Обновляем локальный стейт
setTitle(value);
};
const submit = () => {
// Отправляем задачу в стор сущностей
addTodo(title);
setTitle("");
};
return {
title,
changeTitle,
submit,
};
}
// features/add-todo/ui/AddTodoForm.tsx
import { useAddTodo } from "../model/useAddTodo";
export function AddTodoForm() {
const { title, changeTitle, submit } = useAddTodo();
const handleSubmit = (event) => {
event.preventDefault();
submit();
};
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(event) => changeTitle(event.target.value)}
placeholder="Новая задача"
/>
<button type="submit">Добавить</button>
</form>
);
}
Те же 2–3 строки логики теперь разнесены по слоям. Для сложной фичи это плюс, но для простых случаев — лишний шум.
Итоговый недостаток
- Высокий «входной порог» даже для простых задач.
- Увеличенное количество файлов и абстракций.
- Риск того, что команда будет тратить больше времени на поддержку структуры, чем на реально важную бизнес-логику.
Сложность входа для новой команды
Архитектура, которую сначала нужно выучить
Чтобы эффективно работать с FSD, нужно понимать:
- какие есть слои (app, processes, pages, features, entities, shared);
- какие типы зависимостей допустимы между слоями;
- как именно разделять «feature», «entity» и «page»;
- как именовать фичи и сущности;
- как работать с слайсами внутри слоя (model, ui, lib и т.д.).
Новичок, который не знаком с FSD, часто видит примерно следующее:
src/
app/
processes/
pages/
features/
entities/
shared/
и не понимает, куда класть новый код. Это приводит к «архитектурному ступору», когда разработчик тратит время не на реализацию, а на раздумья о размещении файла.
Типичный сценарий: «Куда положить этот хук?»
Смотрите, я покажу вам типичный спор в команде:
- Хук используется в нескольких фичах — тогда, кажется, он должен быть в
shared/libилиshared/model. - Но этот хук завязан на конкретное доменное понятие — значит, вроде бы ему место в
entities. - При этом сейчас он используется только в одной фиче — и его пытаются положить внутрь
features/.
В итоге одно и то же решение может быть реализовано тремя разными способами в рамках одного проекта, если нет четкого гайдлайна. Это создает хаос и снижает предсказуемость структуры.
Почему это считается недостатком
- Требуется дополнительная документация по архитектуре внутри команды.
- Ошибки размещения кода становятся нормой, а не исключением.
- Код-ревью усложняется: ревьюеры тратят время на обсуждение размещения файлов вместо логики.
Риск переинженеринга и абстракций ради абстракций
Когда FSD подталкивает к лишним уровням
FSD часто воспринимают как сигнал «всегда выносить все в отдельную фичу/сущность». В результате даже простая логика «подписаться на рассылку» превращается в полноценную feature со своим model, ui, lib.
Посмотрите на пример чрезмерного усложнения.
features/
newsletter-subscription/
ui/
SubscriptionForm.tsx
model/
useSubscription.ts
types.ts
lib/
validators.ts
Но при этом реальная логика очень простая:
- Один POST-запрос на API.
- Одно поле email.
- Одна кнопка.
Вместо этого можно было бы спокойно обойтись отдельным компонентом и хуком в shared, если бизнес-ценность этой «фичи» невелика.
Как выглядит переинженеринг в коде
Посмотрите, как можно усложнить простой кейс:
// features/newsletter-subscription/model/useSubscription.ts
import { useState } from "react";
import { subscribeToNewsletter } from "shared/api/newsletter";
import { validateEmail } from "../lib/validators";
export function useSubscription() {
const [email, setEmail] = useState("");
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const changeEmail = (value: string) => {
// Проверяем email на фронте
if (!validateEmail(value)) {
setError("Некорректный email");
} else {
setError(null);
}
setEmail(value);
};
const submit = async () => {
if (error || !email) return;
setIsLoading(true);
try {
await subscribeToNewsletter(email);
} finally {
setIsLoading(false);
}
};
return {
email,
error,
isLoading,
changeEmail,
submit,
};
}
В отрыве от общей картины это выглядит нормально. Но если таких «микрофич» много, вы получаете огромный слой архитектурного шума.
Итоговый недостаток
- FSD провоцирует выносить даже мелкие детали в отдельные фичи.
- Увеличивается объем шаблонного кода (boilerplate).
- Сложнее быстро вносить правки в простые сценарии.
Проблемы с навигацией по коду и поиском логики
Логика разрезана по нескольким слоям
Когда вы ищете причину бага, вам нужно проследить путь:
pages— общая страница.features— конкретные пользовательские сценарии.entities— доменные сущности.shared— базовая утилита или API, которые в итоге вызываются.
Для небольших фич это превращается в лишнюю нагрузку на внимание. Вместо «найти компонент и посмотреть» вы часто вынуждены переключаться между 4–5 файлами.
Пример поиска проблемы
Допустим, у вас не обновляется список задач после добавления новой. В монолитном компоненте вы бы посмотрели:
- как обновляется стейт;
- как он рендерится.
В FSD путь может быть таким:
pages/todos/ui/TodosPage.tsx # Страница
features/add-todo/ui/AddTodoForm.tsx # Форма добавления
features/add-todo/model/useAddTodo.ts # Хук добавления
entities/todo/model/store.ts # Стор задач
entities/todo/ui/TodoList.tsx # Список задач
shared/api/todos.ts # API-клиент
Каждый шаг отдельно логичен, но суммарно это удлиняет цикл поиска.
Навигация в IDE становится менее прозрачной
Автодополнение и переход к определению (go to definition) в IDE работают хорошо внутри файла или модуля, но:
- когда логика рассредоточена по множеству слоев и подкаталогов;
- когда названия файлов повторяются (в каждом слое «index.ts», «model.ts», «types.ts»);
то разработчику приходится чаще смотреть на путь файла, а не только на имя.
Итоговый недостаток
- Диагностика багов усложняется.
- Время «первого контакта» с фичей увеличивается.
- Контекст логики размыт по нескольким файлам.
Высокая зависимость от дисциплины команды
FSD без правил быстро превращается в хаос
Особенность FSD в том, что он задает общие уровни, но не заставляет строго следовать им. Если команда не придерживается дисциплины, возникают такие проблемы:
- В
sharedпопадает доменный код. - В
entitiesреализуется логика, которая относится к конкретной фиче. - В
featuresпоявляются огромные «комбайны», нарушающие принцип single responsibility.
Посмотрите на неправильно организованную структуру:
shared/
api/
user.ts # Вроде бы общая сущность, но внутри куча бизнес-логики
lib/
cart.ts # Логика корзины, хотя это уже домен
features/
checkout/
model/
cartStore.ts # Дублирует часть логики из shared/lib/cart.ts
В результате:
- корзина размазана по
sharedиfeatures; - логику сложно переиспользовать правильно;
- команда тратит время на согласование, куда переносить код.
Как можно нарушить зависимости слоя
FSD предполагает, что:
featuresмогут зависеть отentitiesиshared;pagesзависят отfeatures,entities,shared;entitiesне должны зависеть отfeaturesиpagesи т.д.
Но в реальных проектах при спешке легко сделать вот так:
// entities/user/model/useUser.ts
import { trackLogin } from "features/analytics-login";
// Здесь мы тянем фичу в сущность, что нарушает зависимость слоев
export function useUser() {
const login = () => {
// Выполняем авторизацию
// ...
// Тречим login-событие
trackLogin();
};
return { login };
}
Этот пример показывает типичный антипаттерн: сущность знает о конкретной фиче (аналитике логина), а должна быть от нее независимой.
Итоговый недостаток
- FSD не гарантирует правильную архитектуру сам по себе.
- Без правил и код-ревью структура быстро деградирует.
- Требуется постоянный мониторинг архитектурной целостности.
Неполное соответствие сложным доменным моделям
Когда домен не укладывается в «features / entities»
Реальные бизнес-домены часто сложнее, чем «листинг задач» или «страница профиля». Например:
- сложные workflow (многоэтапные процессы);
- завязка на контекст пользователя;
- кросс-доменные операции.
FSD предлагает делить на features и entities, но не всегда очевидно, как корректно смоделировать домен.
Например, домен «Заказ» в e-commerce:
- корзина;
- оплаты;
- доставка;
- скидки;
- личный кабинет.
Если сделать «entity/order», она может разрастись до огромного «бога-объекта». Если же сильно дробить, возникает путаница, где заканчивается одна сущность и начинается другая.
Перекос в пользу UI-слайсов
FSD исторически фокусируется на фронтенд-слоях, в основном вокруг UI. В результате:
- бизнес-правила иногда оказываются «размазаны» между несколькими features;
- инварианты домена (что можно и нельзя по правилам) не всегда удобно выразить в рамках типичной FSD-структуры.
Посмотрите на пример:
entities/
order/
model/
useOrder.ts
validators.ts
ui/
OrderSummary.tsx
features/
apply-coupon/
model/
useApplyCoupon.ts
change-delivery/
model/
useChangeDelivery.ts
Где должны жить правила:
- можно ли применить купон к этому типу заказа;
- какие статусы доставки допустимы;
- какие ограничения по комбинациям купонов и методов доставки?
Если часть логики спрятана в features, а часть — в entities/order/model, со временем становится сложно понимать, где истина.
Итоговый недостаток
- Сложные бизнес-домены плохо ложатся в простую схему layers + slices.
- Риск размазывания бизнес-логики по нескольким слоям.
- Трудности с формализацией инвариантов и правил домена.
Усложнение тестирования и мокирования
Разделение по слоям усложняет интеграционные тесты
Компоненты и хуки в FSD часто сильно декомпозированы. Это полезно для unit-тестов, но интеграционные сценарии становятся более многослойными.
Например, чтобы протестировать поведение страницы:
- нужно замокать стор сущностей;
- правильно замокать фичи (если они ходят в API);
- учесть провайдеры из слоя app (роутинг, контексты).
Вот типичный тест для страницы в FSD-подходе (упрощенно):
// pages/todos/ui/TodosPage.test.tsx
import { render, screen } from "@testing-library/react";
import { TodosPage } from "./TodosPage";
import { TodosProvider } from "entities/todo/model/TodosProvider";
import { QueryClientProvider } from "@tanstack/react-query";
import { createTestQueryClient } from "shared/lib/testUtils";
test("отображает список задач", async () => {
const queryClient = createTestQueryClient({
// Здесь мы настраиваем мок-ответы API
});
render(
<QueryClientProvider client={queryClient}>
<TodosProvider>
<TodosPage />
</TodosProvider>
</QueryClientProvider>
);
// Проверяем что элементы списка отобразились
expect(await screen.findByText("Моя первая задача")).toBeInTheDocument();
});
Сама по себе структура теста не страшная, но количество провайдеров и зависимостей возрастает с ростом проекта. FSD способствует тому, что:
- в слое
appпоявляются дополнительные провайдеры; - в
processesесть собственные обвязки; - в
entitiesживут свои провайдеры/контексты.
Мокирование API и стора
Часто встречается ситуация, когда:
- стор или API-клиент лежит глубоко в
entitiesилиshared/api; - фича работает поверх этого, но тестировать вы хотите именно фичу.
Тогда вам приходится:
- либо мокировать исходный модуль (например, через jest.mock);
- либо создавать тестовые реализации стора;
- либо заводить отдельные тест-обертки.
Если структура модулей не продумана, мокирование становится хрупким и завязанным на структуру папок.
Итоговый недостаток
- Интеграционные тесты становятся многослойными.
- Требуется аккуратное проектирование модулей для удобного мокирования.
- Любые изменения архитектуры могут ломать тесты, если они слишком завязаны на структуру.
Сложности миграции существующих проектов на FSD
Полный рефакторинг редко возможен
Многие пытаются внедрять Feature-Sliced Design в уже существующий проект, который:
- организован по другой структуре (по типам файлов, по модулям, по роутам);
- содержит «god-components» и большие модули;
- слабо типизирован.
Внезапный «переезд» на FSD приводит к:
- большим рефакторингам, в которых легко сделать регрессии;
- временной деградации архитектуры (часть по старому, часть по новому);
- сложности с навигацией, потому что структура смешанная.
Вот как может выглядеть переходный период:
src/
components/ # Старый подход
Header/
Footer/
TodoList/
store/ # Старый глобальный стор
app/ # Новый FSD-слой
pages/ # Новый FSD-слой
features/ # Новые фичи
entities/ # Новые сущности
shared/ # Переиспользуемые модули
Пока вы не завершите миграцию, разработчикам нужно держать в голове два подхода одновременно.
Постепенный переход требует четкого плана
Чтобы миграция прошла безболезненнее, нужен план:
- какие части кода остаются «как есть»;
- какие новые фичи сразу делать в FSD;
- какие старые модули постепенно переносить и по каким критериям.
Без этого планирования FSD воспринимается как «еще один слой хаоса», а не как решение.
Итоговый недостаток
- Внедрение FSD в существующий проект требует серьезных усилий.
- Некоторое время структура будет смешанной, что усложняет навигацию.
- Нужен миграционный план, иначе затраты превышают пользу.
Зависимость от инструментов и конвенций
Необходимость дополнительных инструментов
Чтобы работать с FSD комфортно, команды часто добавляют:
- линтеры, проверяющие импорт между слоями;
- генераторы модулей/фич (cli-скрипты);
- шаблоны (blueprints) для новых сущностей и фич.
Смотрите, пример правила ESLint, запрещающего неправильные импорты:
// .eslintrc.js - условный пример
module.exports = {
rules: {
"fsd/layers-imports": [
"error",
{
alias: "@", // псевдоним для src
ignoreImportPatterns: ["**/test-utils/**"],
},
],
},
};
Без таких инструментов:
- легко нарушить зависимости между слоями;
- трудно поддерживать единообразную структуру импорта;
- ревьюеры вынуждены вручную отслеживать архитектурные ошибки.
Жесткая привязка к структуре
Если в проекте сильно «зашита» FSD-структура:
- пути импорта используются повсюду;
- есть множество относительных импортов между слайсами;
- код-сплиттинг и бандлинг настроены под слои FSD;
то любое желание «перекроить» архитектуру потребует значительных изменений.
Итоговый недостаток
- FSD комфортен, когда есть поддерживающие инструменты, а это дополнительные затраты.
- Проект оказывается привязанным к определенной структуре.
- Любой переход на другой подход или пересмотр слоев становится дорогим.
Отсутствие «официального» стандарта и разночтения
Разные интерпретации FSD в разных командах
Хотя у FSD есть документация и подход, нет жесткого стандарта «один в один». В результате:
- команды по-разному трактуют границы слоев;
- одни заводят
widgets, другие — нет; - кто-то объединяет
processesиpagesпо смыслу, кто-то разделяет строго.
Посмотрите на две возможные структуры:
Вариант 1:
src/
app/
processes/
pages/
features/
entities/
shared/
Вариант 2:
src/
app/
pages/
widgets/
features/
entities/
shared/
Или даже упрощенный:
src/
app/
pages/
features/
entities/
shared/
Разные команды под «Feature-Sliced Design» понимают разные конкретные реализации. Это создает дополнительные трудности, если вы:
- переходите между компаниями;
- подключаете новых разработчиков, знакомых с «другим» вариантом FSD;
- читаете статьи, где используется немного иная трактовка.
Итоговый недостаток
- Нет единого жесткого стандарта FSD.
- Опыт работы с FSD не всегда переносится напрямую в другой проект.
- Появляется «архитектурный диалект» внутри каждой команды.
Заключение
Feature-Sliced Design дает мощный набор инструментов для организации фронтенд-архитектуры, но за это приходится платить. Основные недостатки, с которыми вы столкнетесь на практике:
- переусложнение структуры для небольших и средних проектов;
- повышенный порог входа и необходимость обучать команду не только фреймворку, но и архитектурным конвенциям;
- риск переинженеринга, когда даже простая логика обрастает слоями и файлами;
- усложнение навигации по коду, поиска логики и диагностики багов;
- сильная зависимость от дисциплины и инструментов, без которых FSD легко превращается в хаос;
- ограниченная удобность при работе с действительно сложными доменными моделями;
- непростая миграция из существующей структуры кода на FSD;
- отсутствие единого общепринятого стандарта, что приводит к разночтениям.
Если вы рассматриваете FSD для своего проекта, важно трезво оценить:
- масштаб и сложность домена;
- размер и опыт команды;
- готовность инвестировать в обучение, документацию и инструменты;
- необходимость долгосрочной масштабируемости именно на уровне фронтенда.
FSD хорошо работает там, где архитектурные затраты окупаются масштабом продукта и команды. Но если пытаться использовать его «по чек-листу» везде, можно легко столкнуться с перечисленными drawbacks и потратить значительные усилия на поддержку структуры, которая не приносит достаточной пользы.
Частозадаваемые технические вопросы по теме и ответы
Как ограничить неправильные импорты между слоями в FSD
Используйте ESLint-правила. Практический шаг:
- Установите
eslint-plugin-boundariesили специализированный плагин для FSD. - Опишите слои как модули.
- Настройте правило, запрещающее, например, импорт
featuresизentities.
Мини-инструкция:
- создайте конфиг модульных границ;
- добавьте правило в
.eslintrc; - включите проверку в CI.
Как организовать алиасы для слоев чтобы не писать длинные относительные пути
Шаги:
- В
tsconfig.jsonдобавьтеpaths, например:"@features/*": ["src/features/*"]. - В bundler-конфиге (Webpack, Vite) настройте те же алиасы.
- Проверьте, что IDE понимает алиасы (обычно это автоматом).
Теперь вы можете импортировать так:
import { AddTodoForm } from "@features/add-todo";
Как постепенно вытаскивать логику из «god-component» в FSD
Рекомендуемая последовательность:
- Выделите доменную сущность и создайте
entities/<entity>/modelиentities/<entity>/ui. - Вынесите работу со стором и типы в
model. - Перенесите UI-часть в компоненты
ui. - Затем постепенно выносите отдельные пользовательские сценарии в
features.
На каждом шаге проверяйте, что импорты соответствуют слоям.
Как настроить code splitting с учетом FSD слоев
Подход:
- Разделите бандлы по роутам (обычно слой
pages). - Внутри страниц используйте динамический импорт для тяжелых
featuresиwidgets. - Старайтесь, чтобы
sharedиentitiesпопадали в общие чанки, а не дублировались.
Пошагово:
- в роутере используйте
lazy/Suspense; - вынесите большие фичи в отдельные динамические импорты;
- анализируйте бандл-репорты, чтобы убедиться, что разбиение эффективное.
Как документировать правила размещения кода в FSD проекте
Мини-инструкция:
- Создайте файл
ARCHITECTURE.mdв корне репозитория. - Описывайте:
- зачем выбран FSD;
- какие слои используются именно у вас;
- примеры размещения кода для типичных случаев.
- Добавьте в документ реальные примеры структуры папок и кода.
- Сделайте ссылку на документ обязательной частью онбординга для новых разработчиков.