Олег Марков
Преимущества FSD - как архитектура делает фронтенд управляемым и масштабируемым
Введение
Feature-Sliced Design (FSD) — это подход к архитектуре фронтенда, в котором вы организуете проект вокруг функциональных возможностей продукта, а не вокруг технических слоев. Вместо привычной структуры по типам (components, services, hooks) вы получаете слои вроде app, processes, pages, features, entities, shared, а внутри — срезы под конкретные доменные сущности и фичи.
Здесь мы разберем, какие реальные преимущества дает FSD:
- почему код становится понятнее и предсказуемее;
- как FSD влияет на скорость разработки и онбординг новых разработчиков;
- какие организационные плюсы появляются при росте команды;
- как FSD снижает уровень «архитектурного хаоса» в долгоживущих проектах;
- чем FSD отличается от классической «атомарной» или слойной архитектуры.
При этом вы увидите примеры структуры проекта, практические сценарии и типичные плюсы, которые проявляются уже на средних по размеру кодовых базах.
Что такое FSD и зачем он нужен
Краткое напоминание концепции
В основе FSD лежат два ключевых принципа:
Организация по функционалу (feature-first)
Код группируется по фичам и сущностям домена. Любой разработчик может быстро найти все, что относится к конкретной возможности продукта.Явные уровни абстракции (слои)
Слои определяют «насколько высоко» над бизнес-логикой находится модуль и кто имеет право от кого зависеть.
Базовые слои (в классическом варианте FSD):
app— инициализация приложения, роутинг, провайдеры;processes— сквозные бизнес-процессы;pages— композиция фич и сущностей под конкретную страницу;features— пользовательские фичи (логин, добавление в корзину, фильтрация и т.п.);entities— доменные сущности (User, Product, Order);shared— переиспользуемые утилиты, UI-компоненты, либы.
Преимущества FSD для структуры и читаемости кода
Предсказуемая навигация по проекту
Главное, что вы начинаете ощущать — это предсказуемость. Когда вам говорят «исправь баг в фильтре товаров», вы почти автоматически понимаете, что искать:
- слой:
features(функциональность); - срез:
product-filterили похожий; - внутри — папки
ui,model,libи т.п.
Смотрите, как это выглядит в типичном проекте:
src/
app/
providers/
routing/
pages/
product-list/
ui/
model/
features/
product-filter/
ui/
model/
lib/
add-to-cart/
ui/
model/
entities/
product/
ui/
model/
cart/
ui/
model/
shared/
ui/
lib/
api/
Как видите, здесь идея в том, что вы почти бездумно идете по дереву:
- «страница списка товаров» →
pages/product-list; - «фича фильтрации» →
features/product-filter; - «сущность товара» →
entities/product.
В результате:
- меньше времени уходит на поиск кода;
- меньше «магии» и неочевидных зависимостей;
- легче понять границы ответственности каждого модуля.
Минимизация «размазанных» изменений
В архитектуре «по слоям» (например, components, hooks, services) одна фича часто оказывается разбросана по всему проекту:
- часть логики в одном сервисе;
- компоненты — в другой папке;
- хелперы — в третьей.
В FSD все, что относится к фиче, лежит рядом. Когда вы дорабатываете фичу, вы в основном работаете в одной директории, а не прыгаете по всему проекту.
Посмотрите на пример:
features/
add-to-cart/
ui/
AddToCartButton.tsx
model/
hooks/
useAddToCart.ts
services/
addToCartApi.ts
types.ts
- Вся логика добавления в корзину — в одном месте.
- Любой новый разработчик быстро видит, что относится к конкретной пользовательской возможности.
Преимущества FSD для масштабируемости и модульности
Локализация изменений и изоляция фич
Когда у вас есть четкие слои и срезы, вы можете намного проще:
- отключать фичи;
- рефакторить отдельные части кода;
- выносить фичи в отдельные пакеты/репозитории.
Например, у вас появилась задача временно отключить экспериментальную фичу product-recommendations. Если она реализована как отдельный срез в features/product-recommendations, вам достаточно:
- убрать её импорт в нужных страницах;
- при необходимости — выключить её экспорт.
Это намного проще, чем выискивать куски логики по всему проекту.
Переиспользование сущностей и фич
Еще один важный плюс — FSD поощряет выделение сущностей (entities) и их осознанное переиспользование между фичами и страницами.
Представьте, что у вас есть сущность product, которая используется:
- в списке товаров;
- на карточке товара;
- в корзине;
- в блоке рекомендаций.
Вместо создания отдельных компонент ProductCard для каждой страницы вы держите ядро сущности в одном месте:
entities/
product/
ui/
ProductCard.tsx
ProductPrice.tsx
model/
types.ts
selectors.ts
productSlice.ts
Потом страницы и фичи просто композиционно используют это ядро:
// pages/product-list/ui/ProductListPage.tsx
import { ProductCard } from "@/entities/product/ui/ProductCard"
import { Product } from "@/entities/product/model/types"
type Props = {
products: Product[]
}
export const ProductListPage = ({ products }: Props) => {
// Здесь мы используем сущность Product в контексте страницы
return (
<div>
{products.map((product) => (
<ProductCard key={product.id} data={product} />
))}
</div>
)
}
// Здесь ProductCard переиспользуемый компонент сущности Product // Страница лишь композитно собирает UI не дублируя доменную логику
Как видите, это поощряет:
- повторное использование сущностей;
- единый источник правды для бизнес-логики конкретной сущности;
- меньше дублирования кода.
Управляемый рост проекта
Когда проект растет, архитектура без четких границ быстро превращается в «слоеный пирог» с неконтролируемыми связями. FSD предлагает:
- ограничить направления зависимостей (кто от кого может зависеть);
- явно задавать публичные интерфейсы модулей;
- минимизировать неявные связи.
Вы можете, например, установить правило:
sharedне может зависеть отfeaturesиentities;entitiesне может зависеть отfeatures;featuresмогут зависеть отentitiesиshared;pagesмогут зависеть отfeatures,entities,shared;appможет зависеть от всех.
Так вы сразу предотвращаете классическую ситуацию, когда:
- утилита из
sharedвдруг начинает импортировать бизнес-логику изfeatures; - сущность знает о конкретной странице;
- появляется циклическая зависимость.
Часто это еще и закрепляют статическим анализом (ESLint или custom-линтер): описывают разрешенные импорты между слоями.
Преимущества FSD для командной работы
Явное разделение зон ответственности
FSD хорошо ложится на разделение работы внутри команды. Например, в продуктовой разработке можно назначать:
- ответственных за сущности (доменные модели —
entities); - ответственных за фичи (
features); - ответственных за интеграцию и UX на уровне страниц (
pages).
Команда начинает мыслить:
- не «кто занимается кнопками»,
- а «кто отвечает за корзину», «кто за профиль пользователя», «кто за платежи».
Это совпадает с тем, как устроен продукт, а не с тем, как устроен фреймворк.
Ускоренный онбординг новых разработчиков
Новому разработчику не нужно запоминать сложные внутренние правила типа:
- «эти компоненты лежат тут, но используются там»;
- «вот этот хук используется только для заказов, хотя лежит в общей папке».
Достаточно объяснить два принципа:
- Все, что связано с пользовательской возможностью, ищите в
features. - Все, что связано с сущностью домена, — в
entities.
Затем показать пару примеров — и человек уже может самостоятельно ориентироваться.
Посмотрите на небольшой фрагмент структуры:
features/
auth-by-username/
ui/
LoginForm.tsx
model/
slice.ts
selectors.ts
types.ts
lib/
validators.ts
Даже если вы впервые видите проект, легко догадаться:
LoginForm.tsx— форма логина;slice.ts— Redux slice или аналог для состояния логина;validators.ts— логика проверки полей.
Удобное разделение задач
Когда вы заводите таску «добавить сохранение настроек фильтра при перезагрузке страницы», вы сразу можете сформулировать задачу:
- «доработать
features/product-filter»; - «расширить модель
entities/userнастройками фильтров» (если они привязаны к пользователю).
Так задачи из баг-трекера напрямую соотносятся с архитектурой проекта. Это снижает:
- количество коммуникаций;
- риск, что разработчик полезет не в ту часть системы;
- время на оценку задач.
Преимущества FSD для качества кода и снижения технического долга
Управляемые публичные интерфейсы
FSD предполагает, что у каждого среза (feature, entity, page) есть публичный интерфейс — то, что разрешено импортировать снаружи. Чаще всего это файл вроде index.ts или public-api.ts в корне среза.
Давайте разберемся на примере:
features/
add-to-cart/
ui/
AddToCartButton.tsx
model/
useAddToCart.ts
index.ts
// features/add-to-cart/index.ts
export { AddToCartButton } from "./ui/AddToCartButton"
export { useAddToCart } from "./model/useAddToCart"
// Внешний мир видит только то что мы явно экспортировали здесь
// Здесь мы формируем публичный интерфейс фичи // Внешний код не должен импортировать файлы напрямую из ui или model
Теперь в других частях приложения вы импортируете только из корня фичи:
// pages/product-card/ui/ProductCardPage.tsx
import { AddToCartButton } from "@/features/add-to-cart"
// Здесь страница использует только публичный интерфейс фичи
// Внутреннюю структуру фичи можно менять без влияния на внешние модули
Преимущества такого подхода:
- можно безопасно менять внутреннюю структуру фичи;
- упрощается рефакторинг;
- нет «случайных» зависимостей от внутренних деталей реализации.
Снижение связанности и упрощение тестирования
Когда у вас явно выделены сущности и фичи, тестировать становится проще:
- вы тестируете фичу как отдельный модуль;
- вы тестируете сущность как отдельный модуль;
- страницы — это в основном композиция, их тесты содержат минимум логики.
Например, модуль features/add-to-cart может иметь свои unit-тесты:
// features/add-to-cart/model/useAddToCart.test.ts
import { renderHook, act } from "@testing-library/react"
import { useAddToCart } from "./useAddToCart"
test("добавляет товар в корзину", () => {
const { result } = renderHook(() => useAddToCart())
act(() => {
result.current.add({ id: "1", name: "Product" })
})
expect(result.current.items).toHaveLength(1)
expect(result.current.items[0].id).toBe("1")
})
// Здесь мы тестируем поведение фичи в отрыве от страницы
// Тесты изолированы от конкретной страницы и от общего состояния приложения // Это повышает надежность и облегчает рефакторинг
В результате:
- проще поддерживать тестовое покрытие;
- изменения не ломают пол-репозитория тестов;
- можно развивать фичи независимо друг от друга.
Контроль за ростом shared-слоя
Одна из типичных проблем крупных фронтенд-проектов — гигантская папка shared, в которую складывают «все, что неясно куда деть». В FSD есть встроенный «предохранитель»:
- сначала вы пытаетесь реализовать логику внутри конкретной фичи или сущности;
- только потом, если реально появляется повторное использование, выносите это в
shared.
Так shared растет осознанно, а не стихийно. Это существенно снижает риск:
- захламленного набора утилит;
- дублирующих компонентов и функций;
- сильной связанности «всего со всем».
Преимущества FSD для работы с сложными доменами
Явное выделение доменных сущностей
В проектах с нетривиальной предметной областью (маркетплейсы, CRM, банковские системы) доменных сущностей много, и они сложные. FSD предлагает:
- выделить сущности в отдельный слой
entities; - строго разделять их от конкретных пользовательских фич.
Например:
entities/
user/
product/
order/
payment/
notification/
Каждая из этих сущностей:
- имеет свой набор типов, моделей, селекторов;
- может иметь UI-компоненты для отображения;
- может использоваться в нескольких фичах и процессах.
Фичи, в свою очередь, описывают то, что делает пользователь:
features/
place-order/
cancel-order/
pay-order/
apply-discount/
subscribe-notifications/
Так, когда вы расширяете домен (например, добавляете новый статус заказа), вы:
- вносите изменения в
entities/order; - а потом уже дорабатываете фичи, которые зависят от этого статуса.
Проще обсуждать архитектуру с бизнесом
Еще один неожиданный плюс — разговоры с продакт-менеджерами и аналитиками становятся проще. Вы можете обсуждать:
- сущности (User, Product, Order);
- фичи (Place Order, Cancel Order, Pay Order).
И это напрямую соответствует архитектуре проекта:
- «Эта фича реализована в
features/place-order»; - «Эта сущность у нас описана в
entities/order».
Таким образом:
- меньше «перевода» между техническим и бизнес-языком;
- проще синхронизировать требования и реализацию;
- легче держать документацию и код в одном понятийном поле.
Преимущества FSD для внедрения новых технологий и рефакторинга
Локализованный технологический стек внутри фичи
Если вы хотите попробовать новую библиотеку (например, для работы с формами), FSD позволяет ограничить эксперимент рамками одной фичи.
Например:
- текущий стек форм —
react-hook-form; - вы хотите попробовать
Formikтолько в одной фичеfeatures/feedback-form.
Вы можете:
- внутри этой фичи использовать другой подход;
- при успешном результате перенести лучшие практики в другие фичи.
Это снижает риски и упрощает эксперименты.
Постепенный рефакторинг без «большого взрыва»
Когда архитектура хаотична, любое серьезное изменение (например, замена стейт-менеджера) превращается в масштабный рефакторинг всего приложения.
В FSD вы можете:
- менять внутреннюю реализацию состояния в конкретной фиче;
- сохранять ее публичный интерфейс;
- затем постепенно переводить страницы на новый подход.
Например, фича add-to-cart использовала Redux:
// features/add-to-cart/model/index.ts
export { addToCartReducer } from "./slice"
export { useAddToCart } from "./hooks"
Со временем вы переписываете ее на локальный Zustand или jotai, но снаружи интерфейс остается тот же:
// features/add-to-cart/index.ts
export { useAddToCart } from "./model/useAddToCart"
Страницам и другим фичам все равно, что внутри:
- вы постепенно обновляете модули;
- не ломаете внешние зависимости.
Преимущества FSD для DX (Developer Experience)
Снижение когнитивной нагрузки
Когда проект организован по FSD, у вас меньше вещей, которые надо держать в голове:
- вы знаете, где искать фичу;
- вы понимаете, как устроены зависимости;
- вы не тратите время на то, чтобы «разгадать» структуру.
Это особенно заметно:
- в долгосрочной перспективе;
- когда вы переключаетесь между задачами и контекстами;
- когда работаете в проекте время от времени.
Единообразие подходов
FSD поощряет использование одних и тех же паттернов:
- структура среза (ui, model, lib);
- экспорт через публичный интерфейс;
- единый подход к именованию.
Это снижает количество «индивидуальных стилей» внутри проекта. В результате:
- меньше сюрпризов в чужом коде;
- проще ревьюить изменения;
- легче поддерживать договоренности по архитектуре.
Облегчение автоматизации и инструментов
Из-за четкой структуры становится проще:
- автоматизировать генерацию модулей (CLI для создания новой фичи);
- анализировать зависимости и находить нарушения архитектурных правил;
- внедрять code mods для массовых изменений.
Например, вы можете написать простой скрипт, который создает каркас новой фичи:
# Пример условной команды генерации фичи
yarn gen feature add-to-favorites
Форма генератора создаст:
features/
add-to-favorites/
ui/
AddToFavoritesButton.tsx
model/
useAddToFavorites.ts
index.ts
Вы сразу получаете:
- структуру в соответствии с архитектурой;
- готовый публичный интерфейс;
- минимальный шаблон кода.
Это ускоряет задачу «завести новую фичу» и снижает риск, что кто-то нарушит договоренности по структуре.
Заключение
Feature-Sliced Design дает системе координат, в которой фронтенд-проект растет предсказуемо и управляемо. Основные преимущества FSD можно свести к нескольким ключевым эффектам:
- Повышенная читаемость и предсказуемость — вы всегда знаете, где искать код, связанный с конкретной фичей или сущностью.
- Лучшее масштабирование — как по размеру кодовой базы, так и по количеству разработчиков и команд.
- Снижение уровня хаоса и технического долга — из-за четких границ, публичных интерфейсов и контролируемых зависимостей.
- Удобная работа с доменными сущностями — архитектура напрямую отражает предметную область и пользовательские сценарии.
- Упрощенный рефакторинг и эксперименты — изменения можно делать локально, не устраивая «бурю» во всем проекте.
- Улучшенный developer experience — меньше когнитивной нагрузки, единообразные подходы, облегченное ревью и онбординг.
FSD особенно хорошо проявляет себя на средних и крупных проектах, где без архитектурной дисциплины фронтенд быстро превращается в набор сложно связанных модулей. При этом многие принципы FSD можно постепенно внедрять и в существующие проекты, начиная с организации новых фич и сущностей по слоям и срезам.
Частозадаваемые технические вопросы по теме и ответы
Как постепенно внедрять FSD в уже существующий проект
- Не пытайтесь переписать все сразу.
- Начните с новых фич — кладите их в
features, сущности — вentities, общие вещи — вshared. - При доработке старого модуля постепенно выносите его части в FSD-структуру.
- Добавьте правило: новые модули — только в FSD-слоях.
- Постепенно вы «обрастете» островками FSD, которые со временем займут большую часть проекта.
Как контролировать допустимые импорты между слоями
- Введите архитектурные правила зависимостей (например,
featuresне импортируетpages). - Настройте ESLint с плагинами вроде
eslint-plugin-boundariesилиeslint-plugin-import. - Опишите в конфиге разрешенные пути импорта между слоями.
- Включите правило в CI, чтобы запретить merge при нарушении.
Как организовывать типы и API-клиенты в FSD
- Базовые API-клиенты (HTTP-клиент, конфиг, базовые обертки) — в
shared/api. - Типы и адаптеры, специфичные для сущности — в
entities/<entity>/model. - Фича может использовать API сущности через её модель или собственные клиентские функции в
features/<feature>/model. - Избегайте прямых вызовов глобального API-клиента из любого места — лучше через слой сущностей.
Как лучше организовать роутинг в FSD проекте
- Основная конфигурация маршрутов — в
app/routing. - Конкретные компоненты страниц — в
pages/<page-name>/ui. - В конфиге маршрутов импортируйте только публичный компонент страницы, не завязываясь на внутреннюю структуру фич.
- Если у страницы сложная логика загрузки данных, выносите её в
pages/<page-name>/modelи подключайте в корневом компоненте страницы.
Можно ли совмещать FSD с монорепой и разделением на пакеты
- Да, FSD-слои можно выносить в отдельные пакеты монорепы (
@app/*,@entities/*,@features/*,@shared/*). - Следите, чтобы между пакетами сохранялись те же правила зависимостей, что и внутри одного репо.
- Общие сущности и фичи можно выделять в отдельные пакеты и переиспользовать в нескольких приложениях.