Skip to content

Improve performance for cases when React Spectrum is loaded at a runtime #8707

@bhovhannes

Description

@bhovhannes

Provide a general summary of the feature here

Use case

We use microfrontends (MFEs) and multiple teams deploy their front-end pieces independently into the same page. To make everything work, React Spectrum is a shared dependency, external for each MFE. That way we ensure that everyone uses the same version of React Spectrum and that providers and contexts are shared. React Spectrum is not bundled in each MFE.
We use SystemJS and import-maps to be able to load Spectrum at a runtime. Here is how our import map looks like:

{
  "imports": {
    "@adobe/react-spectrum": "https://mfe.static.workfront.com/adobe-react-spectrum/<hash>/@adobe/react-spectrum.js",
    "react-aria": "https://mfe.static.workfront.com/adobe-react-spectrum/<hash>/react-aria.js",
    "react-stately": "https://mfe.static.workfront.com/adobe-react-spectrum/<hash>/react-stately.js",
    "react-aria-components": "https://mfe.static.workfront.com/adobe-react-spectrum/<hash>/react-aria-components.js",
    "@internationalized/": "https://mfe.static.workfront.com/adobe-react-spectrum/<hash>/@internationalized/",
    "@react-aria/": "https://mfe.static.workfront.com/adobe-react-spectrum/<hash>/@react-aria/",
    "@react-spectrum/": "https://mfe.static.workfront.com/adobe-react-spectrum/<hash>/@react-spectrum/",
    "@react-stately/": "https://mfe.static.workfront.com/adobe-react-spectrum/<hash>/@react-stately/"
  }
}

1. How we build Spectrum

We loop through all the components in the monopackages from React Spectrum project and create a Rspack config for each component.
Here is the list of monopackages we scan to create the list of bundles:

  • @adobe/react-spectrum
  • react-aria
  • react-stately

We also add monopackages themselves - @adobe/react-spectrum, react-aria and react-stately.
Additionally, we include packages from @internationalized scope, which are used by React Spectrum components and can be used on their own as well.
And last, we add react-aria-components.

For the each config, every other package from that set is marked as external.

Then, we run Rspack with multiple generated configurations which gives us a SystemJS bundle for each RSP package.

Such approach allows consumers to consume React Spectrum components using both syntaxes mentioned in React Spectrum documentation:

import {Button} from '@adobe/react-spectrum'

and

import {Button} from '@react-spectrum/button'

2. Problem with monopackages

Syntax like import {Button} from '@adobe/react-spectrum' gets transformed to System.import('@adobe/react-spectrum'). At a runtime, SystemJS resolves @adobe/react-spectrum entry from import map, loads JS file, parses it, finds out the list of dependencies, loads them, and so on. As @adobe/react-spectrum re-exports from all Spectrum packages, that downloads whole React Spectrum, even if we need only single button.
The same problem happens with react-aria and react-stately monopackages.

Solving problem for monopackages

To address described problem with monopackages, we have created a babel transform which converts imports from @adobe/react-spectrum to @react-spectrum/<component> syntax automatically. This plugin is being run before deployment in the MFE deployment pipeline, so people don't need to do anything special to use it.

3. Problem with react-aria-components

Let's say someone does:

import {Text} from '@react-spectrum/text'

Everything would be fine, but @react-spectrum/text depends on react-aria-components, which, in its turn, has imports from react-aria and react-stately. As a result, at a runtime we end up downloading whole react-aria-components, react-aria and react-stately. Even worse, we download all that stuff sequentially, because first SystemJS downloads @react-spectrum/text, then discovers it has dependency from react-aria-components, downloads it, discovers it depends on react-aria and react-stately, downloads them, then discovers their dependencies, etc.

🤔 Expected Behavior?

We'd like to see problem described in the 3. Problem with react-aria-components` section solved.

When I use import {Text} from '@react-spectrum/text', I should only get stuff relevant to "Text" component. Currently, use of PURE comments and tree-shaking optimizations in bundlers help with that, but only when you bundle React Spectrum. If you load @react-spectrum/text dynamically, at a runtime, you don't have ways to do tree-shaking, so the code itself should be structured with runtime use-case in mind.

Tell us how the feature should work!
At very least, when I import {Text} from '@react-spectrum/text', I should not download code for ColorPicker. Also, if I use DatePicker, I should not download code for Accordion.

😯 Current Behavior

Current approach does not scale for the case when React Spectrum is being loaded at a runtime and is not bundled. Over time, more and more components get added to the build, which makes our application performance worse with each release.

💁 Possible Solution

The possible approach would be splitting react-aria-components into separate packages. That will allow us to build them separately, and download less at a runtime. The obvious candidate for the new package are contexts. Continuing on @react-spectrum/text example, it needs only HeadingContext and useContextProps from react-aria-components. If we create @react-aria-components/contexts package and move these exports there, it will be a tiny one, and the rest of react-aria-components won't be downloaded when we download @react-spectrum/text.

🔦 Context

described above

💻 Examples

No response

🧢 Your Company/Team

Adobe Workfront, Adobe GenStudio, Adobe Analytics, Adobe Site Optimizer

🕷 Tracking Issue

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions