Skip to content

[Feature]: Improve tree-shaking for LiveAriaAnnouncer #35896

@benkeen

Description

@benkeen

Area

React Components (@fluentui/react-components)

Describe the feature that you would like added

We want to use AriaLiveAnnouncer for proper MessageBar a11y, but because we needed to wrap our whole app in the provider, it dramatically increased the size of our initial bundle. This isn't something we could lazy load.

Claude made the following analysis, suggesting a refactor that would improve webpack tree shaking.


AriaLiveAnnouncer causes tabster (~77 KB minified) to be bundled in synchronous entry chunks due to module colocation in @fluentui/react-tabster

Package: @fluentui/react-aria / @fluentui/react-tabster

Severity: Bundle size regression — any app that uses AriaLiveAnnouncer in its synchronous render path will see ~77 KB added to its initial entry bundle.


Description

Importing AriaLiveAnnouncer from @fluentui/react-aria (or @fluentui/react-components) causes the entire tabster library to be included in whichever webpack chunk imports it. The bundle impact is:

  • tabster: ~77 KB minified (~22 KB gzipped)
  • Plus @fluentui/react-tabster utilities: ~5 KB minified

Total: ~82 KB minified / ~22 KB gzipped added to the entry bundle.

tabster was not present in our entry bundle before adopting AriaLiveAnnouncer. We measured this using webpack bundle analysis comparing our app's entry chunk before and after introducing AriaLiveAnnouncer.


Root cause

The dependency chain is:

AriaLiveAnnouncer
  └─ useAriaLiveAnnouncer_unstable  (useAriaLiveAnnouncer.js)
     └─ useDomAnnounce_unstable      (useDomAnnounce.js)
        └─ useDangerousNeverHidden_unstable  (@fluentui/react-tabster)
           └─ [defined in useModalAttributes.js]
              └─ top-level import: import { getModalizer, getRestorer, RestorerTypes } from 'tabster'

The packaging defect is in @fluentui/react-tabster/lib/useModalAttributes.js. This single file exports two unrelated things:

  1. useModalAttributes — which legitimately uses getModalizer, getRestorer from tabster
  2. useDangerousNeverHidden_unstable — which does not use tabster at all; it returns a static object: { 'data-tabster-never-hide': '' }

Because both are in the same module file, webpack must include the entire file when either is imported — including the top-level import { getModalizer, getRestorer, RestorerTypes } from 'tabster'. This makes tabster unconditionally bundled whenever useDangerousNeverHidden_unstable is used, even though it has no runtime dependency on tabster.

Relevant file (from @fluentui/react-tabster):

// useModalAttributes.js  <-- the problematic file
import { getModalizer, getRestorer, RestorerTypes } from 'tabster';  // ← pulls in 77 KB
import * as React from 'react';
// ...

export const useModalAttributes = (...) => { /* uses getModalizer, getRestorer */ };

// useDangerousNeverHidden_unstable is also exported from this same file:
export const useDangerousNeverHidden_unstable = () => {
  return { 'data-tabster-never-hide': '' };  // ← never touches tabster
};

Expected behavior

useDangerousNeverHidden_unstable should be defined in its own module file, separate from useModalAttributes. Since it has no dependency on tabster, it should have no transitive connection to it. Splitting these exports into separate files would allow webpack's tree-shaking to eliminate the tabster import when only useDangerousNeverHidden_unstable is used.


Suggested fix

Move useDangerousNeverHidden_unstable (and any other exports in useModalAttributes.js that don't depend on tabster) into a separate file, e.g. useDangerousNeverHidden.js. This is a non-breaking change and would allow consumers of AriaLiveAnnouncer to avoid pulling in tabster unless they also use modal-related APIs.


Workaround

We worked around this by not using AriaLiveAnnouncer directly. Instead, we use AnnounceProvider from @fluentui/react-shared-contexts (which is just a React.createContext().Provider with no transitive deps) and route FUI9 announcements through our own pre-existing aria-live region. This means FUI9 AnnounceOptions (priority, batching, batchId) are not honoured, which is a functional regression we accepted to avoid the bundle impact.

Additional context

No response

Have you discussed this feature with our team

No response

Validations

  • Check that there isn't already an issue that requests the same feature to avoid creating a duplicate.

Priority

High

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions