@vetala/vetala 0.1.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @vetala/vetala might be problematic. Click here for more details.
- package/CONTRIBUTING.md +77 -0
- package/LICENSE +184 -0
- package/README.md +136 -0
- package/THIRD_PARTY_LICENSES.md +17 -0
- package/dist/src/agent.d.ts +30 -0
- package/dist/src/agent.js +216 -0
- package/dist/src/agent.js.map +1 -0
- package/dist/src/approvals.d.ts +18 -0
- package/dist/src/approvals.js +81 -0
- package/dist/src/approvals.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +87 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +12 -0
- package/dist/src/config.js +183 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context-memory.d.ts +7 -0
- package/dist/src/context-memory.js +96 -0
- package/dist/src/context-memory.js.map +1 -0
- package/dist/src/ink/command-suggestions.d.ts +7 -0
- package/dist/src/ink/command-suggestions.js +179 -0
- package/dist/src/ink/command-suggestions.js.map +1 -0
- package/dist/src/ink/ink-terminal-ui.d.ts +36 -0
- package/dist/src/ink/ink-terminal-ui.js +79 -0
- package/dist/src/ink/ink-terminal-ui.js.map +1 -0
- package/dist/src/ink/repl-app.d.ts +9 -0
- package/dist/src/ink/repl-app.js +789 -0
- package/dist/src/ink/repl-app.js.map +1 -0
- package/dist/src/ink/transcript-cards.d.ts +6 -0
- package/dist/src/ink/transcript-cards.js +24 -0
- package/dist/src/ink/transcript-cards.js.map +1 -0
- package/dist/src/path-policy.d.ts +11 -0
- package/dist/src/path-policy.js +67 -0
- package/dist/src/path-policy.js.map +1 -0
- package/dist/src/process-utils.d.ts +13 -0
- package/dist/src/process-utils.js +52 -0
- package/dist/src/process-utils.js.map +1 -0
- package/dist/src/repl.d.ts +9 -0
- package/dist/src/repl.js +13 -0
- package/dist/src/repl.js.map +1 -0
- package/dist/src/sarvam/client.d.ts +15 -0
- package/dist/src/sarvam/client.js +208 -0
- package/dist/src/sarvam/client.js.map +1 -0
- package/dist/src/sarvam/models.d.ts +2 -0
- package/dist/src/sarvam/models.js +7 -0
- package/dist/src/sarvam/models.js.map +1 -0
- package/dist/src/search-provider.d.ts +6 -0
- package/dist/src/search-provider.js +8 -0
- package/dist/src/search-provider.js.map +1 -0
- package/dist/src/session-store.d.ts +19 -0
- package/dist/src/session-store.js +318 -0
- package/dist/src/session-store.js.map +1 -0
- package/dist/src/skills/runtime.d.ts +26 -0
- package/dist/src/skills/runtime.js +317 -0
- package/dist/src/skills/runtime.js.map +1 -0
- package/dist/src/skills/types.d.ts +25 -0
- package/dist/src/skills/types.js +2 -0
- package/dist/src/skills/types.js.map +1 -0
- package/dist/src/terminal-ui.d.ts +29 -0
- package/dist/src/terminal-ui.js +236 -0
- package/dist/src/terminal-ui.js.map +1 -0
- package/dist/src/tools/filesystem.d.ts +2 -0
- package/dist/src/tools/filesystem.js +622 -0
- package/dist/src/tools/filesystem.js.map +1 -0
- package/dist/src/tools/git.d.ts +2 -0
- package/dist/src/tools/git.js +326 -0
- package/dist/src/tools/git.js.map +1 -0
- package/dist/src/tools/index.d.ts +6 -0
- package/dist/src/tools/index.js +21 -0
- package/dist/src/tools/index.js.map +1 -0
- package/dist/src/tools/registry.d.ts +15 -0
- package/dist/src/tools/registry.js +59 -0
- package/dist/src/tools/registry.js.map +1 -0
- package/dist/src/tools/shell.d.ts +2 -0
- package/dist/src/tools/shell.js +97 -0
- package/dist/src/tools/shell.js.map +1 -0
- package/dist/src/tools/skill.d.ts +3 -0
- package/dist/src/tools/skill.js +130 -0
- package/dist/src/tools/skill.js.map +1 -0
- package/dist/src/tools/web.d.ts +3 -0
- package/dist/src/tools/web.js +144 -0
- package/dist/src/tools/web.js.map +1 -0
- package/dist/src/types.d.ts +236 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/workspace-trust.d.ts +3 -0
- package/dist/src/workspace-trust.js +31 -0
- package/dist/src/workspace-trust.js.map +1 -0
- package/dist/src/xdg.d.ts +9 -0
- package/dist/src/xdg.js +77 -0
- package/dist/src/xdg.js.map +1 -0
- package/package.json +57 -0
- package/skill/agents-md-generator/SKILL.md +75 -0
- package/skill/agents-md-generator/references/agents_md_template.md +160 -0
- package/skill/agents-md-generator/references/loc_measurement.md +67 -0
- package/skill/agents-md-generator/references/monorepo_detection.md +78 -0
- package/skill/agents-md-generator/references/monorepo_strategy.md +60 -0
- package/skill/agents-md-generator/references/read_only_commands.md +151 -0
- package/skill/agents-md-generator/references/update_strategy.md +160 -0
- package/skill/agents-md-generator/references/working_agreements.md +53 -0
- package/skill/biz-opportunity-scout/SKILL.md +53 -0
- package/skill/biz-opportunity-scout/references/competitive_analysis.md +84 -0
- package/skill/biz-opportunity-scout/references/market_sizing.md +68 -0
- package/skill/biz-opportunity-scout/references/pmf_indicators.md +94 -0
- package/skill/biz-opportunity-scout/references/report_template.md +243 -0
- package/skill/biz-opportunity-scout/references/unit_economics.md +97 -0
- package/skill/code-review/SKILL.md +86 -0
- package/skill/code-review/references/change_analysis.md +116 -0
- package/skill/code-review/references/git_operations.md +115 -0
- package/skill/code-review/references/impact_detection.md +149 -0
- package/skill/code-review/references/output_format.md +137 -0
- package/skill/code-review/references/severity_criteria.md +100 -0
- package/skill/code-security-audit/SKILL.md +123 -0
- package/skill/code-security-audit/references/audit_process.md +277 -0
- package/skill/code-security-audit/references/remediation_patterns.md +599 -0
- package/skill/code-security-audit/references/report_format.md +391 -0
- package/skill/code-security-audit/references/security_domains.md +830 -0
- package/skill/code-security-audit/references/vulnerability_patterns.md +813 -0
- package/skill/composition-patterns/SKILL.md +83 -0
- package/skill/composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
- package/skill/composition-patterns/rules/architecture-compound-components.md +112 -0
- package/skill/composition-patterns/rules/patterns-children-over-render-props.md +87 -0
- package/skill/composition-patterns/rules/patterns-explicit-variants.md +100 -0
- package/skill/composition-patterns/rules/react19-no-forwardref.md +42 -0
- package/skill/composition-patterns/rules/state-context-interface.md +191 -0
- package/skill/composition-patterns/rules/state-decouple-implementation.md +113 -0
- package/skill/composition-patterns/rules/state-lift-state.md +125 -0
- package/skill/deploy-to-vercel/SKILL.md +293 -0
- package/skill/deploy-to-vercel/resources/deploy-sandbox.sh +301 -0
- package/skill/deploy-to-vercel/resources/deploy.sh +301 -0
- package/skill/doc/SKILL_GUIDELINES.md +138 -0
- package/skill/git-workflow/SKILL.md +94 -0
- package/skill/git-workflow/references/advanced-git.md +632 -0
- package/skill/git-workflow/references/branching-strategies.md +344 -0
- package/skill/git-workflow/references/ci-cd-integration.md +683 -0
- package/skill/git-workflow/references/code-quality-tools.md +351 -0
- package/skill/git-workflow/references/commit-conventions.md +439 -0
- package/skill/git-workflow/references/github-releases.md +288 -0
- package/skill/git-workflow/references/pull-request-workflow.md +773 -0
- package/skill/git-workflow/scripts/verify-git-workflow.sh +263 -0
- package/skill/jetbrains-vmoptions/SKILL.md +51 -0
- package/skill/jetbrains-vmoptions/references/common-options.md +357 -0
- package/skill/jetbrains-vmoptions/references/gc-options.md +350 -0
- package/skill/jetbrains-vmoptions/references/memory-options.md +339 -0
- package/skill/jetbrains-vmoptions/references/prerequisite-check.md +65 -0
- package/skill/kysely-converter/SKILL.md +62 -0
- package/skill/kysely-converter/references/delete.md +323 -0
- package/skill/kysely-converter/references/insert.md +386 -0
- package/skill/kysely-converter/references/operators.md +331 -0
- package/skill/kysely-converter/references/select.md +1000 -0
- package/skill/kysely-converter/references/update.md +349 -0
- package/skill/kysely-converter/references/window_function.md +537 -0
- package/skill/react-best-practices/SKILL.md +131 -0
- package/skill/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/skill/react-best-practices/rules/advanced-init-once.md +42 -0
- package/skill/react-best-practices/rules/advanced-use-latest.md +39 -0
- package/skill/react-best-practices/rules/async-api-routes.md +38 -0
- package/skill/react-best-practices/rules/async-defer-await.md +80 -0
- package/skill/react-best-practices/rules/async-dependencies.md +51 -0
- package/skill/react-best-practices/rules/async-parallel.md +28 -0
- package/skill/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/skill/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/skill/react-best-practices/rules/bundle-conditional.md +31 -0
- package/skill/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/skill/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/skill/react-best-practices/rules/bundle-preload.md +50 -0
- package/skill/react-best-practices/rules/client-event-listeners.md +74 -0
- package/skill/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/skill/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/skill/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/skill/react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/skill/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/skill/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/skill/react-best-practices/rules/js-cache-storage.md +70 -0
- package/skill/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/skill/react-best-practices/rules/js-early-exit.md +50 -0
- package/skill/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/skill/react-best-practices/rules/js-index-maps.md +37 -0
- package/skill/react-best-practices/rules/js-length-check-first.md +49 -0
- package/skill/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/skill/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/skill/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/skill/react-best-practices/rules/rendering-activity.md +26 -0
- package/skill/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/skill/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/skill/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/skill/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/skill/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/skill/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/skill/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/skill/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/skill/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/skill/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/skill/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/skill/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/skill/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/skill/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/skill/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/skill/react-best-practices/rules/rerender-memo.md +44 -0
- package/skill/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/skill/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/skill/react-best-practices/rules/rerender-transitions.md +40 -0
- package/skill/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/skill/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/skill/react-best-practices/rules/server-auth-actions.md +96 -0
- package/skill/react-best-practices/rules/server-cache-lru.md +41 -0
- package/skill/react-best-practices/rules/server-cache-react.md +76 -0
- package/skill/react-best-practices/rules/server-dedup-props.md +65 -0
- package/skill/react-best-practices/rules/server-hoist-static-io.md +142 -0
- package/skill/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/skill/react-best-practices/rules/server-serialization.md +38 -0
- package/skill/react-native-skills/SKILL.md +115 -0
- package/skill/react-native-skills/rules/animation-derived-value.md +53 -0
- package/skill/react-native-skills/rules/animation-gesture-detector-press.md +95 -0
- package/skill/react-native-skills/rules/animation-gpu-properties.md +65 -0
- package/skill/react-native-skills/rules/design-system-compound-components.md +66 -0
- package/skill/react-native-skills/rules/fonts-config-plugin.md +71 -0
- package/skill/react-native-skills/rules/imports-design-system-folder.md +68 -0
- package/skill/react-native-skills/rules/js-hoist-intl.md +61 -0
- package/skill/react-native-skills/rules/list-performance-callbacks.md +44 -0
- package/skill/react-native-skills/rules/list-performance-function-references.md +132 -0
- package/skill/react-native-skills/rules/list-performance-images.md +53 -0
- package/skill/react-native-skills/rules/list-performance-inline-objects.md +97 -0
- package/skill/react-native-skills/rules/list-performance-item-expensive.md +94 -0
- package/skill/react-native-skills/rules/list-performance-item-memo.md +82 -0
- package/skill/react-native-skills/rules/list-performance-item-types.md +104 -0
- package/skill/react-native-skills/rules/list-performance-virtualize.md +67 -0
- package/skill/react-native-skills/rules/monorepo-native-deps-in-app.md +46 -0
- package/skill/react-native-skills/rules/monorepo-single-dependency-versions.md +63 -0
- package/skill/react-native-skills/rules/navigation-native-navigators.md +188 -0
- package/skill/react-native-skills/rules/react-compiler-destructure-functions.md +50 -0
- package/skill/react-native-skills/rules/react-compiler-reanimated-shared-values.md +48 -0
- package/skill/react-native-skills/rules/react-state-dispatcher.md +91 -0
- package/skill/react-native-skills/rules/react-state-fallback.md +56 -0
- package/skill/react-native-skills/rules/react-state-minimize.md +65 -0
- package/skill/react-native-skills/rules/rendering-no-falsy-and.md +74 -0
- package/skill/react-native-skills/rules/rendering-text-in-text-component.md +36 -0
- package/skill/react-native-skills/rules/scroll-position-no-state.md +82 -0
- package/skill/react-native-skills/rules/state-ground-truth.md +80 -0
- package/skill/react-native-skills/rules/ui-expo-image.md +66 -0
- package/skill/react-native-skills/rules/ui-image-gallery.md +104 -0
- package/skill/react-native-skills/rules/ui-measure-views.md +78 -0
- package/skill/react-native-skills/rules/ui-menus.md +174 -0
- package/skill/react-native-skills/rules/ui-native-modals.md +77 -0
- package/skill/react-native-skills/rules/ui-pressable.md +61 -0
- package/skill/react-native-skills/rules/ui-safe-area-scroll.md +65 -0
- package/skill/react-native-skills/rules/ui-scrollview-content-inset.md +45 -0
- package/skill/react-native-skills/rules/ui-styling.md +87 -0
- package/skill/react-vite-guide/SKILL.md +101 -0
- package/skill/react-vite-guide/references/composition-patterns.md +709 -0
- package/skill/react-vite-guide/references/performance-optimization.md +1222 -0
- package/skill/react-vite-guide/references/vite-specific.md +385 -0
- package/skill/react-vite-guide/references/web-interface.md +146 -0
- package/skill/skill-maker/SKILL.md +52 -0
- package/skill/skill-maker/references/content_spec.md +67 -0
- package/skill/skill-maker/references/frontmatter_spec.md +96 -0
- package/skill/skill-maker/references/input_validation.md +90 -0
- package/skill/skill-maker/references/skill_structure.md +74 -0
- package/skill/system-prompt-creator/SKILL.md +50 -0
- package/skill/system-prompt-creator/references/data_format_selection.md +135 -0
- package/skill/system-prompt-creator/references/multi_prompt_architecture.md +386 -0
- package/skill/system-prompt-creator/references/prompt_structure.md +140 -0
- package/skill/system-prompt-creator/references/quality_criteria.md +83 -0
- package/skill/typst-creator/SKILL.md +51 -0
- package/skill/typst-creator/references/layout.md +401 -0
- package/skill/typst-creator/references/math.md +297 -0
- package/skill/typst-creator/references/scripting.md +237 -0
- package/skill/typst-creator/references/styling.md +217 -0
- package/skill/typst-creator/references/syntax.md +234 -0
- package/skill/web-design-guidelines/SKILL.md +35 -0
- package/terminal.png +0 -0
|
@@ -0,0 +1,1222 @@
|
|
|
1
|
+
# Performance Optimization
|
|
2
|
+
|
|
3
|
+
Performance optimization patterns for React 19 + Vite SPA applications. Adapted from broader React performance guidance with Next.js-specific patterns removed and Vite equivalents provided.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Async Optimization
|
|
8
|
+
|
|
9
|
+
**Impact: CRITICAL**
|
|
10
|
+
|
|
11
|
+
Waterfalls are the #1 performance killer. Each sequential await adds full network latency.
|
|
12
|
+
|
|
13
|
+
### 1.1 Defer Await Until Needed
|
|
14
|
+
|
|
15
|
+
**Impact: HIGH**
|
|
16
|
+
|
|
17
|
+
Deferred `await` places the keyword in the branch where the value is consumed, avoiding blocking on unused code paths.
|
|
18
|
+
|
|
19
|
+
**Incorrect: blocks both branches**
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
async function handleRequest(userId: string, skipProcessing: boolean) {
|
|
23
|
+
const userData = await fetchUserData(userId)
|
|
24
|
+
|
|
25
|
+
if (skipProcessing) {
|
|
26
|
+
return { skipped: true }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return processUserData(userData)
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Correct: only blocks when needed**
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
async function handleRequest(userId: string, skipProcessing: boolean) {
|
|
37
|
+
if (skipProcessing) {
|
|
38
|
+
return { skipped: true }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const userData = await fetchUserData(userId)
|
|
42
|
+
return processUserData(userData)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 1.2 Promise.all() for Independent Operations
|
|
47
|
+
|
|
48
|
+
**Impact: CRITICAL (2-10× improvement)**
|
|
49
|
+
|
|
50
|
+
Independent async operations support concurrent execution via `Promise.all()`.
|
|
51
|
+
|
|
52
|
+
**Incorrect: sequential execution, 3 round trips**
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const user = await fetchUser()
|
|
56
|
+
const posts = await fetchPosts()
|
|
57
|
+
const comments = await fetchComments()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Correct: parallel execution, 1 round trip**
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
const [user, posts, comments] = await Promise.all([
|
|
64
|
+
fetchUser(),
|
|
65
|
+
fetchPosts(),
|
|
66
|
+
fetchComments()
|
|
67
|
+
])
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 1.3 Dependency-Based Parallelization
|
|
71
|
+
|
|
72
|
+
**Impact: CRITICAL (2-10× improvement)**
|
|
73
|
+
|
|
74
|
+
Operations with partial dependencies support starting independent work immediately while dependent chains resolve.
|
|
75
|
+
|
|
76
|
+
**Incorrect: profile waits for config unnecessarily**
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
const [user, config] = await Promise.all([
|
|
80
|
+
fetchUser(),
|
|
81
|
+
fetchConfig()
|
|
82
|
+
])
|
|
83
|
+
const profile = await fetchProfile(user.id)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Correct: config and profile run in parallel**
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const userPromise = fetchUser()
|
|
90
|
+
const profilePromise = userPromise.then(user => fetchProfile(user.id))
|
|
91
|
+
|
|
92
|
+
const [user, config, profile] = await Promise.all([
|
|
93
|
+
userPromise,
|
|
94
|
+
fetchConfig(),
|
|
95
|
+
profilePromise
|
|
96
|
+
])
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 1.4 Strategic Suspense Boundaries
|
|
100
|
+
|
|
101
|
+
**Impact: HIGH (faster initial paint)**
|
|
102
|
+
|
|
103
|
+
Suspense boundaries show wrapper UI while data loads. In Vite SPA, `use()` unwraps promises inside Suspense.
|
|
104
|
+
|
|
105
|
+
**Incorrect: blocks entire page**
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
function Page() {
|
|
109
|
+
const [data, setData] = useState(null)
|
|
110
|
+
const [loading, setLoading] = useState(true)
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
fetchData().then(d => {
|
|
114
|
+
setData(d)
|
|
115
|
+
setLoading(false)
|
|
116
|
+
})
|
|
117
|
+
}, [])
|
|
118
|
+
|
|
119
|
+
if (loading) return <Skeleton />
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div>
|
|
123
|
+
<Sidebar />
|
|
124
|
+
<Header />
|
|
125
|
+
<DataDisplay data={data} />
|
|
126
|
+
<Footer />
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Correct: layout shows immediately, data streams in**
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
function Page() {
|
|
136
|
+
const dataPromise = useMemo(() => fetchData(), [])
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div>
|
|
140
|
+
<Sidebar />
|
|
141
|
+
<Header />
|
|
142
|
+
<Suspense fallback={<Skeleton />}>
|
|
143
|
+
<DataDisplay dataPromise={dataPromise} />
|
|
144
|
+
</Suspense>
|
|
145
|
+
<Footer />
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
|
151
|
+
const data = use(dataPromise)
|
|
152
|
+
return <div>{data.content}</div>
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 2. Bundle Size Optimization
|
|
161
|
+
|
|
162
|
+
**Impact: CRITICAL**
|
|
163
|
+
|
|
164
|
+
Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
|
|
165
|
+
|
|
166
|
+
### 2.1 Barrel File Import Cost
|
|
167
|
+
|
|
168
|
+
**Impact: CRITICAL (200-800ms import cost)**
|
|
169
|
+
|
|
170
|
+
Barrel file imports pull in the entire module tree. Direct source file imports load only what is referenced.
|
|
171
|
+
|
|
172
|
+
**Incorrect: imports entire library**
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { Check, X, Menu } from 'lucide-react'
|
|
176
|
+
// Loads 1,583 modules
|
|
177
|
+
|
|
178
|
+
import { Button, TextField } from '@mui/material'
|
|
179
|
+
// Loads 2,225 modules
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Correct: imports only what you need**
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import Check from 'lucide-react/dist/esm/icons/check'
|
|
186
|
+
import X from 'lucide-react/dist/esm/icons/x'
|
|
187
|
+
import Menu from 'lucide-react/dist/esm/icons/menu'
|
|
188
|
+
|
|
189
|
+
import Button from '@mui/material/Button'
|
|
190
|
+
import TextField from '@mui/material/TextField'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@radix-ui/react-*`, `lodash`, `date-fns`, `rxjs`.
|
|
194
|
+
|
|
195
|
+
### 2.2 Dynamic Imports with React.lazy()
|
|
196
|
+
|
|
197
|
+
**Impact: CRITICAL (directly affects TTI and LCP)**
|
|
198
|
+
|
|
199
|
+
`React.lazy()` + `Suspense` lazy-loads large components not needed on initial render.
|
|
200
|
+
|
|
201
|
+
**Incorrect: Monaco bundles with main chunk ~300KB**
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
import { MonacoEditor } from './monaco-editor'
|
|
205
|
+
|
|
206
|
+
function CodePanel({ code }: { code: string }) {
|
|
207
|
+
return <MonacoEditor value={code} />
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Correct: Monaco loads on demand**
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
import { lazy, Suspense } from 'react'
|
|
215
|
+
|
|
216
|
+
const MonacoEditor = lazy(() =>
|
|
217
|
+
import('./monaco-editor').then(m => ({ default: m.MonacoEditor }))
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
function CodePanel({ code }: { code: string }) {
|
|
221
|
+
return (
|
|
222
|
+
<Suspense fallback={<div>Loading editor…</div>}>
|
|
223
|
+
<MonacoEditor value={code} />
|
|
224
|
+
</Suspense>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 2.3 Conditional Module Loading
|
|
230
|
+
|
|
231
|
+
**Impact: HIGH**
|
|
232
|
+
|
|
233
|
+
Load large data or modules only when a feature is activated.
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
function AnimationPlayer({ enabled, setEnabled }: Props) {
|
|
237
|
+
const [frames, setFrames] = useState<Frame[] | null>(null)
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (enabled && !frames) {
|
|
241
|
+
import('./animation-frames.js')
|
|
242
|
+
.then(mod => setFrames(mod.frames))
|
|
243
|
+
.catch(() => setEnabled(false))
|
|
244
|
+
}
|
|
245
|
+
}, [enabled, frames, setEnabled])
|
|
246
|
+
|
|
247
|
+
if (!frames) return <Skeleton />
|
|
248
|
+
return <Canvas frames={frames} />
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 2.4 Defer Non-Critical Third-Party Libraries
|
|
253
|
+
|
|
254
|
+
**Impact: MEDIUM**
|
|
255
|
+
|
|
256
|
+
Analytics, logging, and error tracking are non-blocking for user interaction and support lazy loading.
|
|
257
|
+
|
|
258
|
+
**Incorrect: blocks initial bundle**
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
import { init as initAnalytics } from '@analytics/core'
|
|
262
|
+
|
|
263
|
+
function App() {
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
initAnalytics({ /* config */ })
|
|
266
|
+
}, [])
|
|
267
|
+
|
|
268
|
+
return <Router />
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Correct: loads after initial render**
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
function App() {
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
import('@analytics/core').then(({ init }) => {
|
|
278
|
+
init({ /* config */ })
|
|
279
|
+
})
|
|
280
|
+
}, [])
|
|
281
|
+
|
|
282
|
+
return <Router />
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### 2.5 Preload Based on User Intent
|
|
287
|
+
|
|
288
|
+
**Impact: MEDIUM**
|
|
289
|
+
|
|
290
|
+
Hover/focus events on triggers preload heavy bundles before navigation occurs.
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
function EditorButton({ onClick }: { onClick: () => void }) {
|
|
294
|
+
const preload = () => {
|
|
295
|
+
void import('./monaco-editor')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<button
|
|
300
|
+
onMouseEnter={preload}
|
|
301
|
+
onFocus={preload}
|
|
302
|
+
onClick={onClick}
|
|
303
|
+
>
|
|
304
|
+
Open Editor
|
|
305
|
+
</button>
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## 3. Client-Side Data Fetching
|
|
313
|
+
|
|
314
|
+
**Impact: MEDIUM-HIGH**
|
|
315
|
+
|
|
316
|
+
### 3.1 Use SWR for Automatic Deduplication
|
|
317
|
+
|
|
318
|
+
**Impact: MEDIUM-HIGH**
|
|
319
|
+
|
|
320
|
+
SWR enables request deduplication, caching, and revalidation across component instances.
|
|
321
|
+
|
|
322
|
+
**Incorrect: no deduplication, each instance fetches**
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
function UserList() {
|
|
326
|
+
const [users, setUsers] = useState([])
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
fetch('/api/users')
|
|
329
|
+
.then(r => r.json())
|
|
330
|
+
.then(setUsers)
|
|
331
|
+
}, [])
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Correct: multiple instances share one request**
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
import useSWR from 'swr'
|
|
339
|
+
|
|
340
|
+
function UserList() {
|
|
341
|
+
const { data: users } = useSWR('/api/users', fetcher)
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**For mutations:**
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
import useSWRMutation from 'swr/mutation'
|
|
349
|
+
|
|
350
|
+
function UpdateButton() {
|
|
351
|
+
const { trigger } = useSWRMutation('/api/user', updateUser)
|
|
352
|
+
return <button onClick={() => trigger()}>Update</button>
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### 3.2 Deduplicate Global Event Listeners
|
|
357
|
+
|
|
358
|
+
**Impact: LOW**
|
|
359
|
+
|
|
360
|
+
`useSWRSubscription()` shares global event listeners across component instances, reducing N listeners to 1.
|
|
361
|
+
|
|
362
|
+
**Incorrect: N instances = N listeners**
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
function useKeyboardShortcut(key: string, callback: () => void) {
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
const handler = (e: KeyboardEvent) => {
|
|
368
|
+
if (e.metaKey && e.key === key) {
|
|
369
|
+
callback()
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
window.addEventListener('keydown', handler)
|
|
373
|
+
return () => window.removeEventListener('keydown', handler)
|
|
374
|
+
}, [key, callback])
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Correct: N instances = 1 listener**
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
import useSWRSubscription from 'swr/subscription'
|
|
382
|
+
|
|
383
|
+
const keyCallbacks = new Map<string, Set<() => void>>()
|
|
384
|
+
|
|
385
|
+
function useKeyboardShortcut(key: string, callback: () => void) {
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
if (!keyCallbacks.has(key)) {
|
|
388
|
+
keyCallbacks.set(key, new Set())
|
|
389
|
+
}
|
|
390
|
+
keyCallbacks.get(key)!.add(callback)
|
|
391
|
+
|
|
392
|
+
return () => {
|
|
393
|
+
const set = keyCallbacks.get(key)
|
|
394
|
+
if (set) {
|
|
395
|
+
set.delete(callback)
|
|
396
|
+
if (set.size === 0) keyCallbacks.delete(key)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}, [key, callback])
|
|
400
|
+
|
|
401
|
+
useSWRSubscription('global-keydown', () => {
|
|
402
|
+
const handler = (e: KeyboardEvent) => {
|
|
403
|
+
if (e.metaKey && keyCallbacks.has(e.key)) {
|
|
404
|
+
keyCallbacks.get(e.key)!.forEach(cb => cb())
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
window.addEventListener('keydown', handler)
|
|
408
|
+
return () => window.removeEventListener('keydown', handler)
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### 3.3 Passive Event Listeners for Scrolling
|
|
414
|
+
|
|
415
|
+
**Impact: MEDIUM**
|
|
416
|
+
|
|
417
|
+
`{ passive: true }` on touch and wheel event listeners signals the browser that `preventDefault()` is not called, enabling scroll optimization.
|
|
418
|
+
|
|
419
|
+
**Incorrect:**
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
document.addEventListener('touchstart', handleTouch)
|
|
423
|
+
document.addEventListener('wheel', handleWheel)
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Correct:**
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
document.addEventListener('touchstart', handleTouch, { passive: true })
|
|
430
|
+
document.addEventListener('wheel', handleWheel, { passive: true })
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Use passive when: tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
|
|
434
|
+
|
|
435
|
+
### 3.4 Version and Minimize localStorage Data
|
|
436
|
+
|
|
437
|
+
**Impact: MEDIUM**
|
|
438
|
+
|
|
439
|
+
Add version prefix to keys and store only needed fields.
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
const VERSION = 'v2'
|
|
443
|
+
|
|
444
|
+
function saveConfig(config: { theme: string; language: string }) {
|
|
445
|
+
try {
|
|
446
|
+
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
|
|
447
|
+
} catch {
|
|
448
|
+
// Throws in incognito, quota exceeded, or disabled
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function loadConfig() {
|
|
453
|
+
try {
|
|
454
|
+
const data = localStorage.getItem(`userConfig:${VERSION}`)
|
|
455
|
+
return data ? JSON.parse(data) : null
|
|
456
|
+
} catch {
|
|
457
|
+
return null
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Always wrap in try-catch. `getItem()` and `setItem()` throw in incognito/private browsing.
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## 4. Re-render Optimization
|
|
467
|
+
|
|
468
|
+
**Impact: MEDIUM**
|
|
469
|
+
|
|
470
|
+
### 4.1 Calculate Derived State During Rendering
|
|
471
|
+
|
|
472
|
+
**Impact: MEDIUM**
|
|
473
|
+
|
|
474
|
+
Values computable from current props/state are derived during render, eliminating redundant state and effects.
|
|
475
|
+
|
|
476
|
+
**Incorrect: redundant state and effect**
|
|
477
|
+
|
|
478
|
+
```tsx
|
|
479
|
+
function Form() {
|
|
480
|
+
const [firstName, setFirstName] = useState('First')
|
|
481
|
+
const [lastName, setLastName] = useState('Last')
|
|
482
|
+
const [fullName, setFullName] = useState('')
|
|
483
|
+
|
|
484
|
+
useEffect(() => {
|
|
485
|
+
setFullName(firstName + ' ' + lastName)
|
|
486
|
+
}, [firstName, lastName])
|
|
487
|
+
|
|
488
|
+
return <p>{fullName}</p>
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Correct: derive during render**
|
|
493
|
+
|
|
494
|
+
```tsx
|
|
495
|
+
function Form() {
|
|
496
|
+
const [firstName, setFirstName] = useState('First')
|
|
497
|
+
const [lastName, setLastName] = useState('Last')
|
|
498
|
+
const fullName = firstName + ' ' + lastName
|
|
499
|
+
|
|
500
|
+
return <p>{fullName}</p>
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### 4.2 Defer State Reads to Usage Point
|
|
505
|
+
|
|
506
|
+
**Impact: MEDIUM**
|
|
507
|
+
|
|
508
|
+
Reading dynamic state only inside callbacks avoids subscribing to changes that do not affect render output.
|
|
509
|
+
|
|
510
|
+
**Incorrect: subscribes to all URL changes**
|
|
511
|
+
|
|
512
|
+
```tsx
|
|
513
|
+
function ShareButton({ chatId }: { chatId: string }) {
|
|
514
|
+
const [searchParams] = useSearchParams()
|
|
515
|
+
|
|
516
|
+
const handleShare = () => {
|
|
517
|
+
const ref = searchParams.get('ref')
|
|
518
|
+
shareChat(chatId, { ref })
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return <button onClick={handleShare}>Share</button>
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Correct: reads on demand**
|
|
526
|
+
|
|
527
|
+
```tsx
|
|
528
|
+
function ShareButton({ chatId }: { chatId: string }) {
|
|
529
|
+
const handleShare = () => {
|
|
530
|
+
const params = new URLSearchParams(window.location.search)
|
|
531
|
+
const ref = params.get('ref')
|
|
532
|
+
shareChat(chatId, { ref })
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return <button onClick={handleShare}>Share</button>
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### 4.3 Simple Expressions vs useMemo
|
|
540
|
+
|
|
541
|
+
**Impact: LOW-MEDIUM**
|
|
542
|
+
|
|
543
|
+
For simple expressions with primitive results, `useMemo` overhead exceeds the computation cost.
|
|
544
|
+
|
|
545
|
+
**Incorrect:**
|
|
546
|
+
|
|
547
|
+
```tsx
|
|
548
|
+
const isLoading = useMemo(() => {
|
|
549
|
+
return user.isLoading || notifications.isLoading
|
|
550
|
+
}, [user.isLoading, notifications.isLoading])
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Correct:**
|
|
554
|
+
|
|
555
|
+
```tsx
|
|
556
|
+
const isLoading = user.isLoading || notifications.isLoading
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### 4.4 Extract Default Non-primitive Values from Memoized Components
|
|
560
|
+
|
|
561
|
+
**Impact: MEDIUM**
|
|
562
|
+
|
|
563
|
+
Default non-primitive parameter values create new instances on every render, breaking `memo()`.
|
|
564
|
+
|
|
565
|
+
**Incorrect:**
|
|
566
|
+
|
|
567
|
+
```tsx
|
|
568
|
+
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: Props) {
|
|
569
|
+
// ...
|
|
570
|
+
})
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**Correct:**
|
|
574
|
+
|
|
575
|
+
```tsx
|
|
576
|
+
const NOOP = () => {};
|
|
577
|
+
|
|
578
|
+
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: Props) {
|
|
579
|
+
// ...
|
|
580
|
+
})
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### 4.5 Extract to Memoized Components
|
|
584
|
+
|
|
585
|
+
**Impact: MEDIUM**
|
|
586
|
+
|
|
587
|
+
Memoized components encapsulate expensive work, enabling early returns that skip computation entirely.
|
|
588
|
+
|
|
589
|
+
**Incorrect: computes avatar even when loading**
|
|
590
|
+
|
|
591
|
+
```tsx
|
|
592
|
+
function Profile({ user, loading }: Props) {
|
|
593
|
+
const avatar = useMemo(() => {
|
|
594
|
+
const id = computeAvatarId(user)
|
|
595
|
+
return <Avatar id={id} />
|
|
596
|
+
}, [user])
|
|
597
|
+
|
|
598
|
+
if (loading) return <Skeleton />
|
|
599
|
+
return <div>{avatar}</div>
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Correct: skips computation when loading**
|
|
604
|
+
|
|
605
|
+
```tsx
|
|
606
|
+
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
|
607
|
+
const id = useMemo(() => computeAvatarId(user), [user])
|
|
608
|
+
return <Avatar id={id} />
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
function Profile({ user, loading }: Props) {
|
|
612
|
+
if (loading) return <Skeleton />
|
|
613
|
+
return (
|
|
614
|
+
<div>
|
|
615
|
+
<UserAvatar user={user} />
|
|
616
|
+
</div>
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
> If React Compiler is enabled, manual `memo()` and `useMemo()` are unnecessary.
|
|
622
|
+
|
|
623
|
+
### 4.6 Narrow Effect Dependencies
|
|
624
|
+
|
|
625
|
+
**Impact: LOW**
|
|
626
|
+
|
|
627
|
+
Primitive dependencies trigger effects only on meaningful changes; object dependencies trigger on every new reference.
|
|
628
|
+
|
|
629
|
+
```tsx
|
|
630
|
+
// ❌ Re-runs on any user field change
|
|
631
|
+
useEffect(() => {
|
|
632
|
+
console.log(user.id)
|
|
633
|
+
}, [user])
|
|
634
|
+
|
|
635
|
+
// ✅ Re-runs only when id changes
|
|
636
|
+
useEffect(() => {
|
|
637
|
+
console.log(user.id)
|
|
638
|
+
}, [user.id])
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Derived state outside effect:**
|
|
642
|
+
|
|
643
|
+
```tsx
|
|
644
|
+
// ❌ Runs on width=767, 766, 765...
|
|
645
|
+
useEffect(() => {
|
|
646
|
+
if (width < 768) enableMobileMode()
|
|
647
|
+
}, [width])
|
|
648
|
+
|
|
649
|
+
// ✅ Runs only on boolean transition
|
|
650
|
+
const isMobile = width < 768
|
|
651
|
+
useEffect(() => {
|
|
652
|
+
if (isMobile) enableMobileMode()
|
|
653
|
+
}, [isMobile])
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### 4.7 Interaction Logic in Event Handlers
|
|
657
|
+
|
|
658
|
+
**Impact: MEDIUM**
|
|
659
|
+
|
|
660
|
+
Side effects triggered by specific user actions belong in event handlers. Modeling actions as state + effect creates unnecessary render cycles.
|
|
661
|
+
|
|
662
|
+
**Incorrect:**
|
|
663
|
+
|
|
664
|
+
```tsx
|
|
665
|
+
function Form() {
|
|
666
|
+
const [submitted, setSubmitted] = useState(false)
|
|
667
|
+
const theme = useContext(ThemeContext)
|
|
668
|
+
|
|
669
|
+
useEffect(() => {
|
|
670
|
+
if (submitted) {
|
|
671
|
+
post('/api/register')
|
|
672
|
+
showToast('Registered', theme)
|
|
673
|
+
}
|
|
674
|
+
}, [submitted, theme])
|
|
675
|
+
|
|
676
|
+
return <button onClick={() => setSubmitted(true)}>Submit</button>
|
|
677
|
+
}
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
**Correct:**
|
|
681
|
+
|
|
682
|
+
```tsx
|
|
683
|
+
function Form() {
|
|
684
|
+
const theme = useContext(ThemeContext)
|
|
685
|
+
|
|
686
|
+
function handleSubmit() {
|
|
687
|
+
post('/api/register')
|
|
688
|
+
showToast('Registered', theme)
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return <button onClick={handleSubmit}>Submit</button>
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### 4.8 Subscribe to Derived State
|
|
696
|
+
|
|
697
|
+
**Impact: MEDIUM**
|
|
698
|
+
|
|
699
|
+
Derived boolean state changes less frequently than continuous values, reducing re-render frequency.
|
|
700
|
+
|
|
701
|
+
**Incorrect: re-renders on every pixel change**
|
|
702
|
+
|
|
703
|
+
```tsx
|
|
704
|
+
function Sidebar() {
|
|
705
|
+
const width = useWindowWidth()
|
|
706
|
+
const isMobile = width < 768
|
|
707
|
+
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
**Correct: re-renders only when boolean changes**
|
|
712
|
+
|
|
713
|
+
```tsx
|
|
714
|
+
function Sidebar() {
|
|
715
|
+
const isMobile = useMediaQuery('(max-width: 767px)')
|
|
716
|
+
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### 4.9 Use Functional setState Updates
|
|
721
|
+
|
|
722
|
+
**Impact: MEDIUM**
|
|
723
|
+
|
|
724
|
+
Prevents stale closures and unnecessary callback recreations.
|
|
725
|
+
|
|
726
|
+
**Incorrect: requires state as dependency**
|
|
727
|
+
|
|
728
|
+
```tsx
|
|
729
|
+
const addItems = useCallback((newItems: Item[]) => {
|
|
730
|
+
setItems([...items, ...newItems])
|
|
731
|
+
}, [items]) // items dependency causes recreations
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
**Correct: stable callbacks, no stale closures**
|
|
735
|
+
|
|
736
|
+
```tsx
|
|
737
|
+
const addItems = useCallback((newItems: Item[]) => {
|
|
738
|
+
setItems(curr => [...curr, ...newItems])
|
|
739
|
+
}, []) // No dependencies needed
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**When to use functional updates:** any `setState` that depends on the current state value, inside `useCallback`/`useMemo`, event handlers that reference state, async operations.
|
|
743
|
+
|
|
744
|
+
**When direct updates are fine:** setting to a static value (`setCount(0)`), setting from props/arguments only, state doesn't depend on previous value.
|
|
745
|
+
|
|
746
|
+
### 4.10 Lazy State Initialization
|
|
747
|
+
|
|
748
|
+
**Impact: MEDIUM**
|
|
749
|
+
|
|
750
|
+
A function passed to `useState` runs only on initial render; a direct expression runs on every render.
|
|
751
|
+
|
|
752
|
+
**Incorrect: runs on every render**
|
|
753
|
+
|
|
754
|
+
```tsx
|
|
755
|
+
const [settings, setSettings] = useState(
|
|
756
|
+
JSON.parse(localStorage.getItem('settings') || '{}')
|
|
757
|
+
)
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
**Correct: runs only once**
|
|
761
|
+
|
|
762
|
+
```tsx
|
|
763
|
+
const [settings, setSettings] = useState(() => {
|
|
764
|
+
const stored = localStorage.getItem('settings')
|
|
765
|
+
return stored ? JSON.parse(stored) : {}
|
|
766
|
+
})
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### 4.11 Use Transitions for Non-Urgent Updates
|
|
770
|
+
|
|
771
|
+
**Impact: MEDIUM**
|
|
772
|
+
|
|
773
|
+
```tsx
|
|
774
|
+
import { startTransition } from 'react'
|
|
775
|
+
|
|
776
|
+
function ScrollTracker() {
|
|
777
|
+
const [scrollY, setScrollY] = useState(0)
|
|
778
|
+
useEffect(() => {
|
|
779
|
+
const handler = () => {
|
|
780
|
+
startTransition(() => setScrollY(window.scrollY))
|
|
781
|
+
}
|
|
782
|
+
window.addEventListener('scroll', handler, { passive: true })
|
|
783
|
+
return () => window.removeEventListener('scroll', handler)
|
|
784
|
+
}, [])
|
|
785
|
+
}
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### 4.12 Use useRef for Transient Values
|
|
789
|
+
|
|
790
|
+
**Impact: MEDIUM**
|
|
791
|
+
|
|
792
|
+
When a value changes frequently and you don't need a re-render on every update, use `useRef` instead of `useState`.
|
|
793
|
+
|
|
794
|
+
**Incorrect: renders every update**
|
|
795
|
+
|
|
796
|
+
```tsx
|
|
797
|
+
function Tracker() {
|
|
798
|
+
const [lastX, setLastX] = useState(0)
|
|
799
|
+
|
|
800
|
+
useEffect(() => {
|
|
801
|
+
const onMove = (e: MouseEvent) => setLastX(e.clientX)
|
|
802
|
+
window.addEventListener('mousemove', onMove)
|
|
803
|
+
return () => window.removeEventListener('mousemove', onMove)
|
|
804
|
+
}, [])
|
|
805
|
+
|
|
806
|
+
return <div style={{ left: lastX }} />
|
|
807
|
+
}
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
**Correct: no re-render for tracking**
|
|
811
|
+
|
|
812
|
+
```tsx
|
|
813
|
+
function Tracker() {
|
|
814
|
+
const lastXRef = useRef(0)
|
|
815
|
+
const dotRef = useRef<HTMLDivElement>(null)
|
|
816
|
+
|
|
817
|
+
useEffect(() => {
|
|
818
|
+
const onMove = (e: MouseEvent) => {
|
|
819
|
+
lastXRef.current = e.clientX
|
|
820
|
+
if (dotRef.current) {
|
|
821
|
+
dotRef.current.style.transform = `translateX(${e.clientX}px)`
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
window.addEventListener('mousemove', onMove)
|
|
825
|
+
return () => window.removeEventListener('mousemove', onMove)
|
|
826
|
+
}, [])
|
|
827
|
+
|
|
828
|
+
return <div ref={dotRef} style={{ transform: 'translateX(0px)' }} />
|
|
829
|
+
}
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
---
|
|
833
|
+
|
|
834
|
+
## 5. Rendering Performance
|
|
835
|
+
|
|
836
|
+
**Impact: MEDIUM**
|
|
837
|
+
|
|
838
|
+
### 5.1 Animate SVG Wrapper Instead of SVG Element
|
|
839
|
+
|
|
840
|
+
**Impact: LOW**
|
|
841
|
+
|
|
842
|
+
```tsx
|
|
843
|
+
// ❌ No hardware acceleration
|
|
844
|
+
<svg className="animate-spin" width="24" height="24" viewBox="0 0 24 24">
|
|
845
|
+
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
|
846
|
+
</svg>
|
|
847
|
+
|
|
848
|
+
// ✅ Hardware accelerated
|
|
849
|
+
<div className="animate-spin">
|
|
850
|
+
<svg width="24" height="24" viewBox="0 0 24 24">
|
|
851
|
+
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
|
852
|
+
</svg>
|
|
853
|
+
</div>
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
### 5.2 CSS content-visibility for Long Lists
|
|
857
|
+
|
|
858
|
+
**Impact: HIGH**
|
|
859
|
+
|
|
860
|
+
```css
|
|
861
|
+
.message-item {
|
|
862
|
+
content-visibility: auto;
|
|
863
|
+
contain-intrinsic-size: 0 80px;
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
For 1000 items, browser skips layout/paint for ~990 off-screen items.
|
|
868
|
+
|
|
869
|
+
### 5.3 Hoist Static JSX Elements
|
|
870
|
+
|
|
871
|
+
**Impact: LOW**
|
|
872
|
+
|
|
873
|
+
```tsx
|
|
874
|
+
// ❌ Recreates element every render
|
|
875
|
+
function LoadingSkeleton() {
|
|
876
|
+
return <div className="animate-pulse h-20 bg-gray-200" />
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// ✅ Reuses same element
|
|
880
|
+
const loadingSkeleton = (
|
|
881
|
+
<div className="animate-pulse h-20 bg-gray-200" />
|
|
882
|
+
)
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
> If React Compiler is enabled, it automatically hoists static JSX.
|
|
886
|
+
|
|
887
|
+
### 5.4 Use Activity Component for Show/Hide
|
|
888
|
+
|
|
889
|
+
**Impact: MEDIUM**
|
|
890
|
+
|
|
891
|
+
```tsx
|
|
892
|
+
import { Activity } from 'react'
|
|
893
|
+
|
|
894
|
+
function Dropdown({ isOpen }: Props) {
|
|
895
|
+
return (
|
|
896
|
+
<Activity mode={isOpen ? 'visible' : 'hidden'}>
|
|
897
|
+
<ExpensiveMenu />
|
|
898
|
+
</Activity>
|
|
899
|
+
)
|
|
900
|
+
}
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
Avoids expensive re-renders and state loss.
|
|
904
|
+
|
|
905
|
+
### 5.5 Use Explicit Conditional Rendering
|
|
906
|
+
|
|
907
|
+
**Impact: LOW**
|
|
908
|
+
|
|
909
|
+
```tsx
|
|
910
|
+
// ❌ Renders "0" when count is 0
|
|
911
|
+
{count && <span className="badge">{count}</span>}
|
|
912
|
+
|
|
913
|
+
// ✅ Renders nothing when count is 0
|
|
914
|
+
{count > 0 ? <span className="badge">{count}</span> : null}
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
### 5.6 Use useTransition Over Manual Loading States
|
|
918
|
+
|
|
919
|
+
**Impact: LOW**
|
|
920
|
+
|
|
921
|
+
```tsx
|
|
922
|
+
import { useTransition, useState } from 'react'
|
|
923
|
+
|
|
924
|
+
function SearchResults() {
|
|
925
|
+
const [query, setQuery] = useState('')
|
|
926
|
+
const [results, setResults] = useState([])
|
|
927
|
+
const [isPending, startTransition] = useTransition()
|
|
928
|
+
|
|
929
|
+
const handleSearch = (value: string) => {
|
|
930
|
+
setQuery(value)
|
|
931
|
+
startTransition(async () => {
|
|
932
|
+
const data = await fetchResults(value)
|
|
933
|
+
setResults(data)
|
|
934
|
+
})
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return (
|
|
938
|
+
<>
|
|
939
|
+
<input onChange={(e) => handleSearch(e.target.value)} />
|
|
940
|
+
{isPending && <Spinner />}
|
|
941
|
+
<ResultsList results={results} />
|
|
942
|
+
</>
|
|
943
|
+
)
|
|
944
|
+
}
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
---
|
|
948
|
+
|
|
949
|
+
## 6. JavaScript Performance
|
|
950
|
+
|
|
951
|
+
**Impact: LOW-MEDIUM**
|
|
952
|
+
|
|
953
|
+
### 6.1 Layout Thrashing
|
|
954
|
+
|
|
955
|
+
**Impact: MEDIUM**
|
|
956
|
+
|
|
957
|
+
Interleaving style writes with layout reads forces synchronous reflow on each read. Batching writes then reading once avoids this.
|
|
958
|
+
|
|
959
|
+
```typescript
|
|
960
|
+
// ❌ Forces reflow on each read
|
|
961
|
+
element.style.width = '100px'
|
|
962
|
+
const width = element.offsetWidth // Forces reflow
|
|
963
|
+
element.style.height = '200px'
|
|
964
|
+
const height = element.offsetHeight // Forces another reflow
|
|
965
|
+
|
|
966
|
+
// ✅ Batch writes, then read once
|
|
967
|
+
element.style.width = '100px'
|
|
968
|
+
element.style.height = '200px'
|
|
969
|
+
const { width, height } = element.getBoundingClientRect()
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
Prefer CSS classes over inline styles when possible.
|
|
973
|
+
|
|
974
|
+
### 6.2 Build Index Maps for Repeated Lookups
|
|
975
|
+
|
|
976
|
+
**Impact: LOW-MEDIUM**
|
|
977
|
+
|
|
978
|
+
```typescript
|
|
979
|
+
// ❌ O(n) per lookup
|
|
980
|
+
orders.map(order => ({
|
|
981
|
+
...order,
|
|
982
|
+
user: users.find(u => u.id === order.userId)
|
|
983
|
+
}))
|
|
984
|
+
|
|
985
|
+
// ✅ O(1) per lookup
|
|
986
|
+
const userById = new Map(users.map(u => [u.id, u]))
|
|
987
|
+
orders.map(order => ({
|
|
988
|
+
...order,
|
|
989
|
+
user: userById.get(order.userId)
|
|
990
|
+
}))
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
### 6.3 Cache Repeated Function Calls
|
|
994
|
+
|
|
995
|
+
**Impact: MEDIUM**
|
|
996
|
+
|
|
997
|
+
```typescript
|
|
998
|
+
const slugifyCache = new Map<string, string>()
|
|
999
|
+
|
|
1000
|
+
function cachedSlugify(text: string): string {
|
|
1001
|
+
if (slugifyCache.has(text)) return slugifyCache.get(text)!
|
|
1002
|
+
const result = slugify(text)
|
|
1003
|
+
slugifyCache.set(text, result)
|
|
1004
|
+
return result
|
|
1005
|
+
}
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
### 6.4 Cache Storage API Calls
|
|
1009
|
+
|
|
1010
|
+
**Impact: LOW-MEDIUM**
|
|
1011
|
+
|
|
1012
|
+
```typescript
|
|
1013
|
+
const storageCache = new Map<string, string | null>()
|
|
1014
|
+
|
|
1015
|
+
function getLocalStorage(key: string) {
|
|
1016
|
+
if (!storageCache.has(key)) {
|
|
1017
|
+
storageCache.set(key, localStorage.getItem(key))
|
|
1018
|
+
}
|
|
1019
|
+
return storageCache.get(key)
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function setLocalStorage(key: string, value: string) {
|
|
1023
|
+
localStorage.setItem(key, value)
|
|
1024
|
+
storageCache.set(key, value)
|
|
1025
|
+
}
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
Invalidate on external changes:
|
|
1029
|
+
|
|
1030
|
+
```typescript
|
|
1031
|
+
window.addEventListener('storage', (e) => {
|
|
1032
|
+
if (e.key) storageCache.delete(e.key)
|
|
1033
|
+
})
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
### 6.5 Combined Array Iterations
|
|
1037
|
+
|
|
1038
|
+
**Impact: LOW-MEDIUM**
|
|
1039
|
+
|
|
1040
|
+
```typescript
|
|
1041
|
+
// ❌ 3 iterations
|
|
1042
|
+
const admins = users.filter(u => u.isAdmin)
|
|
1043
|
+
const testers = users.filter(u => u.isTester)
|
|
1044
|
+
const inactive = users.filter(u => !u.isActive)
|
|
1045
|
+
|
|
1046
|
+
// ✅ 1 iteration
|
|
1047
|
+
const admins: User[] = []
|
|
1048
|
+
const testers: User[] = []
|
|
1049
|
+
const inactive: User[] = []
|
|
1050
|
+
|
|
1051
|
+
for (const user of users) {
|
|
1052
|
+
if (user.isAdmin) admins.push(user)
|
|
1053
|
+
if (user.isTester) testers.push(user)
|
|
1054
|
+
if (!user.isActive) inactive.push(user)
|
|
1055
|
+
}
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
### 6.6 Use Set/Map for O(1) Lookups
|
|
1059
|
+
|
|
1060
|
+
**Impact: LOW-MEDIUM**
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
// ❌ O(n) per check
|
|
1064
|
+
const allowedIds = ['a', 'b', 'c']
|
|
1065
|
+
items.filter(item => allowedIds.includes(item.id))
|
|
1066
|
+
|
|
1067
|
+
// ✅ O(1) per check
|
|
1068
|
+
const allowedIds = new Set(['a', 'b', 'c'])
|
|
1069
|
+
items.filter(item => allowedIds.has(item.id))
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
### 6.7 Use toSorted() for Immutability
|
|
1073
|
+
|
|
1074
|
+
**Impact: MEDIUM-HIGH**
|
|
1075
|
+
|
|
1076
|
+
```typescript
|
|
1077
|
+
// ❌ Mutates the users prop array
|
|
1078
|
+
const sorted = users.sort((a, b) => a.name.localeCompare(b.name))
|
|
1079
|
+
|
|
1080
|
+
// ✅ Creates new sorted array
|
|
1081
|
+
const sorted = users.toSorted((a, b) => a.name.localeCompare(b.name))
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
Other immutable methods: `.toReversed()`, `.toSpliced()`, `.with()`.
|
|
1085
|
+
|
|
1086
|
+
### 6.8 Early Return from Functions
|
|
1087
|
+
|
|
1088
|
+
**Impact: LOW-MEDIUM**
|
|
1089
|
+
|
|
1090
|
+
```typescript
|
|
1091
|
+
// ❌ Processes all items even after finding answer
|
|
1092
|
+
function validateUsers(users: User[]) {
|
|
1093
|
+
let hasError = false
|
|
1094
|
+
let errorMessage = ''
|
|
1095
|
+
for (const user of users) {
|
|
1096
|
+
if (!user.email) { hasError = true; errorMessage = 'Email required' }
|
|
1097
|
+
if (!user.name) { hasError = true; errorMessage = 'Name required' }
|
|
1098
|
+
}
|
|
1099
|
+
return hasError ? { valid: false, error: errorMessage } : { valid: true }
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// ✅ Returns immediately on first error
|
|
1103
|
+
function validateUsers(users: User[]) {
|
|
1104
|
+
for (const user of users) {
|
|
1105
|
+
if (!user.email) return { valid: false, error: 'Email required' }
|
|
1106
|
+
if (!user.name) return { valid: false, error: 'Name required' }
|
|
1107
|
+
}
|
|
1108
|
+
return { valid: true }
|
|
1109
|
+
}
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
### 6.9 Hoisted RegExp Creation
|
|
1113
|
+
|
|
1114
|
+
**Impact: LOW-MEDIUM**
|
|
1115
|
+
|
|
1116
|
+
```tsx
|
|
1117
|
+
// ❌ New RegExp every render
|
|
1118
|
+
function Highlighter({ text, query }: Props) {
|
|
1119
|
+
const regex = new RegExp(`(${query})`, 'gi')
|
|
1120
|
+
const parts = text.split(regex)
|
|
1121
|
+
return <>{parts.map((part, i) => ...)}</>
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// ✅ Memoize
|
|
1125
|
+
function Highlighter({ text, query }: Props) {
|
|
1126
|
+
const regex = useMemo(
|
|
1127
|
+
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
|
|
1128
|
+
[query]
|
|
1129
|
+
)
|
|
1130
|
+
const parts = text.split(regex)
|
|
1131
|
+
return <>{parts.map((part, i) => ...)}</>
|
|
1132
|
+
}
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
### 6.10 Early Length Check for Array Comparisons
|
|
1136
|
+
|
|
1137
|
+
**Impact: MEDIUM-HIGH**
|
|
1138
|
+
|
|
1139
|
+
```typescript
|
|
1140
|
+
function hasChanges(current: string[], original: string[]) {
|
|
1141
|
+
if (current.length !== original.length) return true
|
|
1142
|
+
|
|
1143
|
+
const currentSorted = current.toSorted()
|
|
1144
|
+
const originalSorted = original.toSorted()
|
|
1145
|
+
for (let i = 0; i < currentSorted.length; i++) {
|
|
1146
|
+
if (currentSorted[i] !== originalSorted[i]) return true
|
|
1147
|
+
}
|
|
1148
|
+
return false
|
|
1149
|
+
}
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
---
|
|
1153
|
+
|
|
1154
|
+
## 7. Advanced Patterns
|
|
1155
|
+
|
|
1156
|
+
**Impact: LOW**
|
|
1157
|
+
|
|
1158
|
+
### 7.1 Initialize App Once, Not Per Mount
|
|
1159
|
+
|
|
1160
|
+
**Impact: LOW-MEDIUM**
|
|
1161
|
+
|
|
1162
|
+
```tsx
|
|
1163
|
+
let didInit = false
|
|
1164
|
+
|
|
1165
|
+
function App() {
|
|
1166
|
+
useEffect(() => {
|
|
1167
|
+
if (didInit) return
|
|
1168
|
+
didInit = true
|
|
1169
|
+
loadFromStorage()
|
|
1170
|
+
checkAuthToken()
|
|
1171
|
+
}, [])
|
|
1172
|
+
|
|
1173
|
+
// ...
|
|
1174
|
+
}
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
### 7.2 Store Event Handlers in Refs
|
|
1178
|
+
|
|
1179
|
+
**Impact: LOW**
|
|
1180
|
+
|
|
1181
|
+
```tsx
|
|
1182
|
+
import { useEffectEvent } from 'react'
|
|
1183
|
+
|
|
1184
|
+
function useWindowEvent(event: string, handler: (e: Event) => void) {
|
|
1185
|
+
const onEvent = useEffectEvent(handler)
|
|
1186
|
+
|
|
1187
|
+
useEffect(() => {
|
|
1188
|
+
window.addEventListener(event, onEvent)
|
|
1189
|
+
return () => window.removeEventListener(event, onEvent)
|
|
1190
|
+
}, [event])
|
|
1191
|
+
}
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
### 7.3 useEffectEvent for Stable Callback Refs
|
|
1195
|
+
|
|
1196
|
+
**Impact: LOW**
|
|
1197
|
+
|
|
1198
|
+
```tsx
|
|
1199
|
+
import { useEffectEvent } from 'react'
|
|
1200
|
+
|
|
1201
|
+
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
|
1202
|
+
const [query, setQuery] = useState('')
|
|
1203
|
+
const onSearchEvent = useEffectEvent(onSearch)
|
|
1204
|
+
|
|
1205
|
+
useEffect(() => {
|
|
1206
|
+
const timeout = setTimeout(() => onSearchEvent(query), 300)
|
|
1207
|
+
return () => clearTimeout(timeout)
|
|
1208
|
+
}, [query])
|
|
1209
|
+
}
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
---
|
|
1213
|
+
|
|
1214
|
+
## References
|
|
1215
|
+
|
|
1216
|
+
1. [https://react.dev](https://react.dev)
|
|
1217
|
+
2. [https://react.dev/reference/react/use](https://react.dev/reference/react/use)
|
|
1218
|
+
3. [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect)
|
|
1219
|
+
4. [https://react.dev/reference/react/useTransition](https://react.dev/reference/react/useTransition)
|
|
1220
|
+
5. [https://swr.vercel.app](https://swr.vercel.app)
|
|
1221
|
+
6. [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
|
1222
|
+
7. [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
|