Skip to content

Commit 6938f37

Browse files
authored
Merge pull request #226 from vercel-labs/shu/vPV6R
react-best-practices: Prefer Statically Analyzable Paths
2 parents 73140fc + 805687f commit 6938f37

4 files changed

Lines changed: 169 additions & 3 deletions

File tree

packages/react-best-practices-build/test-cases.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,38 @@
159159
"language": "tsx",
160160
"description": "wrapper shows immediately, data streams in"
161161
},
162+
{
163+
"ruleId": "",
164+
"ruleTitle": "Prefer Statically Analyzable Paths",
165+
"type": "bad",
166+
"code": "const PAGE_MODULES = {\n home: './pages/home',\n settings: './pages/settings',\n} as const\n\nconst Page = await import(PAGE_MODULES[pageName])",
167+
"language": "ts",
168+
"description": "the bundler cannot tell what may be imported"
169+
},
170+
{
171+
"ruleId": "",
172+
"ruleTitle": "Prefer Statically Analyzable Paths",
173+
"type": "good",
174+
"code": "const PAGE_MODULES = {\n home: () => import('./pages/home'),\n settings: () => import('./pages/settings'),\n} as const\n\nconst Page = await PAGE_MODULES[pageName]()",
175+
"language": "ts",
176+
"description": "use an explicit map of allowed modules"
177+
},
178+
{
179+
"ruleId": "",
180+
"ruleTitle": "Prefer Statically Analyzable Paths",
181+
"type": "bad",
182+
"code": "const CONTENT_DIRS = {\n blog: 'content/blog',\n docs: 'content/docs',\n} as const\n\nconst baseDir = path.join(process.cwd(), CONTENT_DIRS[contentKind])",
183+
"language": "ts",
184+
"description": "a 2-value enum still hides the final path from static analysis"
185+
},
186+
{
187+
"ruleId": "",
188+
"ruleTitle": "Prefer Statically Analyzable Paths",
189+
"type": "good",
190+
"code": "const baseDir =\n kind === ContentKind.Blog\n ? path.join(process.cwd(), 'content/blog')\n : path.join(process.cwd(), 'content/docs')",
191+
"language": "ts",
192+
"description": "make each final path literal at the callsite"
193+
},
162194
{
163195
"ruleId": "",
164196
"ruleTitle": "Avoid Barrel File Imports",

skills/react-best-practices/AGENTS.md

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ Comprehensive performance optimization guide for React and Next.js applications,
3232
- 2.2 [Conditional Module Loading](#22-conditional-module-loading)
3333
- 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries)
3434
- 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
35-
- 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
35+
- 2.5 [Prefer Statically Analyzable Paths](#25-prefer-statically-analyzable-paths)
36+
- 2.6 [Preload Based on User Intent](#26-preload-based-on-user-intent)
3637
3. [Server-Side Performance](#3-server-side-performance)**HIGH**
3738
- 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)
3839
- 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)
@@ -578,7 +579,71 @@ function CodePanel({ code }: { code: string }) {
578579
}
579580
```
580581

581-
### 2.5 Preload Based on User Intent
582+
### 2.5 Prefer Statically Analyzable Paths
583+
584+
**Impact: HIGH (avoids accidental broad bundles and file traces)**
585+
586+
Build tools work best when import and file-system paths are obvious at build time. If you hide the real path inside a variable or compose it too dynamically, the tool either has to include a broad set of possible files, warn that it cannot analyze the import, or widen file tracing to stay safe.
587+
588+
Prefer explicit maps or literal paths so the set of reachable files stays narrow and predictable. This is the same rule whether you are choosing modules with `import()` or reading files in server/build code.
589+
590+
When analysis becomes too broad, the cost is real:
591+
592+
- Larger server bundles
593+
594+
- Slower builds
595+
596+
- Worse cold starts
597+
598+
- More memory use
599+
600+
**Incorrect: the bundler cannot tell what may be imported**
601+
602+
```ts
603+
const PAGE_MODULES = {
604+
home: './pages/home',
605+
settings: './pages/settings',
606+
} as const
607+
608+
const Page = await import(PAGE_MODULES[pageName])
609+
```
610+
611+
**Correct: use an explicit map of allowed modules**
612+
613+
```ts
614+
const PAGE_MODULES = {
615+
home: () => import('./pages/home'),
616+
settings: () => import('./pages/settings'),
617+
} as const
618+
619+
const Page = await PAGE_MODULES[pageName]()
620+
```
621+
622+
**Incorrect: a 2-value enum still hides the final path from static analysis**
623+
624+
```ts
625+
const CONTENT_DIRS = {
626+
blog: 'content/blog',
627+
docs: 'content/docs',
628+
} as const
629+
630+
const baseDir = path.join(process.cwd(), CONTENT_DIRS[contentKind])
631+
```
632+
633+
**Correct: make each final path literal at the callsite**
634+
635+
```ts
636+
const baseDir =
637+
kind === ContentKind.Blog
638+
? path.join(process.cwd(), 'content/blog')
639+
: path.join(process.cwd(), 'content/docs')
640+
```
641+
642+
In Next.js server code, this matters for output file tracing too. `path.join(process.cwd(), someVar)` can widen the traced file set because Next.js statically analyze `import`, `require`, and `fs` usage.
643+
644+
Reference: [https://nextjs.org/docs/app/api-reference/config/next-config-js/output](https://nextjs.org/docs/app/api-reference/config/next-config-js/output), [https://nextjs.org/learn/seo/dynamic-imports](https://nextjs.org/learn/seo/dynamic-imports), [https://vite.dev/guide/features.html](https://vite.dev/guide/features.html), [https://esbuild.github.io/api/](https://esbuild.github.io/api/), [https://www.npmjs.com/package/@rollup/plugin-dynamic-import-vars](https://www.npmjs.com/package/@rollup/plugin-dynamic-import-vars), [https://webpack.js.org/guides/dependency-management/](https://webpack.js.org/guides/dependency-management/)
645+
646+
### 2.6 Preload Based on User Intent
582647

583648
**Impact: MEDIUM (reduces perceived latency)**
584649

skills/react-best-practices/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ metadata:
99

1010
# Vercel React Best Practices
1111

12-
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 69 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
12+
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 70 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
1313

1414
## When to Apply
1515

@@ -47,6 +47,7 @@ Reference these guidelines when:
4747
### 2. Bundle Size Optimization (CRITICAL)
4848

4949
- `bundle-barrel-imports` - Import directly, avoid barrel files
50+
- `bundle-analyzable-paths` - Prefer statically analyzable import and file-system paths to avoid broad bundles and traces
5051
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
5152
- `bundle-defer-third-party` - Load analytics/logging after hydration
5253
- `bundle-conditional` - Load modules only when feature is activated
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
title: Prefer Statically Analyzable Paths
3+
impact: HIGH
4+
impactDescription: avoids accidental broad bundles and file traces
5+
tags: bundle, nextjs, vite, webpack, rollup, esbuild, path
6+
---
7+
8+
## Prefer Statically Analyzable Paths
9+
10+
Build tools work best when import and file-system paths are obvious at build time. If you hide the real path inside a variable or compose it too dynamically, the tool either has to include a broad set of possible files, warn that it cannot analyze the import, or widen file tracing to stay safe.
11+
12+
Prefer explicit maps or literal paths so the set of reachable files stays narrow and predictable. This is the same rule whether you are choosing modules with `import()` or reading files in server/build code.
13+
14+
When analysis becomes too broad, the cost is real:
15+
- Larger server bundles
16+
- Slower builds
17+
- Worse cold starts
18+
- More memory use
19+
20+
### Import Paths
21+
22+
**Incorrect (the bundler cannot tell what may be imported):**
23+
24+
```ts
25+
const PAGE_MODULES = {
26+
home: './pages/home',
27+
settings: './pages/settings',
28+
} as const
29+
30+
const Page = await import(PAGE_MODULES[pageName])
31+
```
32+
33+
**Correct (use an explicit map of allowed modules):**
34+
35+
```ts
36+
const PAGE_MODULES = {
37+
home: () => import('./pages/home'),
38+
settings: () => import('./pages/settings'),
39+
} as const
40+
41+
const Page = await PAGE_MODULES[pageName]()
42+
```
43+
44+
### File-System Paths
45+
46+
**Incorrect (a 2-value enum still hides the final path from static analysis):**
47+
48+
```ts
49+
const CONTENT_DIRS = {
50+
blog: 'content/blog',
51+
docs: 'content/docs',
52+
} as const
53+
54+
const baseDir = path.join(process.cwd(), CONTENT_DIRS[contentKind])
55+
```
56+
57+
**Correct (make each final path literal at the callsite):**
58+
59+
```ts
60+
const baseDir =
61+
kind === ContentKind.Blog
62+
? path.join(process.cwd(), 'content/blog')
63+
: path.join(process.cwd(), 'content/docs')
64+
```
65+
66+
In Next.js server code, this matters for output file tracing too. `path.join(process.cwd(), someVar)` can widen the traced file set because Next.js statically analyze `import`, `require`, and `fs` usage.
67+
68+
Reference: [Next.js output](https://nextjs.org/docs/app/api-reference/config/next-config-js/output), [Next.js dynamic imports](https://nextjs.org/learn/seo/dynamic-imports), [Vite features](https://vite.dev/guide/features.html), [esbuild API](https://esbuild.github.io/api/), [Rollup dynamic import vars](https://www.npmjs.com/package/@rollup/plugin-dynamic-import-vars), [Webpack dependency management](https://webpack.js.org/guides/dependency-management/)

0 commit comments

Comments
 (0)