sdds-infra: Тришейкинг оптимизация #2792
Draft
glebmachine wants to merge 7 commits into
Draft
Conversation
Без `exports` потребитель резолвит `@salutejs/plasma-themes/tokens` и `@salutejs/plasma-themes/tokens/<theme>` напрямую в CJS-копию (`tokens/*.js`), которую Rollup/Vite не tree-shake-ит — в бандл попадают все ~2k токенов независимо от того, сколько потребитель реально использует. Маршрутизирует subpath-ы на уже собираемую ESM-копию (`es/tokens/*`, `es/themes/*`), где каждый токен — отдельный `export var X = 'var(--...)'`, который шейкается до фактически использованных. Маппинг покрывает все известные публичные subpath-ы: `.`, `./tokens`, `./tokens/*`, `./themes`, `./themes/*`, `./css/*`, `./package.json`. Legacy-путь `./es/themes/*` (используется в сторибуках и пакетных mixins по всей монорепе) оставлен в `exports` без `require`-варианта для обратной совместимости — последующая миграция на `./themes/*` рекомендуется, но не блокирует релиз. Эффект на бандл потребителя (Vite): chunk `plasma-themes` 325 KB raw / 28.6 KB gz → 6.7 KB raw / 1.5 KB gz. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Главный barrel `@salutejs/plasma-icons` через `Icons/IconX.tsx` статически импортирует все три ассета (`Icon.assets.16/24/36`) и выбирает один в рантайме по prop `size`. Tree-shaking не может выбросить неиспользуемые размеры — все три считаются «used by name». На typical-приложении с ~80 иконками это даёт ~800 КБ raw / 350 КБ gz лишнего веса (триплицированные SVG-ассеты). Генератор теперь параллельно с `Icons/IconX.tsx` эмитит single-size компоненты `Icons.16/IconX.tsx`, `Icons.24/IconX.tsx`, `Icons.36/IconX.tsx`, каждый из которых импортирует ровно один ассет своего размера. Плюс barrel-файлы `index.16.ts`, `index.24.ts`, `index.36.ts` экспортируют все single-size компоненты + реэкспортируют legacy-иконки из `old/Icons/` (у них размер фиксирован в SVG). Публичный API расширен через `exports`: - `@salutejs/plasma-icons/16` → 16-px вариант всех иконок - `@salutejs/plasma-icons/24` → 24-px (`size="s"` по умолчанию) - `@salutejs/plasma-icons/36` → 36-px Старый импорт `@salutejs/plasma-icons` сохраняется без изменений. Внутренние пути (`./scalable/*`, `./old/*`) намеренно НЕ добавлены в `exports` — это закрывает их от случайного импорта из-вне как часть публичного контракта. Sber-aliased иконки (`IconSberX` → `IconSbX`) в single-size файлах получают `@deprecated` JSDoc со ссылкой на канонический `IconSbX` — миграция работает идентично root barrel-у. Эффект на бандл потребителя при использовании одного размера: ~786 КБ raw → 193 КБ raw (с es2020-фиксом в следующем коммите). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Импорт `Icon<Name>` из `@salutejs/plasma-icons` тянет ассеты для всех трёх размеров (`Icon.assets.16/24/36`) и выбирает один в рантайме — конструкция не поддаётся tree-shaking-у и увеличивает вес иконки в потребительском бандле примерно втрое. Генератор теперь оборачивает каждый ре-экспорт в root barrel-е JSDoc-блок `@deprecated` с конкретной инструкцией миграции на `@salutejs/plasma-icons/16|24|36` для статических размеров. IDE/tsserver покажет strikethrough при использовании из root-а. ESLint `deprecation/deprecation` будет ругаться — для случаев динамического `size` (когда миграция невозможна) предупреждение подавляется локально. Build/runtime не ломается — JSDoc живёт только в `.d.ts`/source и не влияет на эмит. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Общий root `swc.config.json` использует `target: es5` — SWC при
этом инлайнит `_define_property`, `_object_spread`, `_object_without_properties`
и `_object_without_properties_loose` в каждый файл компонента
(~80 строк на каждый из ~1.4k компонентов × 3 размера в новой
схеме single-size). Хелперы пожимаются gzip-ом, но всё равно
оставляют десятки КБ.
Локальный `packages/plasma-icons/.swcrc` повышает `target` до
`es2020` — native `{ ...a }`-спред, опциональные аргументы,
optional-chaining не транспилируются. Это уменьшает каждый
сгенерированный компонент с ~80 строк до ~10.
Плагин `@swc/plugin-styled-components` (`displayName: true`,
`ssr: true`) сохранён из root-конфига — `IconRoot.tsx` собирается
с теми же SSR-стабильными class-name-хешами и debug-метками.
Build-scripts `build:cjs`/`build:esm` теперь указывают на
локальный `./.swcrc` вместо `../../swc.config.json`.
Эффект на бандл потребителя при использовании одного размера:
~467 КБ raw → 193 КБ raw / ~191 КБ gz → 99.9 КБ gz.
Альтернатива была `jsc.externalHelpers: true` + добавление
`@swc/helpers` в dependencies — выбрана модернизация target-а,
т. к. dev-зависимость не добавляется, а IE11/legacy уже не
поддерживается дизайн-системой.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…figs Локальные SWC-конфиги `swc-sc.config.json` и `swc-emotion.config.json` использовали `target: es5`. На каждый собранный модуль это даёт inline-хелперы (`_object_spread`, `_define_property`, `_extends` и т. п.), которые после rollup-агрегации остаются и в финальном бандле потребителя десятками КБ. Переключаем на `target: es2020` — native spread/rest/optional-args сохраняются, бандл сжимается без потери поведения для поддерживаемых браузеров (Chrome ≥ 90 / Firefox ≥ 88 / Safari ≥ 14 / Edge ≥ 90). IE11 дизайн-системой не поддерживается, регрессии нет. Дополнительно добавлен `parser.tsx: true` — главные source-файлы содержат JSX (`*.tsx`), но `parser.syntax: typescript` без флага `tsx` парсит их как обычный TS. На существующих файлах текущая сборка работает за счёт расширения, но флаг делает конфиг консистентным с `swc.config.json` plasma-icons. Эффект на бандл потребителя: plasma-new-hope chunk 312 КБ raw → 302 КБ raw / 110 КБ gz → 108 КБ gz. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
added 2 commits
May 22, 2026 11:31
`PopupRoot.tsx` and `Popup/ui/Resizable/Resizable.tsx` (used by Modal/Sheet/ Notification/Drawer via Popup) plus `_Resizable/Resizable.tsx` (used by Popover) unconditionally imported `react-draggable` / `re-resizable` even when `draggable`/`resizable` props were not set — the most common case. This pulled ~50 KB raw / ~13 KB gz of drag/resize code into the eager Popup bundle for every consumer. Wrap the heavy components in `React.lazy(() => import(...))` + `Suspense` with the non-interactive subtree as fallback, so the chunks load on demand only when drag/resize is actually requested.
…rdRefComponent Commit 05833e4 ("feat(): fix types in Button") added a second function signature intersection that narrows `as=`-polymorphism to the default element: & ((props: PolymorphicComponentPropsWithRef<DefaultElement, OwnProps>) => ReactElement | null) In downstream consumers this breaks valid patterns like `styled(Other)({ as: PlasmaButton })` and `<X as={PlasmaButton}>` — TypeScript collapses the polymorphic call signature to the literal default element ("button") and reports the polymorphic component as not assignable. Revert to the pre-intersection form. The original generic call signature already lets TS instantiate `Button<'a'>`-style usage without the extra constraint.
This was referenced May 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bundle size: tree-shake-friendly subpaths, модернизация SWC target и lazy heavy popup deps
TL;DR
Сокращение «плазма»-чанка в потребительском бандле с 1824 КБ raw / 598 КБ gz до 664 КБ raw / 271 КБ gz (−64% raw, −55% gz) за счёт семи независимых изменений в
plasma-themes,plasma-iconsиplasma-new-hope. Каждое изменение оформлено отдельным коммитом и может мерджиться независимо — все они аддитивны, без breaking changes для существующих импортов.Мотивация
Замер бандла потребителя на Vite (rolldown) с
rollup-plugin-visualizerпоказал, что чанкplasma(eager-загружаемый при первом рендере) тащит:plasma-themes, хотя реально используется ~5% из них;plasma-icons, при том что каждая иконка триплицируется (16/24/36 ассеты) даже когдаsizeизвестен статически;plasma-new-hope, существенная часть которого — инлайн SWC-helper-ы (_object_spread,_define_propertyи т. п.) из-заtarget: es5в общемswc.config.json.Все три источника лишнего веса — следствия конфигурации публикации, а не дизайна компонентов. Фиксы делаются на уровне
package.jsonexports, генератора иконок и SWC-конфигов; компонентный API не трогается.Эффект на бандл
plasma-themes—exports+ ESM subpathplasma-icons— single-size entrypointsplasma-icons— SWC target=es2020plasma-new-hope— SWC target=es2020plasma-new-hope— lazy Draggable/ResizableШаги 6 и 7 (lazy popup deps + revert Polymorphic intersection) — добавлены после первичного review, описание ниже. Шаг 7 — type-only revert, на бандл не влияет, в таблице не отражён.
Замеры сняты на реальном чанке потребителя; каждый пакет публиковался через
npm packи устанавливался какfile:-tarball, чтобы изолировать вклад каждого шага.Содержимое по коммитам
Все семь коммитов независимы — порядок применения не критичен, любой можно cherry-pick-нуть отдельно.
1.
feat(plasma-themes): expose ESM subpaths via exports fieldФайл:
packages/themes/plasma-themes/package.json.Без поля
exportsпотребитель резолвит@salutejs/plasma-themes/tokensи@salutejs/plasma-themes/tokens/<theme>напрямую в CJS-копию (tokens/index.js,tokens/<theme>/index.js), которая по структуре — единый файл с ~2k константами. Rollup/Vite CJS не tree-shake-ят такой формат — в бандл попадает всё.ESM-копия (
es/tokens/*,es/themes/*) уже собирается черезtsconfig.es.jsonи публикуется тем жеnpm pack. Каждый токен в ней —export var X = 'var(--...)', идеальный кандидат для шейкинга.Поле
exportsмаршрутизирует subpath-ы на ESM-вариант. Покрытие:.— корневой entry./tokens,./tokens/*— токены и темы./themes,./themes/*— корневые theme-файлы./es/themes/*— legacy-шим для существующих в монорепе и публичных потребителей импортов вида@salutejs/plasma-themes/es/themes/<theme>(35 файлов внутри репы используют такой путь — сторибуки, mixins,ViewContainer.config.ts). Маппится на тот же ESM-файл, чтобы не сломать существующие сборки. После одной мажорной итерации может быть мигрирован на./themes/*../css/*,./package.json— пассивный passthrough.Эффект:
plasma-themes-chunk потребителя 325 КБ raw / 28.6 КБ gz → 6.7 КБ raw / 1.5 КБ gz.Совместимость: SemVer-минор.
exportsсужает «легальные» подпути до перечисленных, но все известные публичные пути в маппинге. Внутренние пути (src/*) намеренно недоступны — это закрепляет публичный контракт.2.
feat(plasma-icons): add single-size entrypoints /16, /24, /36Файлы:
packages/plasma-icons/package.json,packages/plasma-icons/scripts/utils.ts,packages/plasma-icons/scripts/generateReactComponents.ts.Каждый
IconXвIcons/IconX.tsxсейчас статически импортирует все три ассета:Tree-shaking не может выкинуть ни один размер — все три используются по имени. На приложении с ~80 иконками это ~786 КБ raw / 347 КБ gz триплицированных SVG.
Генератор теперь параллельно с
Icons/IconX.tsxэмитит three single-size-варианта:Icons.16/IconX.tsx,Icons.24/IconX.tsx,Icons.36/IconX.tsx. Каждый тянет ровно один ассет:Плюс три barrel-а
index.16.ts,index.24.ts,index.36.ts— каждый экспортирует все компоненты под своим размером и реэкспортирует legacy-иконки изold/Icons/(их размер фиксирован в SVG).В
package.jsonэто публикуется черезexports:{ "exports": { ".": { ... }, "./16": { "import": "./es/scalable/index.16.js", "require": "./scalable/index.16.js", "types": "./scalable/index.16.d.ts" }, "./24": { ... }, "./36": { ... }, "./css/*": "./css/*", "./package.json": "./package.json" } }Потребитель пишет
import { IconClose } from '@salutejs/plasma-icons/24'если иконка используется вsize="s"(24-px) — tree-shaking уберёт остальные две трети ассетов. Старый импорт@salutejs/plasma-iconsбез подпути продолжает работать без изменений.Sber-aliased имена (
IconSberX→IconSbX) в single-size файлах получают тот же@deprecatedJSDoc, что и в multi-size варианте — миграция через/16|/24|/36работает идентично root barrel-у.Внутренние подпути (
./scalable/*,./old/*) намеренно не открыты черезexports— это защищает структуру пакета от случайного использования как публичного API.Совместимость: SemVer-минор. Чистое добавление новых entry-point-ов. Существующий barrel
@salutejs/plasma-iconsне изменяется.3.
refactor(plasma-icons): mark root barrel exports as @deprecatedФайл:
packages/plasma-icons/scripts/generateReactComponents.ts.Без подсказки потребитель продолжит импортировать из
@salutejs/plasma-iconsи не получит выигрыша от single-size entrypoints. Генератор теперь оборачивает каждый ре-экспорт в root barrel-е JSDoc-блоком@deprecatedс конкретной инструкцией миграции:IDE/tsserver покажет strikethrough при использовании; ESLint-rule
deprecation/deprecationругнётся в lint. Для редких случаев действительно динамическогоsizeпредупреждение глушится локально.Build и runtime не затрагиваются — JSDoc живёт в
.d.ts/source и не попадает в эмит.Совместимость: SemVer-патч. Никакого изменения поведения; только IDE/lint-подсказки.
4.
build(plasma-icons): switch SWC to local config with es2020 targetФайлы:
packages/plasma-icons/.swcrc(новый),packages/plasma-icons/package.json.Общий корневой
swc.config.jsonиспользуетtarget: es5. SWC при этом инлайнит helper-функции (_define_property,_object_spread,_object_without_properties,_object_without_properties_loose) в каждый файл компонента — ~80 строк на каждый из ~1.4k компонентов × 3 размера в новой single-size схеме.Локальный
.swcrcдляplasma-iconsповышаетtargetдоes2020:{ "jsc": { "target": "es2020", "parser": { "syntax": "typescript", "tsx": true }, "transform": { "react": { "runtime": "classic" } }, "experimental": { "plugins": [ ["@swc/plugin-styled-components", { "displayName": true, "ssr": true }] ] } }, "exclude": [".*\\.stories.tsx$", ".*\\.component-test.tsx$"] }Native spread/rest/optional-args не транспилируются — каждый сгенерированный компонент сжимается с ~80 строк до ~10. Плагин
@swc/plugin-styled-components(displayName: true, ssr: true) сохранён из root-конфига, чтобыIconRootсобирался с SSR-стабильными class-name-хешами и debug-метками.build:cjs/build:esmвpackage.jsonпереключены на локальный./.swcrcвместо../../swc.config.json.Альтернатива —
jsc.externalHelpers: true+ добавление@swc/helpersв dependencies — отклонена, чтобы не добавлять runtime-зависимость. Современный target дополнительно соответствует ситуации: дизайн-система не поддерживает IE11/legacy уже несколько лет.Эффект:
plasma-iconsв потребительском бандле (при использовании одного размера через subpath) 467 КБ raw / 191 КБ gz → 193 КБ raw / 99.9 КБ gz.Совместимость: для строгих — major (формально меняется browser-target). На практике для design-system-потребителей — патч/минор, т. к. поддержки IE11 нет давно.
5.
build(plasma-new-hope): bump SWC target to es2020 in sc/emotion configsФайлы:
packages/plasma-new-hope/swc-sc.config.json,packages/plasma-new-hope/swc-emotion.config.json.Аналогично пункту 4, но для
plasma-new-hope. Локальные SWC-конфиги переключены сtarget: es5наtarget: es2020; дополнительно явно выставленparser.tsx: trueдля консистентности (источники содержат JSX, без флага парсер ругался бы на потенциально-неоднозначные конструкции).Эффект:
plasma-new-hope-chunk 312 КБ raw → 302 КБ raw, 110 КБ gz → 108 КБ gz. Скромный, но «бесплатный» выигрыш — побочный эффект модернизации, не требующий поведенческих рисков.Совместимость: аналогично пункту 4.
6.
perf(plasma-new-hope): lazy-load react-draggable/re-resizable in PopupФайлы:
packages/plasma-new-hope/src/components/Popup/PopupRoot.tsx,packages/plasma-new-hope/src/components/Popup/ui/Resizable/Resizable.tsx,packages/plasma-new-hope/src/components/_Resizable/Resizable.tsx.PopupRootбезусловно делалimport Draggable from 'react-draggable', но рендерил его только если потребитель передавалdraggable={true}(line 81 — ранее раннийreturn popupNodeдля не-draggable пути). Аналогично, обе копииResizable(Popup/ui/Resizableи_Resizable) безусловно импортировалиre-resizable, но в run-time использовали его только приresizable && !resizable.disabled. Импорты были hoisted на module-level, поэтому код drag/resize тянулся в чанк Popup-а у каждого потребителя — даже уModal/Sheet/Notification/Drawer/Popover, которые в подавляющем большинстве используются без этих фич.Все три места завёрнуты в
React.lazy(() => import(...))+Suspenseс не-интерактивным поддеревом вfallback:fallbackрендерит то же поддерево без drag/resize-обёртки — для потребителя визуально нет flash или layout-shift; после resolveimport()интерактив включается.Эффект (на чанке потребителя
gigachat-neo):cjs-*(react-draggable ~33 КБ raw / 10 КБ gz) иlib-*(re-resizable ~28 КБ raw / 7 КБ gz). Если потребитель никогда не передаётdraggable/resizable(типичный кейс дляgigachat-neo) — чанки не запрашиваются вовсе.Совместимость: SemVer-минор. Внешний API не меняется (
draggable/resizableprops работают идентично), но React-tree теперь содержитSuspense-границу внутриPopupRoot/Resizable. Это требует React 18+ (плазма уже на этом требовании). Для SSRSuspense fallbackрендерится сразу, без visual diff с прежним поведением (т. к. fallback — то же дерево без обёртки).7.
revert(plasma-new-hope): drop strict intersection in PolymorphicForwardRefComponentФайл:
packages/plasma-new-hope/src/types/Polymorphic.ts.Коммит
05833e4abc(«feat(): fix types in Button») добавил вторую сигнатуру в intersectionPolymorphicForwardRefComponent:Вторая сигнатура сужает тип до конкретного
DefaultElement(например,'button'). В downstream-потребителях это ломает валидные patterns, где плазменные полиморфные компоненты используются как значение пропсовas=:TypeScript при сравнении с
as: ElementTypeколлапсирует полиморфный тип к literal-default-element-у и репортит несоответствие. Та же проблема воспроизводится вstyled(Other)сattrs({ as: Button }).Коммит откатывает intersection к предыдущему виду:
Generic call-signature уже поддерживает TypeScript instantiation expressions вида
Button<'a'>(см. оригинальный комментарий в файле), отдельная default-element-сигнатура не нужна для этого кейса. Если требовался конкретный случай, который покрывала вторая сигнатура, — см. репродукцию в обсуждении PR; сейчас она ломает больше, чем чинит.Эффект: только на типы. Runtime/bundle не затронуты.
Совместимость: SemVer-патч (формально downgrade type-strictness, но фактически возвращение к ранее работавшему контракту, по которому собрались downstream-потребители).
Совместимость и migration plan
plasma-themesexportsplasma-icons/16/24/36plasma-iconsroot@deprecatedplasma-iconsSWC es2020plasma-new-hopeSWC es2020plasma-new-hopelazy Draggable/Resizableplasma-new-hoperevert Polymorphic intersectionНикаких изменений компонентного API. Никаких изменений в сгенерированных
.d.ts-сигнатурах публичных компонентов (кромеPolymorphicForwardRefComponent— там реверт к более liberal-варианту). Все изменения вexports— аддитивные либо сохраняют существующие пути через явный маппинг.