You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: declare @ionic/core component modules side-effect free, so unused components can be tree-shaken #31610
I have searched for existing issues that already include this feature request, without success.
Describe the Feature Request
@ionic/core's package.json has no sideEffects field, so a bundler keeps any module of the package it cannot prove free of side effects on its own. A component module is such a module: it passes the component's class to a function at its top level, and the published files carry no /*@__PURE__*/ annotation.
@ionic/react imports 91 of the 96 @ionic/core/components/ion-*.js modules in order to wrap them. Its wrappers are annotated /*@__PURE__*/ (91 of 92, see below), and the ones an app never uses are dropped. The component modules behind them are not: a blank app ships 68 of the 91, whether it renders them or not.
A minimal app, IonApp and one IonButton, built with Vite 8.3.4 (Rolldown 1.2.14), @ionic/react 9.0.8 and React 19.3.1:
JavaScript, gzip
@ionic/core component modules in the bundle
as is
301.4 kB
68
component modules marked side-effect free
130.8 kB
9
Both columns are read from the emitted files, not from a bundle analyzer (the concern raised in #25212): the gzipped size of the JavaScript in dist-*/assets/, and the @ionic/core/components modules named in its source maps.
Both builds register the same nine custom elements: ion-app, ion-button, ion-icon, ion-ripple-effect, and five the app does not render (ion-nav, ion-router-outlet, ion-tabs, ion-tab-bar, ion-tab-button). The reproduction's page lists them, from every call to customElements.define, and the button upgrades and renders in both. @ionic/core 9.0.8, the current release, has no sideEffects field, and neither has today's 9.0.9 nightly.
In a production app that renders 38 Ionic components, 37 of the 80 component modules in its bundle were components it never renders. Marking them side-effect free took the first load from 437.8 to 369.3 kB gzipped, and the set of elements the built app registers was the same 52 before and after.
Describe the Use Case
Every Ionic React app built with a bundler that respects sideEffects pays, on its first load, for components it does not use; I would expect the same of Vue, though I have only measured React. In the app above, Ionic was 245.7 kB of a 437.8 kB first load, and an Ionic app is made to be opened on a phone, where the cost shows: on Lighthouse's mobile preset (a local build served gzipped, analytics blocked, three runs each way), its login page's largest contentful paint went from 4.7 s to 4.2–4.4 s after the change.
Describe Preferred Solution
Declare in @ionic/core/package.json that the per-component entry modules have no side effects of their own, while keeping the modules that do have them (CSS, the global setup, the lazy loader under dist/) as they are. The shape would be a sideEffects array that lists what does have side effects; I would leave the exact list to the team, who know the package layout.
As far as I can see, nothing relies on importing a component module for its side effect. A component is registered by calling its defineCustomElement: the React wrappers call it when they are created, and each component's own defineCustomElement registers the components it renders, by name, from an explicit list. That list is why the elements nobody renders by name, such as ion-backdrop, still arrive.
Describe Alternatives
What the app above does today, in its Vite config:
It works, but every app has to discover it for itself, and it asserts on Ionic's behalf something Ionic does not promise. So the app also checks, in the built bundle, that every component its source renders, and every element those components list as their dependencies, is registered. An upgrade that moved a registration where a bundler cannot see it would otherwise ship a dialog without a backdrop, as happened once in #24129.
With webpack, the equivalent is a module.rules entry with sideEffects: false for the same paths.
Related Code
Minimal reproduction: https://github.com/Alexandr-Paleev/ionic-treeshake-repro (npm install, npm run build:default, npm run build:rule, npm run measure; npm run preview:default and npm run preview:rule show what each build registered).
Related: #25212 (all components bundled, closed in 2022), #24313 (side effects removed from the React and Vue packages, 2021), #24129 (popover backdrop missing from production builds, 2021). For Angular, #31303 gave v9 per-component entry points (#30114), so that a page brings only the components it imports. For React the problem comes a step earlier: the modules of components an app never renders are in its bundle in the first place.
One wrapper is created without the annotation: IonNavInner, createReactComponent('ion-nav', …) in @ionic/react 9.0.8. createReactComponent calls the component's defineCustomElement when the wrapper is created, so ion-nav is registered in every app, which is why it appears in both builds above.
If the team agrees with the approach, I am happy to open a pull request.
Activity