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:
useModalAttributes — which legitimately uses getModalizer, getRestorer from tabster
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
Priority
High
Area
React Components (@fluentui/react-components)
Describe the feature that you would like added
We want to use
AriaLiveAnnouncerfor 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.
AriaLiveAnnouncercausestabster(~77 KB minified) to be bundled in synchronous entry chunks due to module colocation in@fluentui/react-tabsterPackage:
@fluentui/react-aria/@fluentui/react-tabsterSeverity: Bundle size regression — any app that uses
AriaLiveAnnouncerin its synchronous render path will see ~77 KB added to its initial entry bundle.Description
Importing
AriaLiveAnnouncerfrom@fluentui/react-aria(or@fluentui/react-components) causes the entiretabsterlibrary to be included in whichever webpack chunk imports it. The bundle impact is:tabster: ~77 KB minified (~22 KB gzipped)@fluentui/react-tabsterutilities: ~5 KB minifiedTotal: ~82 KB minified / ~22 KB gzipped added to the entry bundle.
tabsterwas not present in our entry bundle before adoptingAriaLiveAnnouncer. We measured this using webpack bundle analysis comparing our app's entry chunk before and after introducingAriaLiveAnnouncer.Root cause
The dependency chain is:
The packaging defect is in
@fluentui/react-tabster/lib/useModalAttributes.js. This single file exports two unrelated things:useModalAttributes— which legitimately usesgetModalizer,getRestorerfromtabsteruseDangerousNeverHidden_unstable— which does not usetabsterat 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 makestabsterunconditionally bundled wheneveruseDangerousNeverHidden_unstableis used, even though it has no runtime dependency ontabster.Relevant file (from
@fluentui/react-tabster):Expected behavior
useDangerousNeverHidden_unstableshould be defined in its own module file, separate fromuseModalAttributes. Since it has no dependency ontabster, it should have no transitive connection to it. Splitting these exports into separate files would allow webpack's tree-shaking to eliminate thetabsterimport when onlyuseDangerousNeverHidden_unstableis used.Suggested fix
Move
useDangerousNeverHidden_unstable(and any other exports inuseModalAttributes.jsthat don't depend ontabster) into a separate file, e.g.useDangerousNeverHidden.js. This is a non-breaking change and would allow consumers ofAriaLiveAnnouncerto avoid pulling intabsterunless they also use modal-related APIs.Workaround
We worked around this by not using
AriaLiveAnnouncerdirectly. Instead, we useAnnounceProviderfrom@fluentui/react-shared-contexts(which is just aReact.createContext().Providerwith no transitive deps) and route FUI9 announcements through our own pre-existingaria-liveregion. This means FUI9AnnounceOptions(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
Priority
High