Fixes #23104 /claim #23104
Reduces dev server start time from 7.8s to 6.9s (11.8% improvement) by implementing a universal dynamic loader for all 108 app-store modules, meeting the <7 second target.
Before (Local Baseline):
✓ Ready in 1498ms
Total: 7.825s
After (Optimized):
✓ Ready in 1169ms
Total: 6.905s
Improvement: 920ms faster (11.8%)
This PR modifies the app-store build generator (packages/app-store-cli/src/build.ts) to generate switch-based dynamic loaders instead of object-of-promises:
// Generated code uses switch statement with runtime lookup
export const getApiHandler = async (slug: string) => {
switch(slug) {
case "alby": return await import("./alby/api");
case "amie": return await import("./amie/api");
// ... 106 more apps
default: throw new Error(`Unknown app: ${slug}`);
}
};
// Backward compatible Proxy
export const apiHandlers = new Proxy({}, {
get: (_, prop) => {
if (typeof prop === 'string') {
return getApiHandler(prop);
}
return undefined;
}
});
Webpack cannot statically analyze which case in the switch will execute (determined by runtime string value), forcing it to create separate chunks for each app. Only the accessed app’s chunk is downloaded/executed.
Previous approach (object of promises):
export const apiHandlers = {
"alby": import("./alby/api"),
"stripe": import("./stripepayment/api"),
// All 108 apps loaded eagerly
};
Webpack sees all imports upfront and bundles them into the initial dev server load.
New approach (switch statement):
switch(slug) {
case "alby": return await import("./alby/api");
case "stripe": return await import("./stripepayment/api");
}
Webpack cannot determine which slug value will be passed at runtime, so it creates separate chunks that load on-demand.
This PR completes the optimization work started by:
# App-store tests
yarn test packages/app-store
✅ 357 tests passed (34 test files)
# Regeneration works
yarn app-store:build
✅ All files regenerated correctly
# Dev server benchmark
yarn dev
✅ Total time: 6.905s (meets <7s target)
All existing code continues to work unchanged:
// Both patterns work identically
await apiHandlers["stripe"] // ✅ Works via Proxy
await getApiHandler("stripe") // ✅ Direct call
The Proxy wrapper ensures complete backward compatibility with existing code that accesses apiHandlers as an object.
packages/app-store-cli/src/build.ts (lines 191-252)
apiHandlers with lazyImport: truegetApiHandler() functionpackages/app-store/apps.server.generated.ts
All other packages/app-store/*.generated.ts files
From original 14.5s baseline reported in #23104:
| Stage | Time | Improvement |
|---|---|---|
| Original baseline | 14.5s | - |
| After PRs #23435 + #23408 | 7.8s | 46% |
| After this PR | 6.9s | 11.8% additional |
| Total improvement | 6.9s | 52% from original |
PR #23468 attempted to use next/dynamic for lazy loading, but Next.js still includes those components in dev bundles for hot reload. Our switch-based approach prevents webpack static analysis entirely, creating true on-demand chunks.
Detailed benchmarks available in the PR branch:
🤖 Generated with Claude Code
Co-Authored-By: Claude noreply@anthropic.com
Speeds up local dev server by switching all 108 app-store modules to a universal switch-based dynamic loader, dropping startup from 7.8s to 6.9s (11.8%) and meeting the <7s goal in #23104.
Refactors
New Features
Michael O'Boyle
@michaeloboyle
Cal.com, Inc.
@cal