Мы увязли в Feature-Sliced Design
- среда, 6 мая 2026 г. в 00:00:10

Всем привет, меня зовут Сергей Сибара, я фронтенд-разработчик в ИТ-холдинге Т1. Эта статья —продолжение предыдущей: Мой справочник по Feature-Sliced Design. На этот раз я рассмотрю, как по моему субъективному мнению улучшить файловую структуру проекта, нарушая рекомендации FSD. Я разрабатываю на React.js, поэтому статья написана с уклоном на него.
Разделение структуры большого проекта на несколько подпроектов
Вынесение связанного изолированного кода вне основного проекта
Стоит ли использовать разделение на слои entities, features, widgets, pages?
Группирование по функциональности (по вложенным слайсам) вместо группирования по сегментам
Структура слайса с несколькими компонентами интерфейса и моделями
Структура слайса для нескольких вариантов одной сущности или очень похожих сущностей
Чтобы у нас с вами не было путаницы, я добавил небольшой глоссарий по используемым терминам:
Подсистема — группа любых программных сущностей (переменные, функции, объекты, типы), сфокусированная на реализации какой‑либо связанной части приложения. Примеры: утилиты для работы с датой, менеджер модальных окон, страница или связанная группа страниц, любой компонент интерфейса, состоящий из программных сущностей, реализующих его отображение и поведение). Подсистема обычно состоит из вложенных подсистем.
Принцип High Cohesion (высокая связность). Он гласит, что в одной и той же подсистеме, файле, классе или функции желательно писать только код, предназначенный для решения одной задачи либо связанных или похожих задач.
Принцип Low Coupling (низкое зацепление). Он связан с предыдущим и гласит, что подсистемы, файлы, классы, функции и прочие программные сущности, решающие разные задачи, желательно делать менее связанными друг с другом.
Правила методологии носят рекомендательный, а не обязательный характер. Где‑то они помогут, а где‑то — наоборот. Разработчики методологии упоминают, да и в документации FSD написано, что она не является «серебряной пулей». Более того, со временем они приходят к выводу, что те или иные рекомендации часто неэффективны, и она понемногу меняется. Рамок, которыми она ограничивает структуру, недостаточно, чтобы покрыть все возможные сценарии, да ещё и с применением всех существующих технологий. Поэтому в реальности ограничения методологии могут играть роль палок в колесах разработки, и в таких случаях не возбраняется и нарушить FSD.
Правильнее не слепо следовать рекомендациям FSD, а разобраться в них, понять, какие из них и в каких ситуациях будут полезны именно в вашем проекте, а в каких их применение не оправданно. На мой взгляд, при решении, где и каким рекомендациях следовать, надо отталкиваться от навыков разработчиков. Начинающим лучше следовать жёстким правилам. Опытным разработчикам, знакомым с принципами структурирования приложений, зачастую лучше создавать структуру под конкретные ситуации, сильно не загоняя себя в рамки правил, экспериментировать. Правила, конечно, нужны, но в то же время они должны быть достаточно гибкими.
Давайте рассмотрим, когда и как можно отклониться от рекомендаций FSD, чтобы структура проекта стала лучше.
Если не учитывать размер проекта, связи между его функциональностью, команду, условия разработки, то строгое соблюдение архитектурных принципов вроде SOLID, GRASP, применение различных библиотек и подходов для «улучшения» кодовой базы может оказаться избыточным усложнением.
Чем меньше проект, тем меньше необходимость разделять его на многочисленные слои и строго следовать архитектурным правилам. В совсем маленьких проектах они избыточны: на их соблюдение потратится время, но пользу они, скорее всего, не принесут.
В небольших приложениях размещать переиспользуемую бизнес‑логику в слое shared, скорее всего, будет более эффективно, чем вводить дополнительные слои. Для таких проектов хватит трёх слоёв, например:
shared pages app
В проектах побольше уже целесообразно выделять четвёртый слой и выносить часть переиспользуемой бизнес‑логики в отдельный слой. По моим наблюдениям, большинство фронтенд‑проектов попадают именно в эту категорию.
В создаваемой альтернативе FSD — Evolution‑design — также демонстрируется, как количество слоёв можно адаптировать под масштаб проекта. Из интересных особенностей — в ней предлагается в shared слое размещать ещё и глобальные бизнес‑типы, и правила, нужные для работы инфраструктуры и всего приложения.
Альтернативой дальнейшему увеличению количества слоёв является разделение приложения на несколько подпроектов.
Можно вынести подпроекты в отдельные пакеты или добавить ещё один уровень иерархии перед уровнем слоёв. Это может пригодиться в больших проектах, в проектах с толстым клиентом, в монорепозиториях с микрофронтендами.
Подходы с организацией иерархии давно используются для решения проблем с масштабированием больших кодовых баз. Например:
Иерархическая машина состояний (иерархический конечный автомат);
Интерфейсы приложений довольно часто разделяют на разделы, работающие с разными бизнес‑сущностями и слабо (или вовсе не) связанные друг с другом. Эта ситуация встречается, например, при разработке нескольких микрофронтендов в одном монорепозитории (в этом видео с 31.14 до примерно 34.00 как раз обсуждают подобный пример). Стандартная FSD‑структура в таких проектах неэффективна: слои со временем перегружаются большим количеством слайсов, относящихся к разным микрофронтендам, что затрудняет навигацию по проекту и поддержку.
Решить эту проблему можно введением ещё одного уровня иерархии и группированием связанной функциональности внутри отдельных подпроектов. Получается, как бы несколько FSD‑приложений внутри одного. У каждого подпроекта будут только необходимые ему слои. При необходимости слои shared и app могут быть общими, а могут быть свои у каждого подпроекта. Пример:
📂 src ├── 📂 shared ├── 📂 app ├── 📂 fsdApp1 │ ├── 📂 entities │ ├── 📂 features │ ├── 📂 widgets │ └── 📂 pages ├── 📂 fsdApp2 │ ├── 📂 features │ └── 📂 pages └── 📂 fsdApp3 └── 📂 pages
В данном примере нет нарушений методологии, так как она не регламентирует структуру проекта выше уровня слоёв. Наоборот, в официальном чате можно встретить рекомендации разделять несвязанные или слабо связанные части приложения на отдельные подпроекты.
Чтобы снизить зацепление и соблюсти последовательность импортов, в некоторых проектах иногда уместно вынести связанную часть проекта вне его FSD структуры. Это может быть как инфраструктурная, так и бизнес‑часть. Можно сделать её отдельным пакетом, а можно изменить структуру проекта, например так:
📂 src ├── 📂 mainApp │ ├── 📂 shared │ ├── 📂 entities │ ├── 📂 features │ ├── 📂 widgets │ ├── 📂 pages │ └── 📂 app └── 📂 packages ├── 📂 subSystemA └── 📂 subSystemB
Структура подобных вынесенных подсистем может быть произвольной, в зависимости от содержимого. В некоторых случаях неуместно внутри неё применять FSD структуру, а в некоторых случаях внутри подсистемы достаточно только разделения на сегменты, без разделения на слои и слайсы.
Расположение слоя shared зависит от ситуации. Он может быть общим для основного проекта и вынесенных подсистем, а может быть у каждой подсистемы свой слой shared.
В FSD есть общее описание слоёв, не привязанное к особенностям проекта. И тут кроется проблема — не понятно, для каких проектов подходят описанные слои.
В больших проектах с толстым клиентом возможно эффективнее будет отделять доменную часть (та бизнес‑логика, которая при тонком клиенте располагается на бекенде) проекта от части, отвечающей за интерфейс. Для таких проектов понадобиться либо ввести ещё слои для доменной части, либо использовать другое определение слоёв entities и features. Слой entities может использоваться для задания сущностей и связей между разными сущностями, а в слой features может быть вынесена не относящаяся к интерфейсу бизнес‑логика по аналогии с Use Cases из Clean Arhitecture.
В тонких клиентах связи между сущностями уже отражены в DTO. Поэтому в таких проектах слой entities зачастую избыточен. Лучше редкие связи (кросс‑импорты) в слайсах слоя, чем ещё один слой.
В некоторых проектах с толстым клиентом возможно эффективнее будет ввести ещё один уровень иерархии перед уровнем слоёв, чтобы более явно отделить доменную часть от части, отвечающей за интерфейс. Пример:
📂 src ├── 📂 domainApp // здесь для примера взяты слои из Clean Arhitecture │ ├── 📂 entities │ ├── 📂 useCases │ └── 📂 controllers └── 📂 fsdApp ├── 📂 shared ├── 📂 widgets ├── 📂 pages └── 📂 app
Предупрежу, что я не проектировал структуру толстых клиентов, поэтому я описываю лишь своё виденье структуры, которую мне не приходилось применять. К тому же я продемонстрировал пример только одной структуры. Но для разных проектов оптимальная структура будет отличаться.
Я считаю неудачным разделение на эти слои. В нём нет чётких и прозрачных критериев, что к какому слою отнести. В небольших проектах разделение на эти слои избыточно. В крупных же приложениях по мере их роста связанные файлы будут находиться всё дальше друг от друга. Если не страницы создавать внутри этих слоёв, а эти слои внутри слайса страницы, то структура проекта будет менее дефрагментированной.
В проектах со слабосвязанными страницами и группами страниц, введение дополнительного слоя помимо pages будет избыточным. В проектах, где требуется выносить часто переиспользуемую в разных страницах функциональность, я предпочёл бы вынести её всю только в один бизнес‑слой с разрешением кросс‑импортов, а также при необходимости разделял проект не несколько подпроектов.
Согласно методологии FSD, кросс‑импорты между слайсами одного слоя нежелательны, но допускаются при использовании @x‑нотации в слое entities.
Следует минимизировать связи между слайсами одного слоя. Одним из способов уменьшения количества этих связей в FSD являются публичные API, которые можно считать аналогом паттерна фасад. Лично я не стал бы так сильно ограничивать кросс‑импорты между слайсами и не вводил бы символьные нотации.
Если страницы или разделы веб‑приложения часто связаны между собой, то наличие связей между их слайсами нежелательно. С ростом проекта количество связей будет расти, и он будет становиться всё более запутанным. Однако в проектах со слабосвязанными страницами и разделами веб-приложения редкие кросс‑импорты, как правило, не создают проблем. В таких случаях они могут оказаться более эффективными, чем вынесение редко переиспользуемой бизнес‑логики в отдельный слой.
Александр Моргунов в своей статье писал, плохи ли кросс‑импорты.
В моих прошлых проектах с тонким клиентом, объёмом в несколько десятков тысяч строк и написанных без FSD, было несколько связей между «слайсами» слоя pages. При этом, истины ради нужно отметить, что ни в одном проекте такие связи не привели к реальным проблемам. Структуры проектов остались достаточно прозрачными.
В FSD можно объединять в одной папке группу слайсов без кросс‑импортов. Но не рекомендуется создавать вложенные слайсы. В прошлой статье я уже писал, что это разные понятия.
В больших слайсах появляется необходимость как‑то сгруппировать связанные группы файлов. Для этого есть предложение разрешить вложенные слои, за счёт чего внутри слайсов получается фрактальная структура (слой → слайс → сегмент → слой ‑> слайс → сегмент). На мой взгляд, повторять внутри слайса иерархию FSD — это избыточное усложнение. Добавлять слои внутри слайса возможно имеет смысл в редких и очень сложных ситуациях.
В другом подходе к организации файловой структуры проекта — модульном — вместо фрактальной иерархии создаются вложенные папки, аналогичные вложенным слайсам, но без разделения на слои и сегменты. В проектах без FSD, где применялась модульная структура, мы с коллегами часто размещали в одной директории папки с подсистемами, отдельные файлы, иногда ещё и технические папки (вроде utils, stores). Запутанной свалки нигде не образовалось.
В описании Evolution Design в главе Этап 3: Grouped module есть подглава «Группы и подмодули», в которой приводится пример, который был бы аналогичен смешиванию сегментов и вложенных слайсов:

Если в слайсе много связанных вложенных слайсов, то он может получиться запутанным. Тут уже надо смотреть по ситуации, как решать проблему. Например, бывают ситуации, когда есть общие файлы и слайсы для других вложенных слайсов, и есть файлы, использующие несколько вложенных слайсов. В таком случае внутри слайса можно организовать последовательный импорт, аналогичный слоям:
📂 someSlice ├── 📂 common // Общий код, используемый в subSlices. Не может импортировать из subSlices и main. ├── 📂 subSlices // В эту папку нельзя импортировать из слайса main │ ├── 📂 subSlice1 │ └── 📂 subSlice2 └── 📂 main // Основой функционал слайса. Нет запрета на импорт в рамках someSlice
В сложных слайсах для разных ситуаций будут эффективны разные структуры. Искусственное ограничение одним‑двумя подходами ради стандартизации приведёт лишь к проблемам.
Я считаю, что разделение слайса на папки‑сегменты обычно является неудачным решением. Правило, что на третьем уровне (на четвёртом для группы слайсов) вложенности должны быть обязательно сегменты, а также рекомендации по ограничению взаимодействия внутри слайсов (например, рекомендация не размещать сегменты и вложенные слайсы на одном уровне вложенности) во многих ситуациях приводят к неудобной структуре слайса.
Для простых слайсов деление на папки‑сегменты избыточно. Слайс в FSD часто начинается сразу с ненужного усложнения — для 3–4 файлов в слайсе создаются папки‑сегменты. В методологии Evolution Design предлагается, на мой взгляд, более грамотный подход.
Для сложных слайсов деление на папки‑сегменты приводит к тому, что в папках config, lib, api, model, ui располагаются части нескольких подсистем. Иногда есть польза от группирования на папки‑сегменты, но внутри одной подсистемы.
Также встречается другая проблема — когда в сегменте ui появляются вложенные папки при практически полном отсутствии других сегментов в слайсе. Например:
📂 slice ├── 📄 pageModel.ts └── 📂 ui ├── 📂 modal │ ├── 📄 someInnerModalComponent.tsx │ └── 📄 modalContent.tsx ├── 📂 actionButtons │ ├── 📄 button1.tsx │ └── 📄 button2.tsx ├── 📂 form │ ├── 📄 useSubmit.ts │ └── 📄 form.tsx └── 📄 page.tsx
В этом примере файлы внутри сегмента ui сгруппированы по функциональному назначению. Если не следовать FSD и переместить их на уровень слайса, то структура не станет сложнее, но уменьшится вложенность. Если в будущем появятся модели, нужные только для определённых компонентов этого слайса, то их можно разместить рядом со связанными компонентами, а не в отдельной папке model, оторванной от контекста использования. Например (добавлены несколько файлов моделей: modalModel.ts, validationScheme.ts, formLogics.ts):
📂 slice ├── 📄 pageModel.ts ├── 📄 page.tsx ├── 📂 modal │ ├── 📄 someInnerModalComponent.tsx │ ├── 📄 modalModel.ts │ └── 📄 modalContent.tsx ├── 📂 actionButtons │ ├── 📄 button1.tsx │ └── 📄 button2.tsx └── 📂 form ├── 📄 useSubmit.ts ├── 📄 validationScheme.ts ├── 📄 formLogics.ts └── 📄 form.tsx
Чтобы было легче понять, к какому сегменту относится файл, а также с помощью линтеров контролировать связи между сегментами, можно добавлять название сегмента в конце имени файла, например:
order.api.ts orderFormScheme.model.ts orderForm.ui.tsx
Если в сегменте много файлов и нужно видеть какой файл относится к какому сегменту, то чтобы не добавлять суффикс сегмента к каждому файлу, можно завести папку, указывающую одновременно принадлежность к сегменту и назначение, например:
📂 slice ├── 📄 orderForm.tsx ├── 📄 orderApi.ts └── 📂 formModel ├── 📄 orderFormScheme.ts ├── 📄 dtoToFormData.ts └── 📄 formDataToDto.ts
В отличие от FSD, при группировании по функциональности получится меньше вложенность и не будет дублирования структуры в каждом сегменте. Нарушения Low Coupling и High Cohesion не будут настолько сильными. При этом структура останется понятной.
До появления FSD я много лет группировал файлы внутри слайсов по функциональному назначению, как и в примерах выше. При этом реже сталкивался с проблемами, чем в недавних проектах с FSD.
В одном из проектов с очень высоким темпом разработки была папка с группой связанных страниц — в сумме в ней было около 225 файлов. Эту группу страниц не я начинал делать, но я и другой разработчик продолжили. Местами была большевата вложенность, а также вместе с папками, в которых размещался весь функционал для определённых частей страниц, иногда размещались технические папки вроде utils, stores. Тем не менее, там было не особо сложно разобраться, не было споров, что и где разместить. Если бы файлы этой группы страниц были сгруппированы не по функциональному назначению, а по FSD, то проект разрабатывался бы намного медленнее и был бы более запутанным.
В следующих двух главах я рассмотрю пару ситуаций, в которых предпочтительнее использовать вложенные слайсы.
Допустим, в слайсе страницы находятся несколько компонентов интерфейса и каждый их них имеет свою модель, не связанную с другими. Как лучше организовать структуру папок для таких файлов?
В рамках FSD один из вариантов будет такой:
📂 slice ├── 📂 model │ ├── 📄 model1.ts │ ├── 📄 model2.ts │ └── 📄 model3.ts └── 📂 ui ├── 📄 component1.tsx ├── 📄 component2.tsx └── 📄 component3.tsx
Такая структура нарушает принципы Low Coupling и High Cohesion: связанные модель и компонент лежат в разных сегментах, а несвязанные друг с другом файлы — в одном.
В более сложных случаях, когда есть несколько связанных групп файлов, структура может выглядеть так:
📂 slice ├── 📂 model │ ├── 📂 group1 │ │ ├── 📄 model1ForGroup1.ts │ │ └── … // прочие файлы для этой группы │ ├── 📂 group2 │ │ ├── 📄 model1ForGroup2.ts │ │ ├── 📄 model2ForGroup2.ts │ │ └── … │ └── 📂 group3 │ ├── 📄 model1ForGroup3.ts │ └── … └── 📂 ui ├── 📂 group1 │ ├── 📄 component1ForGroup1.tsx │ └── … // прочие файлы для этой группы ├── 📂 group2 │ ├── 📄 component1ForGroup2.tsx │ ├── 📄 component2ForGroup2.tsx │ └── … └── 📂 group3 ├── 📄 component1ForGroup3.tsx └── …
Как видно из примера, в сегментах model и ui повторяется структура папок. В некоторых ситуациях может потребоваться ещё больше сегментов, в которых структура папок будет повторяться.
Будет удобнее группировать файлы по функциональной принадлежности, объединив каждую модель с соответствующим компонентом интерфейса:
📂 slice ├── 📂 subSlice1 │ ├── 📂 model │ │ ├── 📄 model1.ts │ └── 📂 ui │ └── 📄 component1.tsx ├── 📂 subSlice2 │ ├── 📂 model2 │ │ ├── 📄 model1ForSubSlice2.ts │ │ └── 📄 model2ForSubSlice2.ts │ └── 📂 ui │ ├── 📄 component1ForSubSlice2.tsx │ └── 📄 component2ForSubSlice2.tsx └── 📂 subSlice3 ├── 📂 model │ ├── 📄 model3.ts └── 📂 ui └── 📄 component3.tsx
Если во вложенном слайсе мало файлов, то можно обойтись без группирования сегментов в папки:
📂 slice ├── 📂 subSlice1 │ ├── 📄 model1.ts │ └── 📄 component1.tsx ├── 📂 subSlice2 │ ├── 📄 model1ForSubSlice2.ts │ ├── 📄 model2ForSubSlice2.ts │ ├── 📄 component1ForSubSlice2.ts │ └── 📄 component2ForSubSlice2.ts └── 📂 subSlice3 ├── 📄 model3.ts └── 📄 component3.tsx
Думаю, такая структура будет удобнее, чем вариант в рамках FSD с большей вложенностью. К тому же в небольших слайсах структура остаётся понятной и без дополнительного уровня вложенности.
Вкратце рассмотрим другой пример со структурой слайса, похожей на ту, что была в предыдущей главе: есть несколько страниц (или одна страница) для отображения разных вариаций одной сущности, или очень похожих сущностей, и для каждой страницы нужно более одного сегмента в слайсе. Для удобства и избегания дублирования написаны общие компоненты, позволяющие рендерить разные вариации сущности, что позволяет поместить файлы разных вариаций сущности в один слайс. Как лучше организовать структуру папок в такой ситуации?
Для такой ситуации будет удобнее нарушить FSD и создать вложенные слайсы с кросс‑импортами. Также где‑то должны храниться общие файлы для всех вложенных слайсов. Чтобы при этом не было путаницы в зависимостях между вложенными слайсами, потребуется самому продумывать правила импорта между ними исходя из ситуации.
Я считаю, что нет. Не так давно наткнулся на статью, автор которой считает так же. Он пишет, что «Чистую архитектуру» многие применяют для структурирования папок проекта, что на его взгляд ошибочно и приводит к плохой структуре папок проекта.
Структура папок является частью архитектуры проекта и в определённой мере они пересекаются. Но если чрезмерно стремиться, чтобы структура папок отражала архитектуру, скорее всего потеряется удобство.
Группирования по типам файлов, по сегментам, по уровню абстракции (сущность, фича, виджет, страница) сильно противоречат принципам Low Coupling и High Cohesion. А эти принципы довольно полезны для создания удобных, масштабируемых и менее запутанных структуры и архитектуры проекта. Даже при группировании по функциональности эти принципы будут нарушаться, но в меньшей степени.
Разделение кода на сегменты — правильно, но на уровне архитектуры (в связях между программными сущностями), а не файловой структуры проекта. Связи между программными сущностями не зависят от расположения файлов.
В FSD есть как полезные, так и вредные рекомендации. Мои краткие выводы о них:
В документации не хватает более детальной информации о том, для проектов с какими особенностями и какого размера стоит применять те или иные рекомендации.
Pages‑first — правильное направление развития FSD. Подобное решение успешно применялось задолго до появления FSD.
В чате методологии иногда рекомендуют разделять структуру проекта на несколько FSD иерархий (на несколько подпроектов). Считаю правильным разделение большого проекта на подпроекты.
Экспериментальная фрактальная структура — отчасти правильное направление развитие методологии. В предлагаемом виде чрезмерно усложнено. В большинстве случаев результат будет лучше, если убрать вложенные слои и папки‑сегменты, оставив вложенные слайсы и файлы‑сегменты.
Рекомендация не импортировать из верхних слоёв в нижние — несомненно правильна.
Не удачно выбрано группирование по уровню абстракции (сущность, фича, виджет, страница) на первом уровне иерархии, приводящая к тому, что несвязанная функциональность часто оказывается ближе, чем связанная.
Неудачные правила группирования по сегментам в слайсах, часто приводящие к избыточной вложенности или к тому, что несвязанная функциональность из нескольких подсистем оказывается ближе, чем связанная. В сложных слайсах получается неудобная и дефрагментированная структура.
В методологии чрезмерно рекомендуется избегать кросс‑импортов. Но контролируемые кросс‑импорты в умеренном количестве зачастую эффективнее запрета. Они не так уж и страшны, пока всё под контролем и структура получается прозрачной. Кросс‑импорты надо не запрещать, а ограничивать их количество и поддерживать в них порядок. Для контроля связей можно воспользоваться eslint‑plugin‑boundaries или другими линтерами.
Ради избегания кросс‑импортов в методологии часто советуют использовать подходы, усложняющие проект — передача зависимостей через пропсы, использование контекста (Context API) вместо импорта и экспорта, вынесение на слои ниже и так далее. Эти подходы хороши в больших проектах и в средних проектах с большим количеством связей. В маленьких проектах и в проектах с малым количеством связей (особенно, если в них не планируется тестов) они лишь замедлят разработку.
Снижена гибкость из‑за некоторых запрещающих рекомендаций, неподходящих большинству проектов.
К сожалению, из‑за отсутствия документированных альтернатив, в русскоязычном сообществе всё чаще используют FSD, несмотря на довольно серьёзные недостатки. В последнее время стали появляться и описываться более гибкие и более универсальные методологии — Evolution Design, Fractal Entity Oriental Design. К тому же, давно есть модульная структура (везде называется по‑разному: Feature‑based, Feature Folders, Folder‑by‑feature), взяв которую за основу, можно получить гораздо лучшую структуру, чем с использованием FSD. Но, к сожалению, она описана только в общих чертах, без подробностей.
Пора бы frontend сообществу перестать усложнять проекты использованием FSD и вместо этого пробовать другие подходы, дорабатывать и документировать более эффективные методологии.