Олег Марков
Недостатки FSD - drawbacks
Введение
Feature-Sliced Design (FSD) за последние годы стал одним из самых популярных подходов к архитектуре фронтенд-проектов, особенно в экосистеме React. Его часто продают как универсальное решение, которое «закончит хаос в проекте». Но на практике вместе с сильными сторонами FSD приносит и ощутимые недостатки.
В этой статье вы разберете, какие реальные проблемы возникают при использовании FSD:
- где он избыточен и усложняет кодовую базу;
- почему он может тормозить разработку, особенно на старте;
- какие риски несет неправильное внедрение;
- какие типичные антипаттерны появляются именно в FSD-проектах;
- как минимизировать эти drawbacks, если вы все же выбираете FSD.
Смотрите, я покажу вам на конкретных примерах, чем оборачивается слепое следование методологии и в каких случаях стоит задуматься о более простой архитектуре.
Что такое FSD в двух словах и почему это важно для понимания недостатков
Чтобы внятно говорить о недостатках, нужно коротко зафиксировать, что именно подразумевается под FSD.
Базовые принципы FSD
FSD предлагает:
- делить приложение по уровням (layers) – app, processes, pages, widgets, features, entities, shared;
- внутри уровней делить код по слайсам (slices) – как правило, по функциональности или доменным сущностям;
- ограничивать зависимости между уровнями и слайсами;
- максимизировать независимость фич между собой.
Типичная структура может выглядеть так:
src/
app/
index.tsx
providers/
routing/
processes/
checkout/
pages/
home/
profile/
widgets/
header/
sidebar/
features/
auth/
add-comment/
like-post/
entities/
user/
post/
shared/
ui/
lib/
config/
В теории это делает проект масштабируемым и предсказуемым. Но на практике именно эти принципы становятся источником проблем.
Недостаток 1. Высокий порог входа и сложность обучения
Почему FSD тяжело объяснить новичкам
Когда разработчик впервые открывает FSD-проект, он видит множество уровней, папок и правил. Для человека, привыкшего к более простому подходу (например, разделению на components, services, hooks), все это выглядит переусложненным.
Типичная реакция:
- сложно понять, в какой слой положить новый код;
- непонятно, как именно определять границы feature, entity, widget;
- код кажется «размазанный» по проекту.
Давайте разберемся на примерной задаче: вам нужно добавить форму логина.
В неструктурированном проекте вы могли бы сделать так:
src/
components/
LoginForm/
api/
auth.ts
В FSD вам уже нужно думать:
- это feature
authилиlogin? - где расположить форму – в
features/auth/uiили сделатьentities/user/ui? - апи-запрос пойдет в
features/auth/modelили вshared/api?
Смотрите, как это может выглядеть в одном из вариантов:
src/
features/
auth/
ui/
LoginForm/
model/
useLogin.ts
types.ts
shared/
api/
authApi.ts
На словах это звучит логично, но новичку нужно:
- знать договоренности команды;
- понимать, как в этом проекте интерпретируют слои;
- помнить правила зависимостей.
Эффект: замедление работы команды
Пока разработчики не «пропитались» FSD:
- код пишется медленнее из-за постоянных архитектурных вопросов;
- часто возникает страх «положить неправильно» и получить ревью с переносами;
- ментальное усилие тратится на структуру больше, чем на саму бизнес-логику.
Это особенно заметно на маленьких задачах, где архитектурные размышления явно доминируют над полезной работой.
Недостаток 2. Избыточность для небольших и средних проектов
Слишком тяжелая архитектура для простых приложений
Если у вас:
- небольшое SPA с несколькими страницами;
- дашборд-админка;
- MVP с парой ключевых сценариев,
FSD часто оказывается «стрельбой из пушки по воробьям».
Представьте простое TODO-приложение с одной страницей:
- список задач;
- фильтры;
- форма добавления.
С максимально простым подходом:
src/
App.tsx
components/
TodoList/
TodoItem/
AddTodoForm/
api/
todos.ts
hooks/
useTodos.ts
Теперь посмотрим на возможную FSD-структуру того же приложения:
src/
app/
index.tsx
pages/
todos/
ui/
TodosPage/
widgets/
todos-list/
ui/
TodosList/
features/
add-todo/
ui/
AddTodoForm/
model/
useAddTodo.ts
filter-todos/
ui/
TodosFilter/
model/
useTodosFilter.ts
entities/
todo/
ui/
TodoItem/
model/
types.ts
todoStore.ts
shared/
api/
todosApi.ts
ui/
Button/
Input/
По сути, объем функциональности тот же, но:
- количество слоев и папок сильно выросло;
- навигация по проекту стала тяжелее;
- onboarding нового разработчика становится медленнее, чем польза от архитектуры.
Когда FSD начинает «окупаться»
FSD начинает иметь смысл, когда:
- десятки и сотни экранов;
- много независимых фич, которые живут своей жизнью;
- разные команды трогают разные зоны приложения;
- часто идет параллельная разработка.
Если ничего из этого нет, то FSD превращается в архитектурный оверхед и источник лишних решений «на будущее», которое, возможно, никогда не наступит.
Недостаток 3. Сложность рефакторинга на уже существующих проектах
Проблема миграции: «или все, или ничего»
FSD хорошо смотрится на зеленом поле, когда вы начинаете новый проект. Но чаще вы сталкиваетесь с существующим кодом и желанием «перевести его на FSD».
Здесь проявляются следующие проблемы:
- сложно сделать постепенную миграцию без хаоса;
- часть кода уже построена вокруг других принципов (например, domain-driven design, классический MVC или просто «как получилось»);
- надо одновременно поддерживать старую и новую архитектуру.
Типичная картина в ходе миграции:
src/
components/
OldLoginForm/
services/
authService.ts
# новый FSD-код
features/
auth/
ui/
LoginForm/
model/
useLogin.ts
shared/
api/
authApi.ts
В итоге у вас:
- дублируются компоненты и логика;
- растут временные костыли по интеграции старого и нового мира;
- появляются циклические зависимости или хаотичные импорты.
Риски при частичном внедрении
Вы можете попытаться внедрять FSD только в новых фичах. Но тогда:
- у команды два подхода одновременно;
- ревью превращается в споры «это уже FSD-зона или еще старый стиль»;
- сложно провести четкие границы между «новым» и «старым» кодом.
Покажу вам, как это выглядит на уровне импортов:
// Новый FSD-виджет
import { LoginForm } from "@/features/auth/ui/LoginForm";
// Но он же тянет старый сервис
import { authService } from "@/services/authService"; // старый слой
// services/authService.ts
// ...
// Внутри вдруг используется новый shared/api
import { authApi } from "@/shared/api/authApi";
Такие смешения стилей:
- разрушают идею предсказуемых зависимостей;
- усложняют навигацию по коду;
- делают правила FSD «частично правдивыми».
Недостаток 4. Чрезмерная фрагментация кода и навигационная нагрузка
«Код размазан по слоям»
Один из ключевых drawbacks – логика одной бизнес-задачи оказывается разбросанной по множеству папок и файлов.
Допустим, вы хотите понять, как работает процесс оформления заказа. В классическом подходе это часто можно понять по нескольким файлам в одном модуле.
В FSD процесс может быть разбит так:
pages/checkout/ui/CheckoutPage– страница;widgets/cart-summary/ui/CartSummary– краткая сводка корзины;features/apply-coupon/ui/ApplyCouponForm– форма купона;features/apply-coupon/model/useApplyCoupon– логика купона;features/submit-order/model/useSubmitOrder– отправка заказа;entities/cart/model/cartStore– состояние корзины;shared/api/orderApi– вызовы на сервер.
Чтобы проследить полностью сценарий, вам нужно выполнить множество «прыжков» по файлам.
Теперь вы увидите, как это выглядит в коде:
// pages/checkout/ui/CheckoutPage.tsx
import { CartSummary } from "@/widgets/cart-summary";
import { ApplyCouponForm } from "@/features/apply-coupon";
import { SubmitOrderButton } from "@/features/submit-order";
export const CheckoutPage = () => {
return (
<div>
{/* Виджет со сводкой корзины */}
<CartSummary />
{/* Фича применения купона */}
<ApplyCouponForm />
{/* Фича оформления заказа */}
<SubmitOrderButton />
</div>
);
};
Если вы хотите:
- изменить поведение заказа;
- найти, почему купон не применяется;
- добавить дополнительную валидацию,
вам приходится ходить по нескольким уровням. Это утомляет даже опытных разработчиков, а для новых людей в команде становится особенно тяжело.
Микро-файлы и искусственное дробление
Команды часто начинают создавать:
- по файлу на каждый хук;
- по директории на каждый небольшой компонент;
- «пустые» слои с минимальным содержимым, чтобы «следовать методологии».
Получается много файлов с содержимым по 10–20 строк, которые нужно постоянно открывать, переключаться между ними, искать связи.
Эта избыточная фрагментация:
- снижает обзорность;
- мешает быстро понять, что происходит в фиче;
- замедляет ревью – приходится собирать в голове мозаику из мелких деталей.
Недостаток 5. Теория зависимостей против реальной интеграции
Нарушение слоев под давлением реальных задач
На бумаге у FSD есть строгие правила допустимых зависимостей, например:
sharedможно импортировать откуда угодно;entitiesдоступны изfeatures,widgets,pages;featuresдоступны изwidgets,pages;widgetsдоступны изpages;- и так далее.
Но в реальных проектах:
- бизнес-требования меняются;
- фичи начинают зависеть друг от друга;
- нужны кросс-слайсовые сценарии, которые не укладываются в изначальные границы.
Типичный пример нарушения:
// features/order-tracking/model/useOrderTracking.ts
import { Order } from "@/entities/order";
import { User } from "@/entities/user";
// И вдруг тянем конкретный виджет уведомлений
import { NotificationWidget } from "@/widgets/notification"; // нарушение
Причина: реальной задаче важнее быстро показать уведомление, чем сохранить идеальную чистоту зависимостей.
Возникновение «серых зон»
Команды часто создают:
- «полуслои», которые непонятно, к чему относятся;
- общие модули, которые формально shared, но по сути знают слишком много о домене.
Например:
src/
shared/
model/
appStore.ts # глобальное состояние, знает о куче сущностей
В итоге:
- правила зависимостей становятся размытыми;
- часть кода живет в отдельных «особых» местах, не описанных в методологии;
- новые разработчики не понимают, по каким же правилам реально все устроено.
Недостаток 6. Усложнение тестирования из-за слоистости
Тесты становятся заложниками архитектурных границ
FSD поощряет разделение:
- UI-слой;
- модель (hooks, stores);
- api-слой;
- конфигурация.
С одной стороны, это удобно для юнит-тестов отдельных частей. С другой стороны, интеграционные тесты сценариев часто усложняются.
Смотрите, я покажу вам типичный сценарий теста фичи оформления заказа:
- нужно смонтировать страницу;
- смокать контекст роутера из
app; - подменить api из
shared/api; - замокать сторы из
entities.
Пример упрощенного теста:
// features/submit-order/model/useSubmitOrder.test.ts
import { renderHook, act } from "@testing-library/react";
import { useSubmitOrder } from "./useSubmitOrder";
import { orderApi } from "@/shared/api/orderApi";
jest.mock("@/shared/api/orderApi");
// Здесь мы тестируем только хук, без UI
describe("useSubmitOrder", () => {
it("отправляет заказ и возвращает результат", async () => {
// Мокаем ответ API
(orderApi.submitOrder as jest.Mock).mockResolvedValue({ id: 123 });
const { result } = renderHook(() => useSubmitOrder());
await act(async () => {
await result.current.submit();
});
expect(orderApi.submitOrder).toHaveBeenCalled();
expect(result.current.orderId).toBe(123);
});
});
Юнит-тест для хука написать несложно. Но если вы хотите проверить весь сценарий на уровне страницы:
- нужно собрать в кучу множество слоев;
- правильно замокать окружение.
В результате:
- простой по сути сценарий требует длинной настройки теста;
- малейшее изменение архитектуры может ломать интеграционные тесты.
Недостаток 7. Сильная зависимость от дисциплины команды
Документация и договоренности обязательны
FSD по сути – это набор идей. Конкретные решения:
- где хранить типы;
- как называть фичи;
- как организовывать api;
- как разделять ответственность между entities и features,
определяет сама команда.
Если:
- нет архитектурной документации;
- нет примеров «как правильно здесь делать»;
- нет ответственного за архитектуру,
то FSD быстро превращается в набор папок без четких правил.
Разработчики начинают:
- по-своему трактовать уровни;
- тащить любой код туда, где «больше подходит по ощущениям»;
- нарушать зависимостные правила, чтобы просто решить задачу.
Расхождение «идеального FSD» и «фактического FSD»
На митингах команда может говорить:
«У нас FSD, у нас все по слоям».
Но при открытии проекта вы увидите:
- слои перепутаны;
- shared содержит кучу доменной логики;
- features напрямую дергают друг друга;
- entities знают о роутере и глобальном состоянии.
В итоге:
- FSD перестает быть инструментом упорядочивания;
- он становится просто красивым словом, прикрывающим архитектурный хаос.
Недостаток 8. Трудности при выборе границ: features, entities, widgets
Неочевидность разбиения приложения на слои
Один из самых сложных моментов – определить:
- что считать entity;
- что считать feature;
- что превращать в widget.
Например, у вас есть «профиль пользователя». Что это:
entity/userс набором UI и моделью;feature/edit-profileдля редактирования иfeature/view-profileдля просмотра;- или
widget/user-profileс вложенными features?
Давайте посмотрим на разные подходы.
Вариант 1. Все вокруг entity user
entities/
user/
ui/
UserProfileCard/
UserAvatar/
model/
userStore.ts
features/
edit-user/
ui/
EditUserForm/
model/
useEditUser.ts
Вариант 2. Сильный упор на features
entities/
user/
model/
types.ts
features/
view-profile/
ui/
UserProfilePageContent/
model/
useUserProfile.ts
edit-profile/
ui/
EditProfileForm/
model/
useEditProfile.ts
widgets/
profile/
ui/
ProfileWidget/
Каждый вариант:
- по-своему логичен;
- вносит разные ограничения в зависимости;
- меняет то, как вы будете переиспользовать код.
Проблема в том, что:
- методология не дает однозначного ответа;
- команда должна договариваться, а это лишнее время и возможные споры;
- миграция от одной схемы к другой стоит дорого.
Эволюция фич ломает исходное разбиение
Еще одна тонкость:
- фича растет;
- появляются новые сценарии;
- entity становится «больше», чем вы ожидали.
Например, feature/auth может со временем вобрать:
- логин;
- регистрацию;
- восстановление пароля;
- двухфакторную авторизацию;
- смену email.
В какой-то момент:
- эту фичу надо дробить;
- переносить части в другие слайсы;
- заново продумывать зависимости.
Каждый такой рефакторинг:
- затрагивает кучу импортов;
- ломает привычное расположение кода;
- требует аккуратной миграции.
Недостаток 9. Потенциальные проблемы с производительностью сборки
Много модулей – больше работы для сборщика
FSD поощряет:
- небольшие файлы;
- модульность;
- разделение логики по слоям.
Все это приводит к тому, что:
- количество импортов растет;
- растет количество модулей, которые должен обработать bundler (Webpack, Vite, esbuild и т.п.).
На малых проектах это почти незаметно, но на реально крупных монолитах:
- время холодной сборки растет;
- время инкрементальной сборки при изменения в глубинных слоях может увеличиваться;
- иногда ломается tree-shaking, если используется сложная структура barrel-файлов (index.ts).
Например:
// shared/ui/index.ts
export * from "./Button";
export * from "./Input";
export * from "./Modal";
// ... и десятки других
// features/auth/ui/index.ts
export * from "./LoginForm";
export * from "./RegisterForm";
export * from "./ResetPasswordForm";
Такое массовое «реэкспортирование» может приводить к тому, что:
- сборщик тянет больше кода, чем реально нужно;
- становится сложнее анализировать, что попало в бандл.
Проблемы с lazy-loading и code splitting
FSD поощряет деление на независимые фичи, но:
- если не думать о границах чанков;
- и не планировать lazy-loading,
то структура по слоям не гарантирует хорошего разделения кода на чанки.
Вместо четкого деления по страницам вы можете получить:
- пересекающиеся чанки;
- дублирование кода в чанках из-за обоюдных зависимостей фич.
Тут важно понимать: это не прямой минус FSD как идеи, но типичный практический drawback, с которым сталкиваются команды без сильной экспертизы по сборке.
Недостаток 10. Неполное соответствие другим архитектурным подходам
Конфликт с DDD и «чистая архитектура»
Если в компании уже приняты:
- domain-driven design (DDD);
- принципы «чистой архитектуры» (clean architecture);
- своя доменная разбивка бекенда,
то FSD может:
- частично дублировать эти подходы;
- создавать второй слой терминологии;
- путать команды.
Например, в DDD основой являются домены и bounded contexts. В FSD – уровни и slices.
Тогда:
- домен
Billingна бекенде может распасться на несколько фич и сущностей в FSD; - границы, понятные для бэкендеров и аналитиков, становятся не такими очевидными на фронте;
- обсуждения задач дробятся на «фронтовую» и «бекендовую» картину мира.
Двойная архитектурная модель
Часто получается так:
- backend живет в DDD-терминах;
- frontend – в FSD-терминах;
- документация продукта – в третьем наборе терминов.
Разработчикам приходится:
- маппить одно на другое в голове;
- объяснять, как некоторое «feature/payment-checkout» связано с доменом
Billingи конкретными bounded contexts.
Это усложняет:
- коммуникацию между командами;
- написание документации;
- онбординг новых разработчиков, которые уже знают DDD, но не знакомы с FSD.
Когда FSD стоит использовать, несмотря на недостатки
Чтобы не складывалось впечатление, что FSD – это только недостатки, важно понимать, когда минусы окупаются:
- большое долгоживущее фронтенд-приложение;
- продуктовая разработка с постоянным ростом функциональности;
- несколько команд или подкоманд, параллельно работающих над разными зонами;
- строгие требования к возможности выделения фич в отдельные модули в будущем.
В таких условиях:
- архитектурный оверхед становится приемлемым;
- навигационная сложность компенсируется предсказуемостью;
- дисциплина и документация обычно сильнее, чем в маленьких командах.
Как минимизировать недостатки FSD на практике
Теперь давайте перейдем к практической части и посмотрим, как смягчить основные drawbacks при внедрении.
1. Вводите FSD поэтапно и осознанно
Не стоит:
- сразу переписывать весь проект под FSD;
- вводить все уровни, даже если часть из них сейчас пустая;
- тратить недели на «идеальную структуру» до начала разработки.
Более мягкий путь:
- Начните с 3–4 ключевых уровней, которые реально нужны:
app,pages,features,shared.
- Добавьте
entities, когда появится повторяющаяся доменная сущность. - Добавьте
widgets, когда почувствуете реальную боль от отсутствия промежуточного слоя между страницами и фичами.
2. Ограничьте глубину вложенности и число файлов
Хорошее практическое правило:
- не дробить маленькие фичи на слишком много модулей;
- держать структуру плоской, пока размер фичи не требует усложнения.
Например, для небольшой фичи авторизации:
features/
auth/
ui/
LoginForm.tsx
model/
useAuth.ts
types.ts
А не:
features/
auth/
ui/
LoginForm/
index.ts
LoginForm.tsx
types.ts
model/
hooks/
useAuth.ts
store/
authStore.ts
api/
authApi.ts
Если объем логики вырастет – тогда уже можно дробить дальше.
3. Формализуйте правила для вашей команды
Сформулируйте небольшую, но понятную архитектурную спецификацию:
- когда создаем новый слой;
- как определяем feature vs entity;
- где живут api-клиенты;
- как именуем слайсы.
Смотрите, я приведу пример минимального документа:
Пример простых правил
- Любая бизнес-функция, которая может появиться на нескольких экранах, – это feature.
- Любая доменная сущность с данными и базовыми операциями – это entity.
- Любой большой самостоятельный блок на странице (layout, header, sidebar) – это widget.
- В
sharedне кладем код, который знает о конкретных сущностях или бизнес-правилах.
Чем конкретнее ваш документ – тем меньше споров и путаницы.
4. Автоматизируйте часть правил
Используйте:
- линтеры зависимостей (eslint plugins, custom rules);
- шаблоны (plop, hygen) для автогенерации структуры feature/entity.
Пример простого plop-шаблона для feature:
// plopfile.js
module.exports = function (plop) {
plop.setGenerator("feature", {
description: "Создать новую feature по FSD",
prompts: [
{
type: "input",
name: "name",
message: "Название feature"
}
],
actions: [
{
type: "add",
path: "src/features/{{kebabCase name}}/ui/{{properCase name}}.tsx",
templateFile: "plop-templates/feature-ui.hbs"
},
{
type: "add",
path: "src/features/{{kebabCase name}}/model/use{{properCase name}}.ts",
templateFile: "plop-templates/feature-hook.hbs"
}
]
});
};
// Здесь мы создаем генератор, который избавляет от ручного создания файлов и директорий
// Это снижает порог входа и делает структуру более предсказуемой
Так вы:
- уменьшаете количество архитектурных ошибок;
- снижаете нагрузку на ревью кода;
- делаете вход в FSD-проект проще для новичков.
Заключение
FSD – это мощная архитектурная методология, но она не бесплатна. За ее преимущества вы платите:
- сложностью обучения и онбординга;
- избыточной структурой на малых и средних проектах;
- усложнением рефакторинга и тестирования;
- зависимостью от дисциплины команды и качества документации;
- навигационной и когнитивной нагрузкой из-за слоистости и фрагментации кода.
Ключевая мысль: FSD – это инструмент, а не самоцель. Прежде чем вводить его, полезно честно ответить:
- насколько ваш проект действительно крупный и долгоживущий;
- насколько дисциплинированна команда;
- готовы ли вы инвестировать время в документацию и внедрение правил;
- есть ли альтернатива попроще, которая решит ваши задачи без лишнего усложнения.
Используйте FSD там, где его сильные стороны действительно критичны, и не бойтесь ограничивать или адаптировать методологию под реальные потребности проекта.
Частозадаваемые технические вопросы по теме и ответы
Как правильно организовать импорты, чтобы не нарушать уровни FSD
- Введите алиасы в сборщике (Vite, Webpack) для каждого слоя, например:
@/app,@/pages,@/features, и используйте их везде. - Опишите в документации, какие слои могут импортировать какие.
- Добавьте eslint-правила (например,
no-restricted-imports) для запрета нежелательных импортов. - Периодически запускайте скрипт-валидатор зависимостей, который строит граф импортов и ищет нарушения.
Как поступать с кросс-фичевыми хуками и утилитами
- Если хук не зависит от сущностей и бизнес-логики – кладите его в
shared/lib. - Если хук использует доменные сущности, но нужен в нескольких фичах – подумайте, не является ли он частью
entities/<entity>/model. - Если хук связывает несколько фич – часто это сигнал, что вам нужен отдельный widget или process, а не общий хук.
Как интегрировать глобальное состояние (Redux, Zustand, Recoil) в FSD
- Не делайте один глобальный стор в
appс доменной логикой. - Храните доменное состояние в
entities/<entity>/model. - В
featuresиспользуйте селекторы/хуки изentities, а не лезьте напрямую к стору. - В
appостаются только провайдеры стора и root-конфигурация.
Как поступать с формами, которые затрагивают несколько сущностей
- Если форма управляет несколькими сущностями – оформляйте ее как feature (например,
feature/checkout-form). - Внутри feature:
- используйте модели из
entitiesдля получения и изменения данных; - не дублируйте логику сущностей;
- выносите сложные операции в отдельные сервисы (внутри feature или в доменных сущностях).
- используйте модели из
- Если форма становится слишком крупной – разбейте ее на несколько features и объедините в widget.
Что делать, если методология FSD конфликтует с уже существующей архитектурой
- Зафиксируйте текущую архитектурную модель проекта в коротком документе.
- Определите точку «стыка», где FSD начинается, а старый подход заканчивается (например, только новые pages и features).
- Не пытайтесь сразу мигрировать все – вводите FSD в новых зонах и описывайте правила взаимодействия между старым и новым кодом.
- Проводите периодические ревью архитектуры и корректируйте границы, если обнаруживаются системные проблемы.