mirror of
https://github.com/kennethnym/aris.git
synced 2026-02-02 21:21:21 +00:00
- react-best-practices: Performance optimization patterns (client-side only) - web-design-guidelines: UI review against Web Interface Guidelines Co-authored-by: Ona <no-reply@ona.com>
808 lines
16 KiB
Markdown
808 lines
16 KiB
Markdown
# React Best Practices
|
||
|
||
**Version 1.0.0**
|
||
|
||
> **Note:**
|
||
> This document is for agents and LLMs to follow when maintaining,
|
||
> generating, or refactoring React codebases. Humans may also find it useful.
|
||
|
||
---
|
||
|
||
## Abstract
|
||
|
||
Performance optimization guide for React applications, designed for AI agents and LLMs. Contains rules across 7 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns).
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**
|
||
2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**
|
||
3. [Client-Side Data Fetching](#3-client-side-data-fetching) — **MEDIUM-HIGH**
|
||
4. [Re-render Optimization](#4-re-render-optimization) — **MEDIUM**
|
||
5. [Rendering Performance](#5-rendering-performance) — **MEDIUM**
|
||
6. [JavaScript Performance](#6-javascript-performance) — **LOW-MEDIUM**
|
||
7. [Advanced Patterns](#7-advanced-patterns) — **LOW**
|
||
|
||
---
|
||
|
||
## 1. Eliminating Waterfalls
|
||
|
||
**Impact: CRITICAL**
|
||
|
||
Waterfalls are the #1 performance killer. Each sequential await adds full network latency.
|
||
|
||
### 1.1 Defer Await Until Needed
|
||
|
||
**Impact: HIGH (avoids blocking unused code paths)**
|
||
|
||
Move `await` operations into the branches where they're actually used.
|
||
|
||
**Incorrect: blocks both branches**
|
||
|
||
```typescript
|
||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||
const userData = await fetchUserData(userId)
|
||
|
||
if (skipProcessing) {
|
||
return { skipped: true }
|
||
}
|
||
|
||
return processUserData(userData)
|
||
}
|
||
```
|
||
|
||
**Correct: only blocks when needed**
|
||
|
||
```typescript
|
||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||
if (skipProcessing) {
|
||
return { skipped: true }
|
||
}
|
||
|
||
const userData = await fetchUserData(userId)
|
||
return processUserData(userData)
|
||
}
|
||
```
|
||
|
||
### 1.2 Dependency-Based Parallelization
|
||
|
||
**Impact: CRITICAL (2-10× improvement)**
|
||
|
||
For operations with partial dependencies, use `better-all` to maximize parallelism.
|
||
|
||
**Incorrect: profile waits for config unnecessarily**
|
||
|
||
```typescript
|
||
const [user, config] = await Promise.all([
|
||
fetchUser(),
|
||
fetchConfig()
|
||
])
|
||
const profile = await fetchProfile(user.id)
|
||
```
|
||
|
||
**Correct: config and profile run in parallel**
|
||
|
||
```typescript
|
||
import { all } from 'better-all'
|
||
|
||
const { user, config, profile } = await all({
|
||
async user() { return fetchUser() },
|
||
async config() { return fetchConfig() },
|
||
async profile() {
|
||
return fetchProfile((await this.$.user).id)
|
||
}
|
||
})
|
||
```
|
||
|
||
### 1.3 Promise.all() for Independent Operations
|
||
|
||
**Impact: CRITICAL (2-10× improvement)**
|
||
|
||
When async operations have no interdependencies, execute them concurrently.
|
||
|
||
**Incorrect: sequential execution, 3 round trips**
|
||
|
||
```typescript
|
||
const user = await fetchUser()
|
||
const posts = await fetchPosts()
|
||
const comments = await fetchComments()
|
||
```
|
||
|
||
**Correct: parallel execution, 1 round trip**
|
||
|
||
```typescript
|
||
const [user, posts, comments] = await Promise.all([
|
||
fetchUser(),
|
||
fetchPosts(),
|
||
fetchComments()
|
||
])
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Bundle Size Optimization
|
||
|
||
**Impact: CRITICAL**
|
||
|
||
Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
|
||
|
||
### 2.1 Avoid Barrel File Imports
|
||
|
||
**Impact: CRITICAL (200-800ms import cost, slow builds)**
|
||
|
||
Import directly from source files instead of barrel files.
|
||
|
||
**Incorrect: imports entire library**
|
||
|
||
```tsx
|
||
import { Check, X, Menu } from 'lucide-react'
|
||
// Loads 1,583 modules
|
||
```
|
||
|
||
**Correct: imports only what you need**
|
||
|
||
```tsx
|
||
import Check from 'lucide-react/dist/esm/icons/check'
|
||
import X from 'lucide-react/dist/esm/icons/x'
|
||
import Menu from 'lucide-react/dist/esm/icons/menu'
|
||
// Loads only 3 modules
|
||
```
|
||
|
||
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`.
|
||
|
||
### 2.2 Conditional Module Loading
|
||
|
||
**Impact: HIGH (loads large data only when needed)**
|
||
|
||
Load large data or modules only when a feature is activated.
|
||
|
||
```tsx
|
||
function AnimationPlayer({ enabled, setEnabled }) {
|
||
const [frames, setFrames] = useState(null)
|
||
|
||
useEffect(() => {
|
||
if (enabled && !frames) {
|
||
import('./animation-frames.js')
|
||
.then(mod => setFrames(mod.frames))
|
||
.catch(() => setEnabled(false))
|
||
}
|
||
}, [enabled, frames, setEnabled])
|
||
|
||
if (!frames) return <Skeleton />
|
||
return <Canvas frames={frames} />
|
||
}
|
||
```
|
||
|
||
### 2.3 Preload Based on User Intent
|
||
|
||
**Impact: MEDIUM (reduces perceived latency)**
|
||
|
||
Preload heavy bundles before they're needed.
|
||
|
||
```tsx
|
||
function EditorButton({ onClick }) {
|
||
const preload = () => {
|
||
void import('./monaco-editor')
|
||
}
|
||
|
||
return (
|
||
<button
|
||
onMouseEnter={preload}
|
||
onFocus={preload}
|
||
onClick={onClick}
|
||
>
|
||
Open Editor
|
||
</button>
|
||
)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Client-Side Data Fetching
|
||
|
||
**Impact: MEDIUM-HIGH**
|
||
|
||
### 3.1 Deduplicate Global Event Listeners
|
||
|
||
**Impact: MEDIUM (prevents memory leaks and duplicate handlers)**
|
||
|
||
Use a singleton pattern for global event listeners.
|
||
|
||
**Incorrect: multiple listeners**
|
||
|
||
```tsx
|
||
function useWindowResize(callback) {
|
||
useEffect(() => {
|
||
window.addEventListener('resize', callback)
|
||
return () => window.removeEventListener('resize', callback)
|
||
}, [callback])
|
||
}
|
||
```
|
||
|
||
**Correct: shared listener**
|
||
|
||
```tsx
|
||
const listeners = new Set()
|
||
let isListening = false
|
||
|
||
function useWindowResize(callback) {
|
||
useEffect(() => {
|
||
listeners.add(callback)
|
||
|
||
if (!isListening) {
|
||
isListening = true
|
||
window.addEventListener('resize', (e) => {
|
||
listeners.forEach(fn => fn(e))
|
||
})
|
||
}
|
||
|
||
return () => listeners.delete(callback)
|
||
}, [callback])
|
||
}
|
||
```
|
||
|
||
### 3.2 Use Passive Event Listeners
|
||
|
||
**Impact: MEDIUM (improves scroll performance)**
|
||
|
||
Use passive listeners for scroll and touch events.
|
||
|
||
```tsx
|
||
useEffect(() => {
|
||
const handler = (e) => { /* handle scroll */ }
|
||
window.addEventListener('scroll', handler, { passive: true })
|
||
return () => window.removeEventListener('scroll', handler)
|
||
}, [])
|
||
```
|
||
|
||
### 3.3 Use SWR for Automatic Deduplication
|
||
|
||
**Impact: HIGH (eliminates duplicate requests)**
|
||
|
||
SWR automatically deduplicates requests to the same key.
|
||
|
||
```tsx
|
||
import useSWR from 'swr'
|
||
|
||
function UserProfile({ userId }) {
|
||
const { data } = useSWR(`/api/users/${userId}`, fetcher)
|
||
return <div>{data?.name}</div>
|
||
}
|
||
|
||
// Multiple components using the same key = single request
|
||
```
|
||
|
||
### 3.4 Version and Minimize localStorage Data
|
||
|
||
**Impact: MEDIUM (prevents data corruption)**
|
||
|
||
Use schema versioning for localStorage.
|
||
|
||
```typescript
|
||
const STORAGE_VERSION = 2
|
||
|
||
interface StoredData {
|
||
version: number
|
||
data: UserPreferences
|
||
}
|
||
|
||
function loadPreferences(): UserPreferences {
|
||
const raw = localStorage.getItem('prefs')
|
||
if (!raw) return defaultPreferences
|
||
|
||
const stored: StoredData = JSON.parse(raw)
|
||
if (stored.version !== STORAGE_VERSION) {
|
||
return migrate(stored)
|
||
}
|
||
return stored.data
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Re-render Optimization
|
||
|
||
**Impact: MEDIUM**
|
||
|
||
### 4.1 Defer State Reads to Usage Point
|
||
|
||
**Impact: MEDIUM (avoids unnecessary re-renders)**
|
||
|
||
Don't subscribe to state that's only used in callbacks.
|
||
|
||
**Incorrect: re-renders on every count change**
|
||
|
||
```tsx
|
||
function Counter() {
|
||
const count = useStore(state => state.count)
|
||
const increment = useStore(state => state.increment)
|
||
|
||
return <button onClick={() => increment()}>+</button>
|
||
}
|
||
```
|
||
|
||
**Correct: no re-renders from count changes**
|
||
|
||
```tsx
|
||
function Counter() {
|
||
const increment = useStore(state => state.increment)
|
||
|
||
return <button onClick={() => increment()}>+</button>
|
||
}
|
||
```
|
||
|
||
### 4.2 Extract to Memoized Components
|
||
|
||
**Impact: MEDIUM (isolates expensive renders)**
|
||
|
||
Extract expensive computations into memoized child components.
|
||
|
||
```tsx
|
||
const ExpensiveList = memo(function ExpensiveList({ items }) {
|
||
return items.map(item => <ExpensiveItem key={item.id} item={item} />)
|
||
})
|
||
|
||
function Parent() {
|
||
const [filter, setFilter] = useState('')
|
||
const items = useItems()
|
||
|
||
return (
|
||
<>
|
||
<input value={filter} onChange={e => setFilter(e.target.value)} />
|
||
<ExpensiveList items={items} />
|
||
</>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 4.3 Narrow Effect Dependencies
|
||
|
||
**Impact: MEDIUM (reduces effect runs)**
|
||
|
||
Use primitive dependencies instead of objects.
|
||
|
||
**Incorrect: runs on every render**
|
||
|
||
```tsx
|
||
useEffect(() => {
|
||
fetchData(options)
|
||
}, [options]) // Object reference changes every render
|
||
```
|
||
|
||
**Correct: runs only when values change**
|
||
|
||
```tsx
|
||
useEffect(() => {
|
||
fetchData({ page, limit })
|
||
}, [page, limit])
|
||
```
|
||
|
||
### 4.4 Subscribe to Derived State
|
||
|
||
**Impact: MEDIUM (reduces re-renders)**
|
||
|
||
Subscribe to derived booleans instead of raw values.
|
||
|
||
**Incorrect: re-renders on every count change**
|
||
|
||
```tsx
|
||
function Badge() {
|
||
const count = useStore(state => state.notifications.length)
|
||
return count > 0 ? <span>New</span> : null
|
||
}
|
||
```
|
||
|
||
**Correct: re-renders only when visibility changes**
|
||
|
||
```tsx
|
||
function Badge() {
|
||
const hasNotifications = useStore(state => state.notifications.length > 0)
|
||
return hasNotifications ? <span>New</span> : null
|
||
}
|
||
```
|
||
|
||
### 4.5 Use Functional setState Updates
|
||
|
||
**Impact: MEDIUM (stable callback references)**
|
||
|
||
Use functional updates to avoid dependency on current state.
|
||
|
||
```tsx
|
||
// Incorrect: callback changes when count changes
|
||
const increment = useCallback(() => {
|
||
setCount(count + 1)
|
||
}, [count])
|
||
|
||
// Correct: stable callback reference
|
||
const increment = useCallback(() => {
|
||
setCount(c => c + 1)
|
||
}, [])
|
||
```
|
||
|
||
### 4.6 Use Lazy State Initialization
|
||
|
||
**Impact: LOW-MEDIUM (avoids expensive initial computation)**
|
||
|
||
Pass a function to useState for expensive initial values.
|
||
|
||
```tsx
|
||
// Incorrect: parses on every render
|
||
const [data, setData] = useState(JSON.parse(localStorage.getItem('data')))
|
||
|
||
// Correct: parses only once
|
||
const [data, setData] = useState(() => JSON.parse(localStorage.getItem('data')))
|
||
```
|
||
|
||
### 4.7 Use Transitions for Non-Urgent Updates
|
||
|
||
**Impact: MEDIUM (keeps UI responsive)**
|
||
|
||
Use startTransition for updates that can be deferred.
|
||
|
||
```tsx
|
||
import { startTransition } from 'react'
|
||
|
||
function SearchResults() {
|
||
const [query, setQuery] = useState('')
|
||
const [results, setResults] = useState([])
|
||
|
||
const handleChange = (e) => {
|
||
setQuery(e.target.value) // Urgent: update input immediately
|
||
|
||
startTransition(() => {
|
||
setResults(search(e.target.value)) // Non-urgent: can be interrupted
|
||
})
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<input value={query} onChange={handleChange} />
|
||
<ResultsList results={results} />
|
||
</>
|
||
)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Rendering Performance
|
||
|
||
**Impact: MEDIUM**
|
||
|
||
### 5.1 Animate SVG Wrapper Instead of SVG Element
|
||
|
||
**Impact: MEDIUM (avoids SVG re-parsing)**
|
||
|
||
Wrap SVGs in a div and animate the wrapper.
|
||
|
||
```tsx
|
||
// Incorrect: triggers SVG re-parsing
|
||
<motion.svg animate={{ scale: 1.2 }}>...</motion.svg>
|
||
|
||
// Correct: animates wrapper only
|
||
<motion.div animate={{ scale: 1.2 }}>
|
||
<svg>...</svg>
|
||
</motion.div>
|
||
```
|
||
|
||
### 5.2 CSS content-visibility for Long Lists
|
||
|
||
**Impact: HIGH (skips off-screen rendering)**
|
||
|
||
Use content-visibility to skip rendering off-screen items.
|
||
|
||
```css
|
||
.list-item {
|
||
content-visibility: auto;
|
||
contain-intrinsic-size: 0 50px;
|
||
}
|
||
```
|
||
|
||
### 5.3 Hoist Static JSX Elements
|
||
|
||
**Impact: LOW-MEDIUM (avoids recreation)**
|
||
|
||
Extract static JSX outside components.
|
||
|
||
```tsx
|
||
const staticIcon = <Icon name="check" />
|
||
|
||
function ListItem({ label }) {
|
||
return (
|
||
<div>
|
||
{staticIcon}
|
||
<span>{label}</span>
|
||
</div>
|
||
)
|
||
}
|
||
```
|
||
|
||
### 5.4 Optimize SVG Precision
|
||
|
||
**Impact: LOW (reduces SVG size)**
|
||
|
||
Reduce coordinate precision in SVGs.
|
||
|
||
```tsx
|
||
// Before: 847 bytes
|
||
<path d="M12.7071067811865 5.29289321881345..." />
|
||
|
||
// After: 324 bytes
|
||
<path d="M12.71 5.29..." />
|
||
```
|
||
|
||
### 5.5 Use Explicit Conditional Rendering
|
||
|
||
**Impact: LOW (prevents rendering bugs)**
|
||
|
||
Use ternary instead of && for conditionals.
|
||
|
||
```tsx
|
||
// Incorrect: renders "0" when count is 0
|
||
{count && <Badge count={count} />}
|
||
|
||
// Correct: renders nothing when count is 0
|
||
{count > 0 ? <Badge count={count} /> : null}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. JavaScript Performance
|
||
|
||
**Impact: LOW-MEDIUM**
|
||
|
||
### 6.1 Batch DOM CSS Changes
|
||
|
||
**Impact: MEDIUM (reduces reflows)**
|
||
|
||
Group CSS changes via classes or cssText.
|
||
|
||
```typescript
|
||
// Incorrect: 3 reflows
|
||
element.style.width = '100px'
|
||
element.style.height = '100px'
|
||
element.style.margin = '10px'
|
||
|
||
// Correct: 1 reflow
|
||
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;'
|
||
```
|
||
|
||
### 6.2 Build Index Maps for Repeated Lookups
|
||
|
||
**Impact: HIGH for large datasets**
|
||
|
||
Build a Map for O(1) lookups instead of O(n) array searches.
|
||
|
||
```typescript
|
||
// Incorrect: O(n) for each lookup
|
||
users.find(u => u.id === targetId)
|
||
|
||
// Correct: O(1) lookup
|
||
const userMap = new Map(users.map(u => [u.id, u]))
|
||
userMap.get(targetId)
|
||
```
|
||
|
||
### 6.3 Cache Property Access in Loops
|
||
|
||
**Impact: LOW-MEDIUM**
|
||
|
||
Cache object properties accessed in loops.
|
||
|
||
```typescript
|
||
// Incorrect
|
||
for (let i = 0; i < items.length; i++) {
|
||
process(items[i])
|
||
}
|
||
|
||
// Correct
|
||
const len = items.length
|
||
for (let i = 0; i < len; i++) {
|
||
process(items[i])
|
||
}
|
||
```
|
||
|
||
### 6.4 Cache Repeated Function Calls
|
||
|
||
**Impact: MEDIUM**
|
||
|
||
Cache expensive function results.
|
||
|
||
```typescript
|
||
const cache = new Map()
|
||
|
||
function expensiveComputation(input) {
|
||
if (cache.has(input)) return cache.get(input)
|
||
const result = /* expensive work */
|
||
cache.set(input, result)
|
||
return result
|
||
}
|
||
```
|
||
|
||
### 6.5 Cache Storage API Calls
|
||
|
||
**Impact: MEDIUM**
|
||
|
||
Cache localStorage/sessionStorage reads.
|
||
|
||
```typescript
|
||
let cachedTheme = null
|
||
|
||
function getTheme() {
|
||
if (cachedTheme === null) {
|
||
cachedTheme = localStorage.getItem('theme') || 'light'
|
||
}
|
||
return cachedTheme
|
||
}
|
||
```
|
||
|
||
### 6.6 Combine Multiple Array Iterations
|
||
|
||
**Impact: LOW-MEDIUM**
|
||
|
||
Combine filter/map into a single loop.
|
||
|
||
```typescript
|
||
// Incorrect: 2 iterations
|
||
const result = items
|
||
.filter(item => item.active)
|
||
.map(item => item.value)
|
||
|
||
// Correct: 1 iteration
|
||
const result = []
|
||
for (const item of items) {
|
||
if (item.active) result.push(item.value)
|
||
}
|
||
```
|
||
|
||
### 6.7 Early Length Check for Array Comparisons
|
||
|
||
**Impact: LOW**
|
||
|
||
Check array length before expensive comparisons.
|
||
|
||
```typescript
|
||
function arraysEqual(a, b) {
|
||
if (a.length !== b.length) return false
|
||
return a.every((val, i) => val === b[i])
|
||
}
|
||
```
|
||
|
||
### 6.8 Early Return from Functions
|
||
|
||
**Impact: LOW**
|
||
|
||
Return early to avoid unnecessary work.
|
||
|
||
```typescript
|
||
function processUser(user) {
|
||
if (!user) return null
|
||
if (!user.active) return null
|
||
|
||
// Process active user
|
||
}
|
||
```
|
||
|
||
### 6.9 Hoist RegExp Creation
|
||
|
||
**Impact: LOW-MEDIUM**
|
||
|
||
Create RegExp outside loops.
|
||
|
||
```typescript
|
||
// Incorrect: creates regex on each iteration
|
||
items.forEach(item => {
|
||
if (/pattern/.test(item)) { /* ... */ }
|
||
})
|
||
|
||
// Correct: reuses regex
|
||
const pattern = /pattern/
|
||
items.forEach(item => {
|
||
if (pattern.test(item)) { /* ... */ }
|
||
})
|
||
```
|
||
|
||
### 6.10 Use Loop for Min/Max Instead of Sort
|
||
|
||
**Impact: MEDIUM for large arrays**
|
||
|
||
Use a loop instead of sorting to find min/max.
|
||
|
||
```typescript
|
||
// Incorrect: O(n log n)
|
||
const max = items.sort((a, b) => b - a)[0]
|
||
|
||
// Correct: O(n)
|
||
const max = Math.max(...items)
|
||
// Or for very large arrays:
|
||
let max = items[0]
|
||
for (let i = 1; i < items.length; i++) {
|
||
if (items[i] > max) max = items[i]
|
||
}
|
||
```
|
||
|
||
### 6.11 Use Set/Map for O(1) Lookups
|
||
|
||
**Impact: HIGH for repeated lookups**
|
||
|
||
Use Set for membership checks, Map for key-value lookups.
|
||
|
||
```typescript
|
||
// Incorrect: O(n)
|
||
const isValid = validIds.includes(id)
|
||
|
||
// Correct: O(1)
|
||
const validIdSet = new Set(validIds)
|
||
const isValid = validIdSet.has(id)
|
||
```
|
||
|
||
### 6.12 Use toSorted() for Immutability
|
||
|
||
**Impact: LOW**
|
||
|
||
Use toSorted() instead of sort() to avoid mutation.
|
||
|
||
```typescript
|
||
// Mutates original
|
||
const sorted = items.sort((a, b) => a - b)
|
||
|
||
// Returns new array
|
||
const sorted = items.toSorted((a, b) => a - b)
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Advanced Patterns
|
||
|
||
**Impact: LOW**
|
||
|
||
### 7.1 Store Event Handlers in Refs
|
||
|
||
**Impact: LOW (avoids effect re-runs)**
|
||
|
||
Store handlers in refs to avoid effect dependencies.
|
||
|
||
```tsx
|
||
function useEventListener(event, handler) {
|
||
const handlerRef = useRef(handler)
|
||
handlerRef.current = handler
|
||
|
||
useEffect(() => {
|
||
const listener = (e) => handlerRef.current(e)
|
||
window.addEventListener(event, listener)
|
||
return () => window.removeEventListener(event, listener)
|
||
}, [event]) // handler not in deps
|
||
}
|
||
```
|
||
|
||
### 7.2 useLatest for Stable Callback Refs
|
||
|
||
**Impact: LOW**
|
||
|
||
Create a useLatest hook for stable references.
|
||
|
||
```tsx
|
||
function useLatest(value) {
|
||
const ref = useRef(value)
|
||
ref.current = value
|
||
return ref
|
||
}
|
||
|
||
function useInterval(callback, delay) {
|
||
const callbackRef = useLatest(callback)
|
||
|
||
useEffect(() => {
|
||
const id = setInterval(() => callbackRef.current(), delay)
|
||
return () => clearInterval(id)
|
||
}, [delay])
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## References
|
||
|
||
1. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
||
2. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
|