mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Hermes-native port of Vercel's official react-best-practices agent skill (vercel-labs/agent-skills, MIT). React/Next.js performance guidance from Vercel Engineering: waterfall elimination, bundle optimization, server-side patterns, re-render hygiene, and more. Core SKILL.md plus 8 on-demand reference files; upstream rule IDs preserved for traceability. Credit: Vercel (vercel-labs).
281 lines
8.6 KiB
Markdown
281 lines
8.6 KiB
Markdown
# Bundle Size Optimization (bundle-*)
|
|
|
|
**Impact: CRITICAL.** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
|
|
|
|
Rules below are from Vercel's `react-best-practices` skill (MIT, vercel-labs/agent-skills). Rule IDs match upstream filenames for traceability.
|
|
|
|
---
|
|
|
|
## `bundle-analyzable-paths` — Prefer Statically Analyzable Paths
|
|
|
|
**Impact: HIGH (avoids accidental broad bundles and file traces)**
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
When analysis becomes too broad, the cost is real:
|
|
- Larger server bundles
|
|
- Slower builds
|
|
- Worse cold starts
|
|
- More memory use
|
|
|
|
### Import Paths
|
|
|
|
**Incorrect (the bundler cannot tell what may be imported):**
|
|
|
|
```ts
|
|
const PAGE_MODULES = {
|
|
home: './pages/home',
|
|
settings: './pages/settings',
|
|
} as const
|
|
|
|
const Page = await import(PAGE_MODULES[pageName])
|
|
```
|
|
|
|
**Correct (use an explicit map of allowed modules):**
|
|
|
|
```ts
|
|
const PAGE_MODULES = {
|
|
home: () => import('./pages/home'),
|
|
settings: () => import('./pages/settings'),
|
|
} as const
|
|
|
|
const Page = await PAGE_MODULES[pageName]()
|
|
```
|
|
|
|
### File-System Paths
|
|
|
|
**Incorrect (a 2-value enum still hides the final path from static analysis):**
|
|
|
|
```ts
|
|
const baseDir = path.join(process.cwd(), 'content/' + contentKind)
|
|
```
|
|
|
|
**Correct (make each final path literal at the callsite):**
|
|
|
|
```ts
|
|
const baseDir =
|
|
kind === ContentKind.Blog
|
|
? path.join(process.cwd(), 'content/blog')
|
|
: path.join(process.cwd(), 'content/docs')
|
|
```
|
|
|
|
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.
|
|
|
|
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/)
|
|
|
|
---
|
|
|
|
## `bundle-barrel-imports` — Avoid Barrel File Imports
|
|
|
|
**Impact: CRITICAL (200-800ms import cost, slow builds)**
|
|
|
|
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
|
|
|
|
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
|
|
|
|
**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
|
|
|
|
**Incorrect (imports entire library):**
|
|
|
|
```tsx
|
|
import { Check, X, Menu } from 'lucide-react'
|
|
// Loads 1,583 modules, takes ~2.8s extra in dev
|
|
// Runtime cost: 200-800ms on every cold start
|
|
|
|
import { Button, TextField } from '@mui/material'
|
|
// Loads 2,225 modules, takes ~4.2s extra in dev
|
|
```
|
|
|
|
**Correct - Next.js 13.5+ (recommended):**
|
|
|
|
```js
|
|
// next.config.js - automatically optimizes barrel imports at build time
|
|
module.exports = {
|
|
experimental: {
|
|
optimizePackageImports: ['lucide-react', '@mui/material']
|
|
}
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
// Keep the standard imports - Next.js transforms them to direct imports
|
|
import { Check, X, Menu } from 'lucide-react'
|
|
// Full TypeScript support, no manual path wrangling
|
|
```
|
|
|
|
This is the recommended approach because it preserves TypeScript type safety and editor autocompletion while still eliminating the barrel import cost.
|
|
|
|
**Correct - Direct imports (non-Next.js projects):**
|
|
|
|
```tsx
|
|
import Button from '@mui/material/Button'
|
|
import TextField from '@mui/material/TextField'
|
|
// Loads only what you use
|
|
```
|
|
|
|
> **TypeScript warning:** Some libraries (notably `lucide-react`) don't ship `.d.ts` files for their deep import paths. Importing from `lucide-react/dist/esm/icons/check` resolves to an implicit `any` type, causing errors under `strict` or `noImplicitAny`. Prefer `optimizePackageImports` when available, or verify the library exports types for its subpaths before using direct imports.
|
|
|
|
These optimizations provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
|
|
|
|
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
|
|
|
|
Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
|
|
|
|
---
|
|
|
|
## `bundle-conditional` — Conditional Module Loading
|
|
|
|
**Impact: HIGH (loads large data only when needed)**
|
|
|
|
Load large data or modules only when a feature is activated.
|
|
|
|
**Example (lazy-load animation frames):**
|
|
|
|
```tsx
|
|
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
|
|
const [frames, setFrames] = useState<Frame[] | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (enabled && !frames && typeof window !== 'undefined') {
|
|
import('./animation-frames.js')
|
|
.then(mod => setFrames(mod.frames))
|
|
.catch(() => setEnabled(false))
|
|
}
|
|
}, [enabled, frames, setEnabled])
|
|
|
|
if (!frames) return <Skeleton />
|
|
return <Canvas frames={frames} />
|
|
}
|
|
```
|
|
|
|
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
|
|
|
|
---
|
|
|
|
## `bundle-defer-third-party` — Defer Non-Critical Third-Party Libraries
|
|
|
|
**Impact: MEDIUM (loads after hydration)**
|
|
|
|
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
|
|
|
|
**Incorrect (blocks initial bundle):**
|
|
|
|
```tsx
|
|
import { Analytics } from '@vercel/analytics/react'
|
|
|
|
export default function RootLayout({ children }) {
|
|
return (
|
|
<html>
|
|
<body>
|
|
{children}
|
|
<Analytics />
|
|
</body>
|
|
</html>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Correct (loads after hydration):**
|
|
|
|
```tsx
|
|
import dynamic from 'next/dynamic'
|
|
|
|
const Analytics = dynamic(
|
|
() => import('@vercel/analytics/react').then(m => m.Analytics),
|
|
{ ssr: false }
|
|
)
|
|
|
|
export default function RootLayout({ children }) {
|
|
return (
|
|
<html>
|
|
<body>
|
|
{children}
|
|
<Analytics />
|
|
</body>
|
|
</html>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## `bundle-dynamic-imports` — Dynamic Imports for Heavy Components
|
|
|
|
**Impact: CRITICAL (directly affects TTI and LCP)**
|
|
|
|
Use `next/dynamic` to lazy-load large components not needed on initial render.
|
|
|
|
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
|
|
|
```tsx
|
|
import { MonacoEditor } from './monaco-editor'
|
|
|
|
function CodePanel({ code }: { code: string }) {
|
|
return <MonacoEditor value={code} />
|
|
}
|
|
```
|
|
|
|
**Correct (Monaco loads on demand):**
|
|
|
|
```tsx
|
|
import dynamic from 'next/dynamic'
|
|
|
|
const MonacoEditor = dynamic(
|
|
() => import('./monaco-editor').then(m => m.MonacoEditor),
|
|
{ ssr: false }
|
|
)
|
|
|
|
function CodePanel({ code }: { code: string }) {
|
|
return <MonacoEditor value={code} />
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## `bundle-preload` — Preload Based on User Intent
|
|
|
|
**Impact: MEDIUM (reduces perceived latency)**
|
|
|
|
Preload heavy bundles before they're needed to reduce perceived latency.
|
|
|
|
**Example (preload on hover/focus):**
|
|
|
|
```tsx
|
|
function EditorButton({ onClick }: { onClick: () => void }) {
|
|
const preload = () => {
|
|
if (typeof window !== 'undefined') {
|
|
void import('./monaco-editor')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<button
|
|
onMouseEnter={preload}
|
|
onFocus={preload}
|
|
onClick={onClick}
|
|
>
|
|
Open Editor
|
|
</button>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Example (preload when feature flag is enabled):**
|
|
|
|
```tsx
|
|
function FlagsProvider({ children, flags }: Props) {
|
|
useEffect(() => {
|
|
if (flags.editorEnabled && typeof window !== 'undefined') {
|
|
void import('./monaco-editor').then(mod => mod.init())
|
|
}
|
|
}, [flags.editorEnabled])
|
|
|
|
return <FlagsContext.Provider value={flags}>
|
|
{children}
|
|
</FlagsContext.Provider>
|
|
}
|
|
```
|
|
|
|
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
|