feat(frontend): code-split paid-tier settings sections (#870)

Every paid-tier React settings section (Users, Webhooks, Security,
Labels, ApiTokens, Registries, CloudBackup, NotificationRouting) was
statically imported into the bundle Community installs download.
SectionGate's runtime tier check hid the components from view but did
not gate the download, so paid feature JSX, copy, error messages, and
prop interfaces shipped to every Community user. Anyone could open
DevTools and read the source.

Convert each paid section to a lazy() declaration that imports the
component module on demand, and remove the corresponding re-exports
from settings/index.ts so rollup actually splits the chunk (without
this, the static export path through the barrel collapses the lazy
import back into the main bundle and emits an INEFFECTIVE_DYNAMIC_
IMPORT warning).

Suspense sits outside SectionGate intentionally: SectionGate short-
circuits to TierLockedCard synchronously for locked tiers, so the
lazy children never mount and no fallback flashes. The skeleton only
appears for the brief window between an unlocked section's chunk
request and its first render.

Build evidence: 8 new chunks total ~89 kB raw / ~27 kB gzip; main
bundle shrunk from 1,537 kB / 421 kB gzip to 1,468 kB / 407 kB gzip.
No INEFFECTIVE_DYNAMIC_IMPORT warnings remain. Dev-server runtime
test: AccountSection (free, eager) loaded as request 221 on app boot;
CloudBackupSection (paid, lazy) loaded as request 351 only after
clicking the sidebar entry.

Non-settings paid views (FleetView, AuditLogView, etc.) are still
static and remain a follow-up.
This commit is contained in:
Anso
2026-05-02 01:50:21 -04:00
committed by GitHub
parent 9ba6b604e3
commit fd05b5ef4b
2 changed files with 67 additions and 18 deletions
+10 -6
View File
@@ -1,18 +1,22 @@
// Free-tier sections are exported eagerly: every operator sees them on every
// install so static imports keep first-paint fast.
export { AccountSection } from './AccountSection';
export { AppearanceSection } from './AppearanceSection';
export { LicenseSection } from './LicenseSection';
export { UsersSection } from './UsersSection';
export { SystemSection } from './SystemSection';
export { NotificationsSection } from './NotificationsSection';
export { WebhooksSection } from './WebhooksSection';
export { SecuritySection } from './SecuritySection';
export { CloudBackupSection } from './CloudBackupSection';
export { DeveloperSection } from './DeveloperSection';
export { AppStoreSection } from './AppStoreSection';
export { SupportSection } from './SupportSection';
export { AboutSection } from './AboutSection';
export { LabelsSection } from './LabelsSection';
export { NotificationRoutingSection } from './NotificationRoutingSection';
// Paid-tier sections (UsersSection, WebhooksSection, SecuritySection,
// LabelsSection, CloudBackupSection, NotificationRoutingSection) are NOT
// re-exported from this barrel. They are dynamically imported with
// React.lazy in SettingsPage.tsx so their JSX, copy, and prop shapes do not
// land in the bundle a Community user downloads. Re-adding any of them as a
// static export here would defeat the split: rollup detects the static path
// and keeps the module in the main chunk regardless of the lazy() call.
export { DEFAULT_SETTINGS } from './types';
export type { PatchableSettings, SectionId, Agent } from './types';
export {