Skip to content

🚀 perf(icons): declare the package side-effect free so barrel imports tree-shake - #968

Merged
flaviafmoraes merged 1 commit into
masterfrom
perf/icons-side-effects-free
Sep 11, 2026
Merged

flaviafmoraes merged 1 commit into
masterfrom
perf/icons-side-effects-free

Conversation

@JoaoPCAzevedo

Copy link
Copy Markdown
Contributor

Description 📄

One line: "sideEffects": false on @gympass/yoga-icons. It brings the icons package in line with packages/yoga, which already declares it.

Why it matters. src/index.ts is export * from './ui' / export * from './flags'. Without sideEffects: false, webpack has to assume evaluating any of those re-exported modules might do something observable, so it cannot prune them — and a consumer that names one icon gets all 233 UI icons in the chunk that imports them.

That is not a hypothetical for anyone using @gympass/yoga, because 49 modules inside it import the icon barrel:

Accordion, AutoComplete, Avatar, Card, Checkbox, Datepicker, Dialog, Dropdown, Feedback, Heading, Input, NavigationMenu, Rating, Result, Snackbar

Since the Yoga barrel pulls in every component, importing something as ordinary as ThemeProvider is enough to drag the whole icon set into an app's shared bundle. Every consumer is paying for it.

Measured impact on a Next.js consumer (buzzlightyear, the sign-up app), by patching the installed package, rebuilding, and attributing generated bytes to source modules through the chunk source maps:

Before After
Shared first-load JS 330 kB 314 kB (gzipped)
Shared _app chunk 187 kB 171 kB (gzipped)
Icons inside that chunk 78.7 KB across 233 modules 5.6 KB across 5 modules (raw)

The five that survive are exactly the ones that app's shell renders: alert_triangle, info, close, checked_full, arrow_left.

For scale: that single line saved more in that app than a whole PR of hand-written optimisations we had just finished (which moved 337 kB → 330 kB).

Why the claim is accurate, not just convenient. sideEffects: false is an assertion about this package, so it should only be made if true. For icons it is: they are generated SVG components with no module-scope work and no CSS imports, and src/index.ts only re-exports. Worth remembering that if something side-effectful is ever added here, this needs to become an array of exceptions rather than a blanket false.

Platforms 📲

  • Web
  • Mobile

sideEffects is consumer-side bundler metadata. It changes nothing about this package's build output, and Metro does not consume it, so React Native is unaffected either way.

Type of change 🔍

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested? 🧪

Three checks, because a wrong sideEffects claim silently deletes code rather than failing loudly — and unit tests cannot catch it, since they don't tree-shake.

1. Nothing that is used got dropped. A census of every chunk in the consumer's production build, read from the source maps: 233+ distinct icon modules before, 26 after, and those 26 are precisely the set that app imports (help_circle, visibility, visibility_off, checkin, heartbeat, money, dumbbell, play, price_tag, user, booking, family_member, star, icon_wellhub, mail_unread, information, plus the ones Yoga uses internally). They were not removed — they moved into the page chunks that actually use them, which is the intended outcome.

2. Identical rendering in a real browser. Both builds served and driven through Chromium across five routes. Same SVG count on every one (13 with paths on the company-search screen, 10 on welcome), zero page errors on either. A dropped icon module would surface as a missing SVG or a React "element type is invalid" error; neither appeared.

3. The field reaches the published artifact. publishConfig.directory is dist, so the published package.json is generated rather than taken from source. I ran the createPackageJson() transform from scripts/prepublish.js against the patched source and confirmed sideEffects: false is carried through — it spreads everything except scripts and tsup.

  • Unit Test
  • Snapshot Test

Not applicable: the change is a single package-manifest field with no runtime surface. I did not run the monorepo's own suite locally (it needs a full install and this cannot interact with it), so CI is the check there.

Checklist: 🔍

  • My code follows the contribution guide of this project Contributing Guide
  • Layout matches design prototype: n/a, no visual change
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works — verified as described above rather than with a new test, since the behaviour lives in consumers' bundlers
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • I have checked my code and corrected any misspellings

Screenshots 📸

No visual change — icons render identically. The measurable difference is bundle composition in consumers:

Before After
233 icon modules, 78.7 KB raw, in the consumer's shared chunk 5 icon modules, 5.6 KB raw

…ree-shake

Without "sideEffects": false, webpack cannot prune the re-exports in src/index.ts
(export * from './ui' / './flags'), so any consumer that names a single icon
receives all 233 UI icons in the chunk that imports them.

That is not hypothetical for consumers of @gympass/yoga: 49 modules inside it
import the icon barrel (Accordion, AutoComplete, Avatar, Card, Checkbox,
Datepicker, Dialog, Dropdown, Feedback, Heading, Input, NavigationMenu, Rating,
Result, Snackbar), so importing ThemeProvider is enough to pull the whole set
into an app's shared bundle.

Measured on a Next.js consumer (buzzlightyear) by patching the installed
package, rebuilding, and attributing generated bytes to source modules via the
chunk source maps:

  shared first-load JS   330 kB -> 314 kB (gzipped)
  shared _app chunk      187 kB -> 171 kB (gzipped)
  icons in that chunk    78.7 KB across 233 modules -> 5.6 KB across 5 (raw)

The 5 that remain are the ones the app actually renders. A census of every
chunk in the build shows 26 distinct icon modules afterwards, exactly the set
the app imports, relocated into the page chunks that use them rather than
dropped.

The claim is accurate for this package: the icons are generated SVG components
with no module-scope work and no CSS imports, and src/index.ts only re-exports.
packages/yoga already declares the same field, so this brings icons in line
with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 9, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
5 Security Hotspots
C Reliability Rating on New Code (required ≥ A)
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@flaviafmoraes
flaviafmoraes merged commit 418b75a into master Sep 11, 2026
2 of 3 checks passed
@flaviafmoraes
flaviafmoraes deleted the perf/icons-side-effects-free branch September 11, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants