Олег Марков
Преимущества Feature-Sliced Design - как этот подход меняет разработку фронтенда
Введение
Feature-Sliced Design (FSD) — это методология архитектуры фронтенд-приложений, которая помогает выстраивать масштабируемую, предсказуемую и удобную в сопровождении структуру проекта. В отличие от классического разделения на слои по типам (components, services, utils), здесь все крутится вокруг фич и сценариев пользователя.
Основная идея проста и при этом довольно мощная: код организуется не по техническим деталям, а по бизнес-логике и областям ответственности. Это означает, что вы группируете сущности, фичи и страницы так, как их воспринимает продукт, а не так, как это удобно только с точки зрения фреймворка.
Здесь я покажу вам, какие реальные преимущества дает Feature-Sliced Design, как он выглядит в коде и структуре директорий, и почему его все чаще выбирают для средних и больших фронтенд-проектов.
Краткий обзор Feature-Sliced Design
Основные принципы
Давайте сначала обозначим ключевые принципы FSD, чтобы дальше было проще связывать их с преимуществами:
Фиче-центричность
Главная единица архитектуры — фича (feature), а не страница или компонент. Именно фичи несут в себе бизнес-ценность.Слои (layers)
Приложение разделяется на уровни ответственности. Классическая схема выглядит примерно так:- app
- processes
- pages
- features
- entities
- shared
Срезы (slices)
Внутри слоев код разбивается по доменным областям (например, profile, cart, product), чтобы логика бизнеса не размазывалась по всему проекту.Явные зависимости
Каждый слой и каждый модуль имеет строго определенные правила, от кого он может зависеть. Более “верхние” слои могут зависеть от нижних, но не наоборот.Public API модулей
Каждый доменный срез (feature, entity и т.д.) предоставляет наружу понятный «публичный API» — обычно через файл index.ts или public.ts. Внешний код не лезет внутрь структуры модуля.
Смотрите, я покажу вам, как это выглядит в типичном React-проекте.
src/
app/
providers/
index.tsx // Точка входа приложения
processes/
auth/
model/
ui/
index.ts
pages/
profile/
ui/
ProfilePage.tsx
index.ts
features/
edit-profile/
model/
useEditProfile.ts
ui/
EditProfileForm.tsx
index.ts // Публичный API фичи
entities/
user/
model/
userStore.ts
ui/
UserCard.tsx
index.ts // Публичный API сущности User
shared/
ui/
Button/
Button.tsx
index.ts
lib/
api/
form/
config/
Комментарии к структуре:
// app — слой инициализации приложения
// processes — долгоживущие процессы, вроде авторизации
// pages — страницы, которые собирают фичи и сущности
// features — законченные пользовательские сценарии
// entities — бизнес-сущности (User, Product и т.п.)
// shared — переиспользуемые блоки без знания домена
Теперь, когда базовая картинка есть, давайте разберем преимущества.
Преимущество 1. Масштабируемость структуры проекта
Проблема “папки components” и хаоса при росте
В классической архитектуре фронтенда часто можно увидеть такое:
src/
components/
Header.tsx
Footer.tsx
ProfileForm.tsx
ProductCard.tsx
...
services/
utils/
pages/
Сначала это кажется удобным, но со временем:
// Компоненты разных доменов перемешиваются
// Тяжело понять, какие компоненты относятся к какой части бизнес-логики
// Изменения в одной фиче часто затрагивают файлы в разных местах
Feature-Sliced Design решает эту проблему за счет фокусировки на доменных областях и фичах.
Как FSD улучшает масштабируемость
За счет деления на слои и срезы каждый новый функционал ложится в понятное место. Давайте разберемся на примере добавления фичи «Редактирование профиля пользователя».
Вместо того чтобы искать “подходящее место” в папке components, вы заводите отдельный модуль фичи:
src/
features/
edit-profile/
model/
useEditProfile.ts // Логика работы формы
ui/
EditProfileForm.tsx // Компонент формы
api/
updateProfile.ts // Запрос к API
index.ts // Публичный API фичи
Теперь вы увидите, как это выглядит в коде использования на странице:
// src/pages/profile/ui/ProfilePage.tsx
// Импортируем только из публичного API фичи
import { EditProfileForm } from "features/edit-profile"
import { UserCard } from "entities/user"
// Здесь мы собираем страницу из сущностей и фич
export const ProfilePage = () => {
return (
<div>
{/* Отображаем данные пользователя */}
<UserCard />
{/* Даём возможность редактировать профиль */}
<EditProfileForm />
</div>
)
}
Комментарии:
// ProfilePage ничего не знает о внутренней структуре edit-profile
// Вся логика фичи спрятана за её публичным API
// При расширении фичи (валидация, шаги, доп. поля) структура профиля не ломается
За счет такой организации:
- можно безопасно расширять любую фичу внутри её папки;
- легко найти, где именно живёт нужный сценарий;
- проще подключать новых разработчиков — они видят структуру по фичам, а не по технологиям.
Преимущество 2. Предсказуемые зависимости и контроль над связями
Зачем контролировать зависимости
Без архитектурных правил удобно быстро начинать, но потом возникает:
// Циклические зависимости между модулями
// Сильная связанность — изменения в одном месте ломают другие
// Сложность тестирования — модуль тащит за собой “полпроекта”
Feature-Sliced Design вводит строгие правила: кто от кого может зависеть. Это делает архитектуру более устойчивой.
Классическое правило слоев (упрощенно):
// shared — базовый слой, от него могут зависеть все
// entities — могут зависеть от shared
// features — могут зависеть от entities и shared
// pages — могут зависеть от features, entities, shared
// app — может зависеть от всех слоев, так как он «корневой»
Смотрите, я покажу вам пример неправильной зависимости.
// ПЛОХО - фича тянет в себя страницу
// src/features/edit-profile/model/useEditProfile.ts
import { ProfilePage } from "pages/profile"
// Здесь фича знает о конкретной странице,
// на которой она может быть использована.
// Это нарушает принцип переиспользования.
Правильный вариант — фича не должна знать, где её используют:
// ХОРОШО - страница использует фичу, а не наоборот
// src/pages/profile/ui/ProfilePage.tsx
import { EditProfileForm } from "features/edit-profile"
export const ProfilePage = () => {
return <EditProfileForm />
}
Комментарии:
// Фича становится переиспользуемой — её можно использовать на другой странице
// Нет циклов вида pages → features → pages
// Тестировать фичу можно отдельно от страниц
Визуализация потока зависимостей
Если кратко, зависимости идут сверху вниз:
app
↓
processes
↓
pages
↓
features
↓
entities
↓
shared
Такой единый “вектор” сильно снижает риск циклов и случайной связности.
Преимущество 3. Легкость локализации изменений
“Локальные” изменения против “расползания” по коду
Когда вы редактируете фичу в обычной архитектуре, часто приходится залезать:
// в компоненты
// в services
// в utils
// в некоторые hooks, разбросанные по проекту
В FSD вся логика конкретной фичи локализована в её модуле. Давайте посмотрим на примере фичи “добавление товара в корзину”.
src/
features/
add-to-cart/
model/
useAddToCart.ts
ui/
AddToCartButton.tsx
api/
addToCartApi.ts
index.ts
Теперь давайте посмотрим, что происходит в следующем примере использования этой фичи:
// src/entities/product/ui/ProductCard.tsx
import { AddToCartButton } from "features/add-to-cart"
export const ProductCard = ({ product }) => {
return (
<div>
{/* Здесь показываем карточку товара */}
<h3>{product.title}</h3>
<p>{product.price}</p>
{/* Кнопка добавления в корзину - отдельная фича */}
<AddToCartButton productId={product.id} />
</div>
)
}
Комментарии:
// Фича add-to-cart “прилипает” к любой сущности Product
// Внутреннюю реализацию (API, store, обработчики ошибок) можно менять локально
// ProductCard не ломается от внутренних изменений фичи
Если вам нужно изменить бизнес-правила добавления в корзину (например, не давать добавлять более 10 единиц), вы идете только в папку features/add-to-cart. Вам не нужно проверять десятки рассыпавшихся по проекту обработчиков.
Преимущество 4. Улучшение командной работы
Разделение зон ответственности в команде
В реальных проектах над кодом работают несколько человек, часто параллельно. Когда структура неявная, разработчики начинают “наступать друг другу на ноги” — трогать одни и те же файлы, пересекаться по ответственности, конфликтовать при слиянии веток.
Feature-Sliced Design помогает распределить зоны:
- один разработчик отвечает за фичу “поиск товаров”;
- второй — за фичу “оформление заказа”;
- третий — за “профиль пользователя”.
Смотрите, я покажу вам упрощенную картину ответственных:
features/
search-products/ # Ответственный: dev1
checkout/ # Ответственный: dev2
edit-profile/ # Ответственный: dev3
entities/
user/ # Ответственный: dev3
product/ # Ответственный: dev1
order/ # Ответственный: dev2
Комментарии:
// Каждый владеет своей частью домена
// Изменения реже приводят к конфликтам в Git
// Код-ревью легче выстроить по зонам ответственности
Параллельная разработка фич
Еще один важный момент: фичи в FSD проектируются так, чтобы они были как можно более независимыми.
Давайте разберемся на примере:
- dev1 делает фичу “wishlist” (список желаний)
- dev2 делает фичу “recommendations” (рекомендованные товары)
Обе фичи опираются на одну сущность Product из entities. При этом каждая фича живет в своей папке и не вмешивается в код другой:
features/
wishlist/
...
recommendations/
...
entities/
product/
...
Это существенно снижает “трение” между разработчиками и ускоряет разработку.
Преимущество 5. Упрощение повторного использования и изоляции фич
Фича как самостоятельный блок
В FSD фича — это не только UI-компонент, но и:
// логика (hooks, store, handlers)
// API-запросы
// валидация
// работа с сущностями
За счет такой упаковки фичу легко:
- перенести между проектами;
- включать и выключать;
- подменять реализацию (например, другой API, но тот же UI).
Покажу вам, как это реализовано на практике на примере фичи “логин по email и паролю”.
features/
auth-by-email/
model/
useAuthByEmail.ts
ui/
AuthByEmailForm.tsx
api/
loginByEmail.ts
index.ts
Публичный API фичи:
// src/features/auth-by-email/index.ts
// Экспортируем только то, что должно быть видно снаружи
export { AuthByEmailForm } from "./ui/AuthByEmailForm"
export { useAuthByEmail } from "./model/useAuthByEmail"
А вот как фича используется внутри страницы:
// src/pages/login/ui/LoginPage.tsx
import { AuthByEmailForm } from "features/auth-by-email"
export const LoginPage = () => {
return (
<div>
<h1>Login</h1>
{/* Вставляем готовую фичу авторизации */}
<AuthByEmailForm />
</div>
)
}
Комментарии:
// Логику авторизации легко вынести в другое приложение
// При необходимости можно заменить реализацию loginByEmail.ts, не трогая LoginPage
// Добавление альтернативного способа входа (например, OAuth) не ломает текущую фичу
Преимущество 6. Улучшенная тестируемость
Тестирование фич и сущностей вместо “страниц целиком”
В FSD каждый модуль имеет относительно узкую ответственност, что делает его хорошей единицей для тестирования.
Рассмотрим фичу “редактирование профиля”. Внутри неё есть хук useEditProfile, который управляет состоянием формы и отправкой данных.
Теперь давайте посмотрим, что происходит в следующем примере теста:
// src/features/edit-profile/model/useEditProfile.test.ts
import { renderHook, act } from "@testing-library/react"
import { useEditProfile } from "./useEditProfile"
describe("useEditProfile", () => {
test("должен вызывать onSuccess после успешного сохранения", async () => {
const onSuccess = jest.fn()
// Рендерим хук
const { result } = renderHook(() => useEditProfile({ onSuccess }))
// Вызываем метод сохранения
await act(async () => {
await result.current.submit({
name: "John",
email: "john@example.com",
})
})
// Проверяем, что коллбек был вызван
expect(onSuccess).toHaveBeenCalled()
})
})
Комментарии:
// Мы тестируем только бизнес-логику фичи, без UI и без страниц
// Фича изолирована от остального приложения — легче мокать зависимости
// Тесты работают быстро и точечно
Если бы фича была “размазана” по страницам и общим hooks, тесты пришлось бы писать сложнее, сильно завязываясь на контекст всего приложения.
Преимущество 7. Лучшая читабельность и onboarding новичков
Когда вы открываете чужой проект
Типичная ситуация: вы открываете незнакомый фронтенд-проект и пытаетесь понять, где искать:
// логику корзины
// обработку профиля
// авторизацию
// фильтры каталога
В FSD все лежит на своих местах по доменам. Если вам нужно разобраться с корзиной, вы идете:
features/
add-to-cart/
remove-from-cart/
update-cart-item/
entities/
cart/
product/
Если вам нужен профиль пользователя:
features/
edit-profile/
change-password/
upload-avatar/
entities/
user/
Давайте посмотрим, как это выглядит для новичка в проекте:
src/
app/ # точка входа и окружение
pages/ # конечные страницы
features/ # основные пользовательские сценарии
entities/ # бизнес-сущности
shared/ # общие компоненты, утилиты и конфиг
Комментарии:
// Новичку достаточно несколько минут, чтобы понять структуру
// Не нужно запоминать “особые” места для специфических компонентов
// Локализация логики по доменам снижает порог входа
Преимущество 8. Гибкость при изменении бизнес-требований
Бизнес меняется — архитектура должна выдерживать
В реальных проектах требования постоянно меняются. Именно в такие моменты хорошо видно, насколько архитектура живая и гибкая.
В FSD фичи и сущности спроектированы так, чтобы их можно было:
// расширять дополнительными сценариями
// заменять частями (например, новый дизайн UI, но старая логика)
// перераспределять между страницами
Давайте посмотрим, что происходит в следующем примере изменения требования:
- Было: фича edit-profile доступна только на странице ProfilePage.
- Стало: нужно дать возможность редактировать профиль сразу после регистрации на странице WelcomePage.
Без FSD можно было бы иметь “жёстко вшитую” форму в ProfilePage. При переносе на WelcomePage понадобилось бы дублирование или вынос в какой‑то общий компонент.
С FSD фича уже вынесена в отдельный модуль:
// src/pages/welcome/ui/WelcomePage.tsx
import { EditProfileForm } from "features/edit-profile"
export const WelcomePage = () => {
return (
<div>
<h1>Welcome</h1>
{/* Повторно используем ту же фичу */}
<EditProfileForm />
</div>
)
}
Комментарии:
// Перенос функционала — это просто новый импорт
// Фича не привязана к конкретной странице
// Менять бизнес-процессы можно, не переписывая внутреннюю реализацию
Преимущество 9. Подготовленность к частичному рефакторингу и миграциям
“Рефакторим по кускам, а не сразу весь проект”
Feature-Sliced Design хорошо поддерживает идею поэтапного рефакторинга. Вы можете:
// взять одну доменную область (например, auth)
// выделить её в entities и features
// постепенно приводить к FSD-структуре
// не трогать остальной проект сразу
Смотрите, я покажу вам пример переходного состояния:
src/
legacy/
components/
OldAuthForm.tsx
services/
authService.ts
features/
auth-by-email/
model/
ui/
api/
entities/
user/
Комментарии:
// Параллельно живут старые и новые модули
// Новая фича auth-by-email уже организована по FSD
// Старый код постепенно выносится из legacy в entities/features
Так можно мигрировать большие монолитные фронтенды, не останавливая разработку и не переписывая все “с нуля”.
Преимущество 10. Облегченная интеграция с инструментами и линтерами
Архитектурные правила можно формализовать
Еще одно важное преимущество — FSD хорошо поддается автоматизации. Вы можете формализовать правила:
// какие слои могут импортировать какие
// откуда нельзя импортировать напрямую
// как должны выглядеть пути импорта
Например, с помощью ESLint и плагинов (страницы не могут импортировать страницы, фичи не могут импортировать app и т.д.).
Пример простого правила импорта в конфигурации ESLint (псевдокод):
// .eslintrc.js
module.exports = {
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["pages/*"],
message: "Нельзя импортировать другие страницы напрямую",
},
{
group: ["app/*"],
message: "Нельзя импортировать слой app из нижних слоев",
},
],
},
],
},
}
Комментарии:
// Такие правила помогают держать архитектуру в порядке
// Новички не смогут “случайно” нарушить слои
// Крупные команды получают единый стандарт
Заключение
Feature-Sliced Design — это не просто “еще одна структура папок”, а целостный подход к проектированию фронтенд-архитектуры на основе бизнес-доменов и пользовательских сценариев.
Ключевые преимущества, которые вы получаете:
- предсказуемая и масштабируемая структура проекта;
- четкий контроль зависимостей между слоями и модулями;
- локализация изменений внутри фич и сущностей;
- удобное разделение ответственности внутри команды;
- повышенная повторноиспользуемость фич;
- более простое и точечное тестирование;
- быстрый onboarding для новых разработчиков;
- гибкость при изменении бизнес-требований;
- поддержка поэтапного рефакторинга и миграций;
- возможность формализовать архитектурные правила через инструменты.
Подход Feature-Sliced Design особенно хорошо раскрывается в средних и больших проектах, где сложность бизнеса и команды уже не позволяет полагаться только на “компонентную” структуру и личную дисциплину разработчиков. Если вы чувствуете, что ваш фронтенд уже начинает “расползаться” по структуре и становится трудно поддерживаемым, FSD дает понятный, практичный и ориентированный на продукт путь упорядочивания кода.
Частозадаваемые технические вопросы
Как начать внедрять Feature-Sliced Design в уже существующий проект без тотального рефакторинга
- Выделите один домен, где больше всего боли, например auth или cart.
- Создайте базовые слои app, pages, features, entities, shared.
- Внутри выбранного домена перенесите код в entities и features по FSD-принципам.
- Страницы пока могут оставаться старыми, но использовать уже новые фичи.
- Постепенно переносите остальные домены, сохраняя рабочее состояние приложения.
Как правильно организовать импорты при использовании алиасов (features, entities и т.д.)
- Настройте алиасы на уровне сборщика (Webpack, Vite, TSConfig) для путей вида features/, entities/, shared/* и т.д.
- Импортируйте только из публичного API модуля — из index.ts внутри среза.
- Избегайте относительных импортов, которые “пробивают” вверх или в соседние домены.
- Добавьте ESLint-правила, запрещающие импорты внутрь private-файлов других модулей.
Как разделять логику между entities и features если кажется что всё относится к сущности
- В entities размещайте “чистое” представление сущности — типы, базовую бизнес-логику, UI-компоненты без сценариев.
- В features выносите конкретные пользовательские сценарии с участием сущности — изменения, формы, сложные взаимодействия.
- Если модуль сложно описать одной фразой “пользователь делает X”, скорее всего это entities.
- Если легко — это feature, даже если она сильно завязана на одну сущность.
Как использовать глобальное состояние (например Redux или Zustand) в контексте FSD
- Размещайте сторы рядом с тем модулем, к которому они логически принадлежат — чаще всего это entities или features.
- Не делайте один большой “global store” без привязки к доменам.
- В слое app можно держать только инфраструктурные сторы (навигация, конфиг, фичефлаги).
- Экспортируйте хук или селекторы через публичный API модуля, не раскрывая внутреннюю структуру стора.
Как поступать с кросс-доменной логикой которая затрагивает несколько сущностей и фич
- Определите, что именно является общим процессом — например “оформление заказа” затрагивает user, cart, payment.
- Вынесите этот процесс в слой processes как отдельный модуль.
- Внутри него оркестрируйте взаимодействие между features и entities, не дублируя их логику.
- Страницы должны работать уже с process как с “супер-фичей”, не влезая в его внутренние связи.