mirror of
https://github.com/langgenius/dify.git
synced 2026-01-24 00:04:19 +00:00
Compare commits
7 Commits
refactor/s
...
refactor/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9972caae3 | ||
|
|
b18cbff88d | ||
|
|
89f5c27edc | ||
|
|
2d8dcbc0d8 | ||
|
|
8ce458a9b0 | ||
|
|
e6aa30f776 | ||
|
|
3b58b0d129 |
1
.agent/skills
Symbolic link
1
.agent/skills
Symbolic link
@@ -0,0 +1 @@
|
||||
../.claude/skills
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/component-refactoring
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/frontend-code-review
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/frontend-testing
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/orpc-contract-first
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/skill-creator
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/vercel-react-best-practices
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/web-design-guidelines
|
||||
@@ -1,107 +0,0 @@
|
||||
---
|
||||
title: Avoid Layout Thrashing
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents forced synchronous layouts and reduces performance bottlenecks
|
||||
tags: javascript, dom, css, performance, reflow, layout-thrashing
|
||||
---
|
||||
|
||||
## Avoid Layout Thrashing
|
||||
|
||||
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
|
||||
|
||||
**This is OK (browser batches style changes):**
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
// Each line invalidates style, but browser batches the recalculation
|
||||
element.style.width = '100px'
|
||||
element.style.height = '200px'
|
||||
element.style.backgroundColor = 'blue'
|
||||
element.style.border = '1px solid black'
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (interleaved reads and writes force reflows):**
|
||||
```typescript
|
||||
function layoutThrashing(element: HTMLElement) {
|
||||
element.style.width = '100px'
|
||||
const width = element.offsetWidth // Forces reflow
|
||||
element.style.height = '200px'
|
||||
const height = element.offsetHeight // Forces another reflow
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (batch writes, then read once):**
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
// Batch all writes together
|
||||
element.style.width = '100px'
|
||||
element.style.height = '200px'
|
||||
element.style.backgroundColor = 'blue'
|
||||
element.style.border = '1px solid black'
|
||||
|
||||
// Read after all writes are done (single reflow)
|
||||
const { width, height } = element.getBoundingClientRect()
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (batch reads, then writes):**
|
||||
```typescript
|
||||
function avoidThrashing(element: HTMLElement) {
|
||||
// Read phase - all layout queries first
|
||||
const rect1 = element.getBoundingClientRect()
|
||||
const offsetWidth = element.offsetWidth
|
||||
const offsetHeight = element.offsetHeight
|
||||
|
||||
// Write phase - all style changes after
|
||||
element.style.width = '100px'
|
||||
element.style.height = '200px'
|
||||
}
|
||||
```
|
||||
|
||||
**Better: use CSS classes**
|
||||
```css
|
||||
.highlighted-box {
|
||||
width: 100px;
|
||||
height: 200px;
|
||||
background-color: blue;
|
||||
border: 1px solid black;
|
||||
}
|
||||
```
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
element.classList.add('highlighted-box')
|
||||
|
||||
const { width, height } = element.getBoundingClientRect()
|
||||
}
|
||||
```
|
||||
|
||||
**React example:**
|
||||
```tsx
|
||||
// Incorrect: interleaving style changes with layout queries
|
||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && isHighlighted) {
|
||||
ref.current.style.width = '100px'
|
||||
const width = ref.current.offsetWidth // Forces layout
|
||||
ref.current.style.height = '200px'
|
||||
}
|
||||
}, [isHighlighted])
|
||||
|
||||
return <div ref={ref}>Content</div>
|
||||
}
|
||||
|
||||
// Correct: toggle class
|
||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||
return (
|
||||
<div className={isHighlighted ? 'highlighted-box' : ''}>
|
||||
Content
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
|
||||
|
||||
See [this gist](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) and [CSS Triggers](https://csstriggers.com/) for more information on layout-forcing operations.
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
|
||||
title: Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
||||
impact: MEDIUM
|
||||
impactDescription: restores memoization by using a constant for default value
|
||||
tags: rerender, memo, optimization
|
||||
|
||||
---
|
||||
|
||||
## Extract Default Non-primitive Parameter Value from Memoized Component to Constant
|
||||
|
||||
When memoized component has a default value for some non-primitive optional parameter, such as an array, function, or object, calling the component without that parameter results in broken memoization. This is because new value instances are created on every rerender, and they do not pass strict equality comparison in `memo()`.
|
||||
|
||||
To address this issue, extract the default value into a constant.
|
||||
|
||||
**Incorrect (`onClick` has different values on every rerender):**
|
||||
|
||||
```tsx
|
||||
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
|
||||
// ...
|
||||
})
|
||||
|
||||
// Used without optional onClick
|
||||
<UserAvatar />
|
||||
```
|
||||
|
||||
**Correct (stable default value):**
|
||||
|
||||
```tsx
|
||||
const NOOP = () => {};
|
||||
|
||||
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
|
||||
// ...
|
||||
})
|
||||
|
||||
// Used without optional onClick
|
||||
<UserAvatar />
|
||||
```
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
title: Do not wrap a simple expression with a primitive result type in useMemo
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: wasted computation on every render
|
||||
tags: rerender, useMemo, optimization
|
||||
---
|
||||
|
||||
## Do not wrap a simple expression with a primitive result type in useMemo
|
||||
|
||||
When an expression is simple (few logical or arithmetical operators) and has a primitive result type (boolean, number, string), do not wrap it in `useMemo`.
|
||||
Calling `useMemo` and comparing hook dependencies may consume more resources than the expression itself.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
function Header({ user, notifications }: Props) {
|
||||
const isLoading = useMemo(() => {
|
||||
return user.isLoading || notifications.isLoading
|
||||
}, [user.isLoading, notifications.isLoading])
|
||||
|
||||
if (isLoading) return <Skeleton />
|
||||
// return some markup
|
||||
}
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
function Header({ user, notifications }: Props) {
|
||||
const isLoading = user.isLoading || notifications.isLoading
|
||||
|
||||
if (isLoading) return <Skeleton />
|
||||
// return some markup
|
||||
}
|
||||
```
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
title: Authenticate Server Actions Like API Routes
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents unauthorized access to server mutations
|
||||
tags: server, server-actions, authentication, security, authorization
|
||||
---
|
||||
|
||||
## Authenticate Server Actions Like API Routes
|
||||
|
||||
**Impact: CRITICAL (prevents unauthorized access to server mutations)**
|
||||
|
||||
Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
|
||||
|
||||
Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
|
||||
|
||||
**Incorrect (no authentication check):**
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
export async function deleteUser(userId: string) {
|
||||
// Anyone can call this! No auth check
|
||||
await db.user.delete({ where: { id: userId } })
|
||||
return { success: true }
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (authentication inside the action):**
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
import { verifySession } from '@/lib/auth'
|
||||
import { unauthorized } from '@/lib/errors'
|
||||
|
||||
export async function deleteUser(userId: string) {
|
||||
// Always check auth inside the action
|
||||
const session = await verifySession()
|
||||
|
||||
if (!session) {
|
||||
throw unauthorized('Must be logged in')
|
||||
}
|
||||
|
||||
// Check authorization too
|
||||
if (session.user.role !== 'admin' && session.user.id !== userId) {
|
||||
throw unauthorized('Cannot delete other users')
|
||||
}
|
||||
|
||||
await db.user.delete({ where: { id: userId } })
|
||||
return { success: true }
|
||||
}
|
||||
```
|
||||
|
||||
**With input validation:**
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
import { verifySession } from '@/lib/auth'
|
||||
import { z } from 'zod'
|
||||
|
||||
const updateProfileSchema = z.object({
|
||||
userId: z.string().uuid(),
|
||||
name: z.string().min(1).max(100),
|
||||
email: z.string().email()
|
||||
})
|
||||
|
||||
export async function updateProfile(data: unknown) {
|
||||
// Validate input first
|
||||
const validated = updateProfileSchema.parse(data)
|
||||
|
||||
// Then authenticate
|
||||
const session = await verifySession()
|
||||
if (!session) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
// Then authorize
|
||||
if (session.user.id !== validated.userId) {
|
||||
throw new Error('Can only update own profile')
|
||||
}
|
||||
|
||||
// Finally perform the mutation
|
||||
await db.user.update({
|
||||
where: { id: validated.userId },
|
||||
data: {
|
||||
name: validated.name,
|
||||
email: validated.email
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
|
||||
@@ -1,65 +0,0 @@
|
||||
---
|
||||
title: Avoid Duplicate Serialization in RSC Props
|
||||
impact: LOW
|
||||
impactDescription: reduces network payload by avoiding duplicate serialization
|
||||
tags: server, rsc, serialization, props, client-components
|
||||
---
|
||||
|
||||
## Avoid Duplicate Serialization in RSC Props
|
||||
|
||||
**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
|
||||
|
||||
RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
|
||||
|
||||
**Incorrect (duplicates array):**
|
||||
|
||||
```tsx
|
||||
// RSC: sends 6 strings (2 arrays × 3 items)
|
||||
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
|
||||
```
|
||||
|
||||
**Correct (sends 3 strings):**
|
||||
|
||||
```tsx
|
||||
// RSC: send once
|
||||
<ClientList usernames={usernames} />
|
||||
|
||||
// Client: transform there
|
||||
'use client'
|
||||
const sorted = useMemo(() => [...usernames].sort(), [usernames])
|
||||
```
|
||||
|
||||
**Nested deduplication behavior:**
|
||||
|
||||
Deduplication works recursively. Impact varies by data type:
|
||||
|
||||
- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
|
||||
- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
|
||||
|
||||
```tsx
|
||||
// string[] - duplicates everything
|
||||
usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
|
||||
|
||||
// object[] - duplicates array structure only
|
||||
users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
|
||||
```
|
||||
|
||||
**Operations breaking deduplication (create new references):**
|
||||
|
||||
- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
|
||||
- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
|
||||
|
||||
**More examples:**
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
<C users={users} active={users.filter(u => u.active)} />
|
||||
<C product={product} productName={product.name} />
|
||||
|
||||
// ✅ Good
|
||||
<C users={users} />
|
||||
<C product={product} />
|
||||
// Do filtering/destructuring in client
|
||||
```
|
||||
|
||||
**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
name: web-design-guidelines
|
||||
description: Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
|
||||
metadata:
|
||||
author: vercel
|
||||
version: "1.0.0"
|
||||
argument-hint: <file-or-pattern>
|
||||
---
|
||||
|
||||
# Web Interface Guidelines
|
||||
|
||||
Review files for compliance with Web Interface Guidelines.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Fetch the latest guidelines from the source URL below
|
||||
2. Read the specified files (or prompt user for files/pattern)
|
||||
3. Check against all rules in the fetched guidelines
|
||||
4. Output findings in the terse `file:line` format
|
||||
|
||||
## Guidelines Source
|
||||
|
||||
Fetch fresh guidelines before each review:
|
||||
|
||||
```
|
||||
https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md
|
||||
```
|
||||
|
||||
Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and output format instructions.
|
||||
|
||||
## Usage
|
||||
|
||||
When a user provides a file or pattern argument:
|
||||
1. Fetch guidelines from the source URL above
|
||||
2. Read the specified files
|
||||
3. Apply all rules from the fetched guidelines
|
||||
4. Output findings using the format specified in the guidelines
|
||||
|
||||
If no files specified, ask the user which files to review.
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/component-refactoring
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/frontend-code-review
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/frontend-testing
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/orpc-contract-first
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/skill-creator
|
||||
@@ -4,6 +4,7 @@ Quick validation script for skills - minimal version
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/vercel-react-best-practices
|
||||
@@ -33,13 +33,11 @@ Comprehensive performance optimization guide for React and Next.js applications,
|
||||
- 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components)
|
||||
- 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent)
|
||||
3. [Server-Side Performance](#3-server-side-performance) — **HIGH**
|
||||
- 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes)
|
||||
- 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props)
|
||||
- 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching)
|
||||
- 3.4 [Minimize Serialization at RSC Boundaries](#34-minimize-serialization-at-rsc-boundaries)
|
||||
- 3.5 [Parallel Data Fetching with Component Composition](#35-parallel-data-fetching-with-component-composition)
|
||||
- 3.6 [Per-Request Deduplication with React.cache()](#36-per-request-deduplication-with-reactcache)
|
||||
- 3.7 [Use after() for Non-Blocking Operations](#37-use-after-for-non-blocking-operations)
|
||||
- 3.1 [Cross-Request LRU Caching](#31-cross-request-lru-caching)
|
||||
- 3.2 [Minimize Serialization at RSC Boundaries](#32-minimize-serialization-at-rsc-boundaries)
|
||||
- 3.3 [Parallel Data Fetching with Component Composition](#33-parallel-data-fetching-with-component-composition)
|
||||
- 3.4 [Per-Request Deduplication with React.cache()](#34-per-request-deduplication-with-reactcache)
|
||||
- 3.5 [Use after() for Non-Blocking Operations](#35-use-after-for-non-blocking-operations)
|
||||
4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
|
||||
- 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners)
|
||||
- 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance)
|
||||
@@ -570,158 +568,7 @@ The `typeof window !== 'undefined'` check prevents bundling preloaded modules fo
|
||||
|
||||
Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
|
||||
|
||||
### 3.1 Authenticate Server Actions Like API Routes
|
||||
|
||||
**Impact: CRITICAL (prevents unauthorized access to server mutations)**
|
||||
|
||||
Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
|
||||
|
||||
Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
|
||||
|
||||
**Incorrect: no authentication check**
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
export async function deleteUser(userId: string) {
|
||||
// Anyone can call this! No auth check
|
||||
await db.user.delete({ where: { id: userId } })
|
||||
return { success: true }
|
||||
}
|
||||
```
|
||||
|
||||
**Correct: authentication inside the action**
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
import { verifySession } from '@/lib/auth'
|
||||
import { unauthorized } from '@/lib/errors'
|
||||
|
||||
export async function deleteUser(userId: string) {
|
||||
// Always check auth inside the action
|
||||
const session = await verifySession()
|
||||
|
||||
if (!session) {
|
||||
throw unauthorized('Must be logged in')
|
||||
}
|
||||
|
||||
// Check authorization too
|
||||
if (session.user.role !== 'admin' && session.user.id !== userId) {
|
||||
throw unauthorized('Cannot delete other users')
|
||||
}
|
||||
|
||||
await db.user.delete({ where: { id: userId } })
|
||||
return { success: true }
|
||||
}
|
||||
```
|
||||
|
||||
**With input validation:**
|
||||
|
||||
```typescript
|
||||
'use server'
|
||||
|
||||
import { verifySession } from '@/lib/auth'
|
||||
import { z } from 'zod'
|
||||
|
||||
const updateProfileSchema = z.object({
|
||||
userId: z.string().uuid(),
|
||||
name: z.string().min(1).max(100),
|
||||
email: z.string().email()
|
||||
})
|
||||
|
||||
export async function updateProfile(data: unknown) {
|
||||
// Validate input first
|
||||
const validated = updateProfileSchema.parse(data)
|
||||
|
||||
// Then authenticate
|
||||
const session = await verifySession()
|
||||
if (!session) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
// Then authorize
|
||||
if (session.user.id !== validated.userId) {
|
||||
throw new Error('Can only update own profile')
|
||||
}
|
||||
|
||||
// Finally perform the mutation
|
||||
await db.user.update({
|
||||
where: { id: validated.userId },
|
||||
data: {
|
||||
name: validated.name,
|
||||
email: validated.email
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication)
|
||||
|
||||
### 3.2 Avoid Duplicate Serialization in RSC Props
|
||||
|
||||
**Impact: LOW (reduces network payload by avoiding duplicate serialization)**
|
||||
|
||||
RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server.
|
||||
|
||||
**Incorrect: duplicates array**
|
||||
|
||||
```tsx
|
||||
// RSC: sends 6 strings (2 arrays × 3 items)
|
||||
<ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
|
||||
```
|
||||
|
||||
**Correct: sends 3 strings**
|
||||
|
||||
```tsx
|
||||
// RSC: send once
|
||||
<ClientList usernames={usernames} />
|
||||
|
||||
// Client: transform there
|
||||
'use client'
|
||||
const sorted = useMemo(() => [...usernames].sort(), [usernames])
|
||||
```
|
||||
|
||||
**Nested deduplication behavior:**
|
||||
|
||||
```tsx
|
||||
// string[] - duplicates everything
|
||||
usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
|
||||
|
||||
// object[] - duplicates array structure only
|
||||
users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)
|
||||
```
|
||||
|
||||
Deduplication works recursively. Impact varies by data type:
|
||||
|
||||
- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated
|
||||
|
||||
- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference
|
||||
|
||||
**Operations breaking deduplication: create new references**
|
||||
|
||||
- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]`
|
||||
|
||||
- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())`
|
||||
|
||||
**More examples:**
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
<C users={users} active={users.filter(u => u.active)} />
|
||||
<C product={product} productName={product.name} />
|
||||
|
||||
// ✅ Good
|
||||
<C users={users} />
|
||||
<C product={product} />
|
||||
// Do filtering/destructuring in client
|
||||
```
|
||||
|
||||
**Exception:** Pass derived data when transformation is expensive or client doesn't need original.
|
||||
|
||||
### 3.3 Cross-Request LRU Caching
|
||||
### 3.1 Cross-Request LRU Caching
|
||||
|
||||
**Impact: HIGH (caches across requests)**
|
||||
|
||||
@@ -758,7 +605,7 @@ Use when sequential user actions hit multiple endpoints needing the same data wi
|
||||
|
||||
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
|
||||
|
||||
### 3.4 Minimize Serialization at RSC Boundaries
|
||||
### 3.2 Minimize Serialization at RSC Boundaries
|
||||
|
||||
**Impact: HIGH (reduces data transfer size)**
|
||||
|
||||
@@ -792,7 +639,7 @@ function Profile({ name }: { name: string }) {
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 Parallel Data Fetching with Component Composition
|
||||
### 3.3 Parallel Data Fetching with Component Composition
|
||||
|
||||
**Impact: CRITICAL (eliminates server-side waterfalls)**
|
||||
|
||||
@@ -871,7 +718,7 @@ export default function Page() {
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 Per-Request Deduplication with React.cache()
|
||||
### 3.4 Per-Request Deduplication with React.cache()
|
||||
|
||||
**Impact: MEDIUM (deduplicates within request)**
|
||||
|
||||
@@ -937,7 +784,7 @@ Use `React.cache()` to deduplicate these operations across your component tree.
|
||||
|
||||
Reference: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache)
|
||||
|
||||
### 3.7 Use after() for Non-Blocking Operations
|
||||
### 3.5 Use after() for Non-Blocking Operations
|
||||
|
||||
**Impact: MEDIUM (faster response times)**
|
||||
|
||||
@@ -1868,32 +1715,79 @@ Micro-optimizations for hot paths can add up to meaningful improvements.
|
||||
|
||||
**Impact: MEDIUM (reduces reflows/repaints)**
|
||||
|
||||
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
|
||||
Avoid changing styles one property at a time. Group multiple CSS changes together via classes or `cssText` to minimize browser reflows.
|
||||
|
||||
**Incorrect: interleaved reads and writes force reflows**
|
||||
**Incorrect: multiple reflows**
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
// Each line triggers a reflow
|
||||
element.style.width = '100px'
|
||||
const width = element.offsetWidth // Forces reflow
|
||||
element.style.height = '200px'
|
||||
const height = element.offsetHeight // Forces another reflow
|
||||
element.style.backgroundColor = 'blue'
|
||||
element.style.border = '1px solid black'
|
||||
}
|
||||
```
|
||||
|
||||
**Correct: batch writes, then read once**
|
||||
**Correct: add class - single reflow**
|
||||
|
||||
```typescript
|
||||
// CSS file
|
||||
.highlighted-box {
|
||||
width: 100px;
|
||||
height: 200px;
|
||||
background-color: blue;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
// JavaScript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
element.classList.add('highlighted-box')
|
||||
|
||||
const { width, height } = element.getBoundingClientRect()
|
||||
}
|
||||
```
|
||||
|
||||
**Better: use CSS classes**
|
||||
**Correct: change cssText - single reflow**
|
||||
|
||||
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
element.style.cssText = `
|
||||
width: 100px;
|
||||
height: 200px;
|
||||
background-color: blue;
|
||||
border: 1px solid black;
|
||||
`
|
||||
}
|
||||
```
|
||||
|
||||
**React example:**
|
||||
|
||||
```tsx
|
||||
// Incorrect: changing styles one by one
|
||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && isHighlighted) {
|
||||
ref.current.style.width = '100px'
|
||||
ref.current.style.height = '200px'
|
||||
ref.current.style.backgroundColor = 'blue'
|
||||
}
|
||||
}, [isHighlighted])
|
||||
|
||||
return <div ref={ref}>Content</div>
|
||||
}
|
||||
|
||||
// Correct: toggle class
|
||||
function Box({ isHighlighted }: { isHighlighted: boolean }) {
|
||||
return (
|
||||
<div className={isHighlighted ? 'highlighted-box' : ''}>
|
||||
Content
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Prefer CSS classes over inline styles when possible. Classes are cached by the browser and provide better separation of concerns.
|
||||
|
||||
### 7.2 Build Index Maps for Repeated Lookups
|
||||
|
||||
@@ -2150,7 +2044,7 @@ function hasChanges(current: string[], original: string[]) {
|
||||
if (current.length !== original.length) {
|
||||
return true
|
||||
}
|
||||
// Only sort when lengths match
|
||||
// Only sort/join when lengths match
|
||||
const currentSorted = current.toSorted()
|
||||
const originalSorted = original.toSorted()
|
||||
for (let i = 0; i < currentSorted.length; i++) {
|
||||
@@ -2335,7 +2229,7 @@ const min = Math.min(...numbers)
|
||||
const max = Math.max(...numbers)
|
||||
```
|
||||
|
||||
This works for small arrays, but can be slower or just throw an error for very large arrays due to spread operator limitations. Maximal array length is approximately 124000 in Chrome 143 and 638000 in Safari 18; exact numbers may vary - see [the fiddle](https://jsfiddle.net/qw1jabsx/4/). Use the loop approach for reliability.
|
||||
This works for small arrays but can be slower for very large arrays due to spread operator limitations. Use the loop approach for reliability.
|
||||
|
||||
### 7.11 Use Set/Map for O(1) Lookups
|
||||
|
||||
@@ -2431,7 +2325,7 @@ Store callbacks in refs when used in effects that shouldn't re-subscribe on call
|
||||
**Incorrect: re-subscribes on every render**
|
||||
|
||||
```tsx
|
||||
function useWindowEvent(event: string, handler: (e) => void) {
|
||||
function useWindowEvent(event: string, handler: () => void) {
|
||||
useEffect(() => {
|
||||
window.addEventListener(event, handler)
|
||||
return () => window.removeEventListener(event, handler)
|
||||
@@ -2444,7 +2338,7 @@ function useWindowEvent(event: string, handler: (e) => void) {
|
||||
```tsx
|
||||
import { useEffectEvent } from 'react'
|
||||
|
||||
function useWindowEvent(event: string, handler: (e) => void) {
|
||||
function useWindowEvent(event: string, handler: () => void) {
|
||||
const onEvent = useEffectEvent(handler)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -2469,7 +2363,7 @@ Access latest values in callbacks without adding them to dependency arrays. Prev
|
||||
```typescript
|
||||
function useLatest<T>(value: T) {
|
||||
const ref = useRef(value)
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value])
|
||||
return ref
|
||||
@@ -1,14 +1,26 @@
|
||||
---
|
||||
title: useEffectEvent for Stable Callback Refs
|
||||
title: useLatest for Stable Callback Refs
|
||||
impact: LOW
|
||||
impactDescription: prevents effect re-runs
|
||||
tags: advanced, hooks, useEffectEvent, refs, optimization
|
||||
tags: advanced, hooks, useLatest, refs, optimization
|
||||
---
|
||||
|
||||
## useEffectEvent for Stable Callback Refs
|
||||
## useLatest for Stable Callback Refs
|
||||
|
||||
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
function useLatest<T>(value: T) {
|
||||
const ref = useRef(value)
|
||||
useLayoutEffect(() => {
|
||||
ref.current = value
|
||||
}, [value])
|
||||
return ref
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (effect re-runs on every callback change):**
|
||||
|
||||
```tsx
|
||||
@@ -22,17 +34,15 @@ function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (using React's useEffectEvent):**
|
||||
**Correct (stable effect, fresh callback):**
|
||||
|
||||
```tsx
|
||||
import { useEffectEvent } from 'react';
|
||||
|
||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
const [query, setQuery] = useState('')
|
||||
const onSearchEvent = useEffectEvent(onSearch)
|
||||
const onSearchRef = useLatest(onSearch)
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onSearchEvent(query), 300)
|
||||
const timeout = setTimeout(() => onSearchRef.current(query), 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [query])
|
||||
}
|
||||
@@ -33,19 +33,4 @@ const { user, config, profile } = await all({
|
||||
})
|
||||
```
|
||||
|
||||
**Alternative without extra dependencies:**
|
||||
|
||||
We can also create all the promises first, and do `Promise.all()` at the end.
|
||||
|
||||
```typescript
|
||||
const userPromise = fetchUser()
|
||||
const profilePromise = userPromise.then(user => fetchProfile(user.id))
|
||||
|
||||
const [user, config, profile] = await Promise.all([
|
||||
userPromise,
|
||||
fetchConfig(),
|
||||
profilePromise
|
||||
])
|
||||
```
|
||||
|
||||
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Batch DOM CSS Changes
|
||||
impact: MEDIUM
|
||||
impactDescription: reduces reflows/repaints
|
||||
tags: javascript, dom, css, performance, reflow
|
||||
---
|
||||
|
||||
## Batch DOM CSS Changes
|
||||
|
||||
Avoid interleaving style writes with layout reads. When you read a layout property (like `offsetWidth`, `getBoundingClientRect()`, or `getComputedStyle()`) between style changes, the browser is forced to trigger a synchronous reflow.
|
||||
|
||||
**Incorrect (interleaved reads and writes force reflows):**
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
element.style.width = '100px'
|
||||
const width = element.offsetWidth // Forces reflow
|
||||
element.style.height = '200px'
|
||||
const height = element.offsetHeight // Forces another reflow
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (batch writes, then read once):**
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
// Batch all writes together
|
||||
element.style.width = '100px'
|
||||
element.style.height = '200px'
|
||||
element.style.backgroundColor = 'blue'
|
||||
element.style.border = '1px solid black'
|
||||
|
||||
// Read after all writes are done (single reflow)
|
||||
const { width, height } = element.getBoundingClientRect()
|
||||
}
|
||||
```
|
||||
|
||||
**Better: use CSS classes**
|
||||
|
||||
```css
|
||||
.highlighted-box {
|
||||
width: 100px;
|
||||
height: 200px;
|
||||
background-color: blue;
|
||||
border: 1px solid black;
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
function updateElementStyles(element: HTMLElement) {
|
||||
element.classList.add('highlighted-box')
|
||||
|
||||
const { width, height } = element.getBoundingClientRect()
|
||||
}
|
||||
```
|
||||
|
||||
Prefer CSS classes over inline styles when possible. CSS files are cached by the browser, and classes provide better separation of concerns and are easier to maintain.
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/web-design-guidelines
|
||||
1
.codex/skills
Symbolic link
1
.codex/skills
Symbolic link
@@ -0,0 +1 @@
|
||||
../.claude/skills
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/component-refactoring
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/frontend-code-review
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/frontend-testing
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/orpc-contract-first
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user