feat: add 10 bundled skills for UI, quality, and code optimization (#999)
Add community-sourced skills covering: Coding & Quality: - code-optimizer: 13-domain parallel optimization audit - react-best-practices: 57 React/Next.js performance rules (Vercel) - best-practices: Web security, CSP, HTTPS, HTML validity UI & Design: - userinterface-wiki: 152 UI/UX rules (animations, springs, UX laws, typography) - make-interfaces-feel-better: 16 practical polish principles Testing & Audit: - web-quality-audit: Lighthouse-style 150+ checks - accessibility: WCAG guidelines and ARIA patterns - core-web-vitals: LCP/CLS/INP deep dive - web-design-guidelines: Vercel Web Interface Guidelines Tooling: - agent-browser: Browser automation CLI for testing and scraping
This commit is contained in:
parent
642323a489
commit
7868761ca0
257 changed files with 14448 additions and 0 deletions
522
src/resources/skills/accessibility/SKILL.md
Normal file
522
src/resources/skills/accessibility/SKILL.md
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
---
|
||||
name: accessibility
|
||||
description: Audit and improve web accessibility following WCAG 2.1 guidelines. Use when asked to "improve accessibility", "a11y audit", "WCAG compliance", "screen reader support", "keyboard navigation", or "make accessible".
|
||||
license: MIT
|
||||
metadata:
|
||||
author: web-quality-skills
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Accessibility (a11y)
|
||||
|
||||
Comprehensive accessibility guidelines based on WCAG 2.1 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities.
|
||||
|
||||
## WCAG Principles: POUR
|
||||
|
||||
| Principle | Description |
|
||||
|-----------|-------------|
|
||||
| **P**erceivable | Content can be perceived through different senses |
|
||||
| **O**perable | Interface can be operated by all users |
|
||||
| **U**nderstandable | Content and interface are understandable |
|
||||
| **R**obust | Content works with assistive technologies |
|
||||
|
||||
## Conformance levels
|
||||
|
||||
| Level | Requirement | Target |
|
||||
|-------|-------------|--------|
|
||||
| **A** | Minimum accessibility | Must pass |
|
||||
| **AA** | Standard compliance | Should pass (legal requirement in many jurisdictions) |
|
||||
| **AAA** | Enhanced accessibility | Nice to have |
|
||||
|
||||
---
|
||||
|
||||
## Perceivable
|
||||
|
||||
### Text alternatives (1.1)
|
||||
|
||||
**Images require alt text:**
|
||||
```html
|
||||
<!-- ❌ Missing alt -->
|
||||
<img src="chart.png">
|
||||
|
||||
<!-- ✅ Descriptive alt -->
|
||||
<img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales">
|
||||
|
||||
<!-- ✅ Decorative image (empty alt) -->
|
||||
<img src="decorative-border.png" alt="" role="presentation">
|
||||
|
||||
<!-- ✅ Complex image with longer description -->
|
||||
<figure>
|
||||
<img src="infographic.png" alt="2024 market trends infographic"
|
||||
aria-describedby="infographic-desc">
|
||||
<figcaption id="infographic-desc">
|
||||
<!-- Detailed description -->
|
||||
</figcaption>
|
||||
</figure>
|
||||
```
|
||||
|
||||
**Icon buttons need accessible names:**
|
||||
```html
|
||||
<!-- ❌ No accessible name -->
|
||||
<button><svg><!-- menu icon --></svg></button>
|
||||
|
||||
<!-- ✅ Using aria-label -->
|
||||
<button aria-label="Open menu">
|
||||
<svg aria-hidden="true"><!-- menu icon --></svg>
|
||||
</button>
|
||||
|
||||
<!-- ✅ Using visually hidden text -->
|
||||
<button>
|
||||
<svg aria-hidden="true"><!-- menu icon --></svg>
|
||||
<span class="visually-hidden">Open menu</span>
|
||||
</button>
|
||||
```
|
||||
|
||||
**Visually hidden class:**
|
||||
```css
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Color contrast (1.4.3, 1.4.6)
|
||||
|
||||
| Text Size | AA minimum | AAA enhanced |
|
||||
|-----------|------------|--------------|
|
||||
| Normal text (< 18px / < 14px bold) | 4.5:1 | 7:1 |
|
||||
| Large text (≥ 18px / ≥ 14px bold) | 3:1 | 4.5:1 |
|
||||
| UI components & graphics | 3:1 | 3:1 |
|
||||
|
||||
```css
|
||||
/* ❌ Low contrast (2.5:1) */
|
||||
.low-contrast {
|
||||
color: #999;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ✅ Sufficient contrast (7:1) */
|
||||
.high-contrast {
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ✅ Focus states need contrast too */
|
||||
:focus-visible {
|
||||
outline: 2px solid #005fcc;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
**Don't rely on color alone:**
|
||||
```html
|
||||
<!-- ❌ Only color indicates error -->
|
||||
<input class="error-border">
|
||||
<style>.error-border { border-color: red; }</style>
|
||||
|
||||
<!-- ✅ Color + icon + text -->
|
||||
<div class="field-error">
|
||||
<input aria-invalid="true" aria-describedby="email-error">
|
||||
<span id="email-error" class="error-message">
|
||||
<svg aria-hidden="true"><!-- error icon --></svg>
|
||||
Please enter a valid email address
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Media alternatives (1.2)
|
||||
|
||||
```html
|
||||
<!-- Video with captions -->
|
||||
<video controls>
|
||||
<source src="video.mp4" type="video/mp4">
|
||||
<track kind="captions" src="captions.vtt" srclang="en" label="English" default>
|
||||
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descriptions">
|
||||
</video>
|
||||
|
||||
<!-- Audio with transcript -->
|
||||
<audio controls>
|
||||
<source src="podcast.mp3" type="audio/mp3">
|
||||
</audio>
|
||||
<details>
|
||||
<summary>Transcript</summary>
|
||||
<p>Full transcript text...</p>
|
||||
</details>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Operable
|
||||
|
||||
### Keyboard accessible (2.1)
|
||||
|
||||
**All functionality must be keyboard accessible:**
|
||||
```javascript
|
||||
// ❌ Only handles click
|
||||
element.addEventListener('click', handleAction);
|
||||
|
||||
// ✅ Handles both click and keyboard
|
||||
element.addEventListener('click', handleAction);
|
||||
element.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleAction();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**No keyboard traps:**
|
||||
```javascript
|
||||
// Modal focus management
|
||||
function openModal(modal) {
|
||||
const focusableElements = modal.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
// Trap focus within modal
|
||||
modal.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') {
|
||||
if (e.shiftKey && document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
firstElement.focus();
|
||||
}
|
||||
```
|
||||
|
||||
### Focus visible (2.4.7)
|
||||
|
||||
```css
|
||||
/* ❌ Never remove focus outlines */
|
||||
*:focus { outline: none; }
|
||||
|
||||
/* ✅ Use :focus-visible for keyboard-only focus */
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid #005fcc;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ✅ Or custom focus styles */
|
||||
button:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
|
||||
}
|
||||
```
|
||||
|
||||
### Skip links (2.4.1)
|
||||
|
||||
```html
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
<header><!-- navigation --></header>
|
||||
<main id="main-content" tabindex="-1">
|
||||
<!-- main content -->
|
||||
</main>
|
||||
</body>
|
||||
```
|
||||
|
||||
```css
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Timing (2.2)
|
||||
|
||||
```javascript
|
||||
// Allow users to extend time limits
|
||||
function showSessionWarning() {
|
||||
const modal = createModal({
|
||||
title: 'Session Expiring',
|
||||
content: 'Your session will expire in 2 minutes.',
|
||||
actions: [
|
||||
{ label: 'Extend session', action: extendSession },
|
||||
{ label: 'Log out', action: logout }
|
||||
],
|
||||
timeout: 120000 // 2 minutes to respond
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Motion (2.3)
|
||||
|
||||
```css
|
||||
/* Respect reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Understandable
|
||||
|
||||
### Page language (3.1.1)
|
||||
|
||||
```html
|
||||
<!-- ❌ No language specified -->
|
||||
<html>
|
||||
|
||||
<!-- ✅ Language specified -->
|
||||
<html lang="en">
|
||||
|
||||
<!-- ✅ Language changes within page -->
|
||||
<p>The French word for hello is <span lang="fr">bonjour</span>.</p>
|
||||
```
|
||||
|
||||
### Consistent navigation (3.2.3)
|
||||
|
||||
```html
|
||||
<!-- Navigation should be consistent across pages -->
|
||||
<nav aria-label="Main">
|
||||
<ul>
|
||||
<li><a href="/" aria-current="page">Home</a></li>
|
||||
<li><a href="/products">Products</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Form labels (3.3.2)
|
||||
|
||||
```html
|
||||
<!-- ❌ No label association -->
|
||||
<input type="email" placeholder="Email">
|
||||
|
||||
<!-- ✅ Explicit label -->
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email"
|
||||
autocomplete="email" required>
|
||||
|
||||
<!-- ✅ Implicit label -->
|
||||
<label>
|
||||
Email address
|
||||
<input type="email" name="email" autocomplete="email" required>
|
||||
</label>
|
||||
|
||||
<!-- ✅ With instructions -->
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password"
|
||||
aria-describedby="password-requirements">
|
||||
<p id="password-requirements">
|
||||
Must be at least 8 characters with one number.
|
||||
</p>
|
||||
```
|
||||
|
||||
### Error handling (3.3.1, 3.3.3)
|
||||
|
||||
```html
|
||||
<!-- Announce errors to screen readers -->
|
||||
<form novalidate>
|
||||
<div class="field" aria-live="polite">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email"
|
||||
aria-invalid="true"
|
||||
aria-describedby="email-error">
|
||||
<p id="email-error" class="error" role="alert">
|
||||
Please enter a valid email address (e.g., name@example.com)
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Focus first error on submit
|
||||
form.addEventListener('submit', (e) => {
|
||||
const firstError = form.querySelector('[aria-invalid="true"]');
|
||||
if (firstError) {
|
||||
e.preventDefault();
|
||||
firstError.focus();
|
||||
|
||||
// Announce error summary
|
||||
const errorSummary = document.getElementById('error-summary');
|
||||
errorSummary.textContent = `${errors.length} errors found. Please fix them and try again.`;
|
||||
errorSummary.focus();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Robust
|
||||
|
||||
### Valid HTML (4.1.1)
|
||||
|
||||
```html
|
||||
<!-- ❌ Duplicate IDs -->
|
||||
<div id="content">...</div>
|
||||
<div id="content">...</div>
|
||||
|
||||
<!-- ❌ Invalid nesting -->
|
||||
<a href="/"><button>Click</button></a>
|
||||
|
||||
<!-- ✅ Unique IDs -->
|
||||
<div id="main-content">...</div>
|
||||
<div id="sidebar-content">...</div>
|
||||
|
||||
<!-- ✅ Proper nesting -->
|
||||
<a href="/" class="button-link">Click</a>
|
||||
```
|
||||
|
||||
### ARIA usage (4.1.2)
|
||||
|
||||
**Prefer native elements:**
|
||||
```html
|
||||
<!-- ❌ ARIA role on div -->
|
||||
<div role="button" tabindex="0">Click me</div>
|
||||
|
||||
<!-- ✅ Native button -->
|
||||
<button>Click me</button>
|
||||
|
||||
<!-- ❌ ARIA checkbox -->
|
||||
<div role="checkbox" aria-checked="false">Option</div>
|
||||
|
||||
<!-- ✅ Native checkbox -->
|
||||
<label><input type="checkbox"> Option</label>
|
||||
```
|
||||
|
||||
**When ARIA is needed:**
|
||||
```html
|
||||
<!-- Custom tabs component -->
|
||||
<div role="tablist" aria-label="Product information">
|
||||
<button role="tab" id="tab-1" aria-selected="true"
|
||||
aria-controls="panel-1">Description</button>
|
||||
<button role="tab" id="tab-2" aria-selected="false"
|
||||
aria-controls="panel-2" tabindex="-1">Reviews</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
|
||||
<!-- Panel content -->
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
|
||||
<!-- Panel content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Live regions (4.1.3)
|
||||
|
||||
```html
|
||||
<!-- Status updates -->
|
||||
<div aria-live="polite" aria-atomic="true" class="status">
|
||||
<!-- Content updates announced to screen readers -->
|
||||
</div>
|
||||
|
||||
<!-- Urgent alerts -->
|
||||
<div role="alert" aria-live="assertive">
|
||||
<!-- Interrupts current announcement -->
|
||||
</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Announce dynamic content changes
|
||||
function showNotification(message, type = 'polite') {
|
||||
const container = document.getElementById(`${type}-announcer`);
|
||||
container.textContent = ''; // Clear first
|
||||
requestAnimationFrame(() => {
|
||||
container.textContent = message;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
### Automated testing
|
||||
```bash
|
||||
# Lighthouse accessibility audit
|
||||
npx lighthouse https://example.com --only-categories=accessibility
|
||||
|
||||
# axe-core
|
||||
npm install @axe-core/cli -g
|
||||
axe https://example.com
|
||||
```
|
||||
|
||||
### Manual testing
|
||||
|
||||
- [ ] **Keyboard navigation:** Tab through entire page, use Enter/Space to activate
|
||||
- [ ] **Screen reader:** Test with VoiceOver (Mac), NVDA (Windows), or TalkBack (Android)
|
||||
- [ ] **Zoom:** Content usable at 200% zoom
|
||||
- [ ] **High contrast:** Test with Windows High Contrast Mode
|
||||
- [ ] **Reduced motion:** Test with `prefers-reduced-motion: reduce`
|
||||
- [ ] **Focus order:** Logical and follows visual order
|
||||
|
||||
### Screen reader commands
|
||||
|
||||
| Action | VoiceOver (Mac) | NVDA (Windows) |
|
||||
|--------|-----------------|----------------|
|
||||
| Start/Stop | ⌘ + F5 | Ctrl + Alt + N |
|
||||
| Next item | VO + → | ↓ |
|
||||
| Previous item | VO + ← | ↑ |
|
||||
| Activate | VO + Space | Enter |
|
||||
| Headings list | VO + U, then arrows | H / Shift + H |
|
||||
| Links list | VO + U | K / Shift + K |
|
||||
|
||||
---
|
||||
|
||||
## Common issues by impact
|
||||
|
||||
### Critical (fix immediately)
|
||||
1. Missing form labels
|
||||
2. Missing image alt text
|
||||
3. Insufficient color contrast
|
||||
4. Keyboard traps
|
||||
5. No focus indicators
|
||||
|
||||
### Serious (fix before launch)
|
||||
1. Missing page language
|
||||
2. Missing heading structure
|
||||
3. Non-descriptive link text
|
||||
4. Auto-playing media
|
||||
5. Missing skip links
|
||||
|
||||
### Moderate (fix soon)
|
||||
1. Missing ARIA labels on icons
|
||||
2. Inconsistent navigation
|
||||
3. Missing error identification
|
||||
4. Timing without controls
|
||||
5. Missing landmark regions
|
||||
|
||||
## References
|
||||
|
||||
- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
|
||||
- [Deque axe Rules](https://dequeuniversity.com/rules/axe/)
|
||||
- [Web Quality Audit](../web-quality-audit/SKILL.md)
|
||||
162
src/resources/skills/accessibility/references/WCAG.md
Normal file
162
src/resources/skills/accessibility/references/WCAG.md
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# WCAG 2.1 Quick Reference
|
||||
|
||||
## Success criteria by level
|
||||
|
||||
### Level A (minimum)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.1.1** Non-text Content | All images, icons have text alternatives |
|
||||
| **1.2.1** Audio-only/Video-only | Provide transcript or audio description |
|
||||
| **1.2.2** Captions | Video with audio has captions |
|
||||
| **1.2.3** Audio Description | Video has audio description |
|
||||
| **1.3.1** Info and Relationships | Information conveyed through presentation is available programmatically |
|
||||
| **1.3.2** Meaningful Sequence | Reading order is logical |
|
||||
| **1.3.3** Sensory Characteristics | Instructions don't rely solely on shape, color, size, location, orientation, or sound |
|
||||
| **1.4.1** Use of Color | Color is not the only visual means of conveying information |
|
||||
| **1.4.2** Audio Control | Audio playing automatically can be paused/stopped |
|
||||
| **2.1.1** Keyboard | All functionality available via keyboard |
|
||||
| **2.1.2** No Keyboard Trap | Keyboard focus can be moved away from any component |
|
||||
| **2.1.4** Character Key Shortcuts | Single-key shortcuts can be turned off or remapped |
|
||||
| **2.2.1** Timing Adjustable | Time limits can be extended |
|
||||
| **2.2.2** Pause, Stop, Hide | Moving/blinking content can be paused |
|
||||
| **2.3.1** Three Flashes | Nothing flashes more than 3 times per second |
|
||||
| **2.4.1** Bypass Blocks | Skip link or landmark navigation available |
|
||||
| **2.4.2** Page Titled | Pages have descriptive titles |
|
||||
| **2.4.3** Focus Order | Focus order preserves meaning |
|
||||
| **2.4.4** Link Purpose | Link purpose clear from link text or context |
|
||||
| **2.5.1** Pointer Gestures | Multi-point gestures have single-pointer alternatives |
|
||||
| **2.5.2** Pointer Cancellation | Down-event doesn't trigger action (use up-event or click) |
|
||||
| **2.5.3** Label in Name | Accessible name contains visible label text |
|
||||
| **2.5.4** Motion Actuation | Motion-triggered functions have alternatives |
|
||||
| **3.1.1** Language of Page | Default language specified in HTML |
|
||||
| **3.2.1** On Focus | Focus doesn't trigger unexpected changes |
|
||||
| **3.2.2** On Input | Input doesn't trigger unexpected changes |
|
||||
| **3.3.1** Error Identification | Input errors clearly described |
|
||||
| **3.3.2** Labels or Instructions | Form inputs have labels or instructions |
|
||||
| **4.1.1** Parsing | HTML is well-formed (no duplicate IDs, proper nesting) |
|
||||
| **4.1.2** Name, Role, Value | UI components have accessible names and correct roles |
|
||||
|
||||
### Level AA (standard)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.2.4** Captions (Live) | Live audio has captions |
|
||||
| **1.2.5** Audio Description | Pre-recorded video has audio description |
|
||||
| **1.3.4** Orientation | Content doesn't restrict orientation |
|
||||
| **1.3.5** Identify Input Purpose | Input purpose can be programmatically determined |
|
||||
| **1.4.3** Contrast (Minimum) | 4.5:1 for normal text, 3:1 for large text |
|
||||
| **1.4.4** Resize Text | Text can be resized to 200% without loss of functionality |
|
||||
| **1.4.5** Images of Text | Text used instead of images of text |
|
||||
| **1.4.10** Reflow | Content reflows at 320px width without horizontal scroll |
|
||||
| **1.4.11** Non-text Contrast | UI components have 3:1 contrast |
|
||||
| **1.4.12** Text Spacing | Content adapts to text spacing changes |
|
||||
| **1.4.13** Content on Hover/Focus | Additional content is dismissible, hoverable, persistent |
|
||||
| **2.4.5** Multiple Ways | Multiple ways to find pages |
|
||||
| **2.4.6** Headings and Labels | Headings and labels are descriptive |
|
||||
| **2.4.7** Focus Visible | Focus indicator is visible |
|
||||
| **3.1.2** Language of Parts | Language changes are marked |
|
||||
| **3.2.3** Consistent Navigation | Navigation is consistent across pages |
|
||||
| **3.2.4** Consistent Identification | Same functionality uses same labels |
|
||||
| **3.3.3** Error Suggestion | Error corrections suggested when known |
|
||||
| **3.3.4** Error Prevention (Legal) | Actions can be reversed or confirmed |
|
||||
| **4.1.3** Status Messages | Status messages announced to screen readers |
|
||||
|
||||
### Level AAA (enhanced)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.4.6** Contrast (Enhanced) | 7:1 for normal text, 4.5:1 for large text |
|
||||
| **1.4.8** Visual Presentation | Foreground/background colors can be selected |
|
||||
| **1.4.9** Images of Text (No Exception) | No images of text |
|
||||
| **2.1.3** Keyboard (No Exception) | All functionality keyboard accessible |
|
||||
| **2.2.3** No Timing | No time limits |
|
||||
| **2.2.4** Interruptions | Interruptions can be postponed |
|
||||
| **2.2.5** Re-authenticating | Data preserved on re-authentication |
|
||||
| **2.2.6** Timeouts | Users warned about data loss from inactivity |
|
||||
| **2.3.2** Three Flashes | No content flashes more than 3 times |
|
||||
| **2.3.3** Animation from Interactions | Motion animation can be disabled |
|
||||
| **2.4.8** Location | User location within site is available |
|
||||
| **2.4.9** Link Purpose (Link Only) | Link purpose clear from link text alone |
|
||||
| **2.4.10** Section Headings | Sections have headings |
|
||||
| **3.1.3** Unusual Words | Definitions available for unusual words |
|
||||
| **3.1.4** Abbreviations | Abbreviations expanded |
|
||||
| **3.1.5** Reading Level | Alternative content for complex text |
|
||||
| **3.1.6** Pronunciation | Pronunciation available where needed |
|
||||
| **3.2.5** Change on Request | Changes initiated only by user |
|
||||
| **3.3.5** Help | Context-sensitive help available |
|
||||
| **3.3.6** Error Prevention (All) | All form submissions can be reviewed |
|
||||
|
||||
## Common ARIA patterns
|
||||
|
||||
### Buttons
|
||||
```html
|
||||
<button>Label</button>
|
||||
<!-- or -->
|
||||
<button aria-label="Close dialog">×</button>
|
||||
```
|
||||
|
||||
### Links
|
||||
```html
|
||||
<a href="/page">Descriptive link text</a>
|
||||
<!-- External links -->
|
||||
<a href="https://external.com" target="_blank" rel="noopener">
|
||||
External site
|
||||
<span class="visually-hidden">(opens in new tab)</span>
|
||||
</a>
|
||||
```
|
||||
|
||||
### Form fields
|
||||
```html
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" aria-describedby="email-hint">
|
||||
<p id="email-hint">We'll never share your email.</p>
|
||||
```
|
||||
|
||||
### Error states
|
||||
```html
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" aria-invalid="true" aria-describedby="email-error">
|
||||
<p id="email-error" role="alert">Please enter a valid email address.</p>
|
||||
```
|
||||
|
||||
### Navigation
|
||||
```html
|
||||
<nav aria-label="Main">
|
||||
<ul>
|
||||
<li><a href="/" aria-current="page">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Modals
|
||||
```html
|
||||
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
|
||||
<h2 id="dialog-title">Confirm Action</h2>
|
||||
<!-- content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Live regions
|
||||
```html
|
||||
<!-- Polite (waits for pause in speech) -->
|
||||
<div aria-live="polite">Status update here</div>
|
||||
|
||||
<!-- Assertive (interrupts immediately) -->
|
||||
<div aria-live="assertive" role="alert">Error message here</div>
|
||||
|
||||
<!-- Status (polite, implicit) -->
|
||||
<div role="status">Loading complete</div>
|
||||
```
|
||||
|
||||
## Testing tools
|
||||
|
||||
| Tool | Type | URL |
|
||||
|------|------|-----|
|
||||
| axe DevTools | Browser extension | [deque.com/axe](https://www.deque.com/axe/) |
|
||||
| WAVE | Browser extension | [wave.webaim.org](https://wave.webaim.org/) |
|
||||
| Lighthouse | Built into Chrome | DevTools → Lighthouse |
|
||||
| NVDA | Screen reader (Windows) | [nvaccess.org](https://www.nvaccess.org/) |
|
||||
| VoiceOver | Screen reader (Mac) | Built into macOS |
|
||||
| Colour Contrast Analyser | Desktop app | [tpgi.com](https://www.tpgi.com/color-contrast-checker/) |
|
||||
517
src/resources/skills/agent-browser/SKILL.md
Normal file
517
src/resources/skills/agent-browser/SKILL.md
Normal file
|
|
@ -0,0 +1,517 @@
|
|||
---
|
||||
name: agent-browser
|
||||
description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction.
|
||||
allowed-tools: Bash(npx agent-browser:*), Bash(agent-browser:*)
|
||||
---
|
||||
|
||||
# Browser Automation with agent-browser
|
||||
|
||||
## Core Workflow
|
||||
|
||||
Every browser automation follows this pattern:
|
||||
|
||||
1. **Navigate**: `agent-browser open <url>`
|
||||
2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`)
|
||||
3. **Interact**: Use refs to click, fill, select
|
||||
4. **Re-snapshot**: After navigation or DOM changes, get fresh refs
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/form
|
||||
agent-browser snapshot -i
|
||||
# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit"
|
||||
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser snapshot -i # Check result
|
||||
```
|
||||
|
||||
## Command Chaining
|
||||
|
||||
Commands can be chained with `&&` in a single shell invocation. The browser persists between commands via a background daemon, so chaining is safe and more efficient than separate calls.
|
||||
|
||||
```bash
|
||||
# Chain open + wait + snapshot in one call
|
||||
agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser snapshot -i
|
||||
|
||||
# Chain multiple interactions
|
||||
agent-browser fill @e1 "user@example.com" && agent-browser fill @e2 "password123" && agent-browser click @e3
|
||||
|
||||
# Navigate and capture
|
||||
agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png
|
||||
```
|
||||
|
||||
**When to chain:** Use `&&` when you don't need to read the output of an intermediate command before proceeding (e.g., open + wait + screenshot). Run commands separately when you need to parse the output first (e.g., snapshot to discover refs, then interact using those refs).
|
||||
|
||||
## Essential Commands
|
||||
|
||||
```bash
|
||||
# Navigation
|
||||
agent-browser open <url> # Navigate (aliases: goto, navigate)
|
||||
agent-browser close # Close browser
|
||||
|
||||
# Snapshot
|
||||
agent-browser snapshot -i # Interactive elements with refs (recommended)
|
||||
agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, cursor:pointer)
|
||||
agent-browser snapshot -s "#selector" # Scope to CSS selector
|
||||
|
||||
# Interaction (use @refs from snapshot)
|
||||
agent-browser click @e1 # Click element
|
||||
agent-browser click @e1 --new-tab # Click and open in new tab
|
||||
agent-browser fill @e2 "text" # Clear and type text
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser select @e1 "option" # Select dropdown option
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser press Enter # Press key
|
||||
agent-browser keyboard type "text" # Type at current focus (no selector)
|
||||
agent-browser keyboard inserttext "text" # Insert without key events
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scroll down 500 --selector "div.content" # Scroll within a specific container
|
||||
|
||||
# Get information
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get title # Get page title
|
||||
|
||||
# Wait
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait --load networkidle # Wait for network idle
|
||||
agent-browser wait --url "**/page" # Wait for URL pattern
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
|
||||
# Downloads
|
||||
agent-browser download @e1 ./file.pdf # Click element to trigger download
|
||||
agent-browser wait --download ./output.zip # Wait for any download to complete
|
||||
agent-browser --download-path ./downloads open <url> # Set default download directory
|
||||
|
||||
# Capture
|
||||
agent-browser screenshot # Screenshot to temp dir
|
||||
agent-browser screenshot --full # Full page screenshot
|
||||
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
|
||||
# Diff (compare page states)
|
||||
agent-browser diff snapshot # Compare current vs last snapshot
|
||||
agent-browser diff snapshot --baseline before.txt # Compare current vs saved file
|
||||
agent-browser diff screenshot --baseline before.png # Visual pixel diff
|
||||
agent-browser diff url <url1> <url2> # Compare two pages
|
||||
agent-browser diff url <url1> <url2> --wait-until networkidle # Custom wait strategy
|
||||
agent-browser diff url <url1> <url2> --selector "#main" # Scope to element
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Form Submission
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/signup
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "Jane Doe"
|
||||
agent-browser fill @e2 "jane@example.com"
|
||||
agent-browser select @e3 "California"
|
||||
agent-browser check @e4
|
||||
agent-browser click @e5
|
||||
agent-browser wait --load networkidle
|
||||
```
|
||||
|
||||
### Authentication with Auth Vault (Recommended)
|
||||
|
||||
```bash
|
||||
# Save credentials once (encrypted with AGENT_BROWSER_ENCRYPTION_KEY)
|
||||
# Recommended: pipe password via stdin to avoid shell history exposure
|
||||
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
|
||||
|
||||
# Login using saved profile (LLM never sees password)
|
||||
agent-browser auth login github
|
||||
|
||||
# List/show/delete profiles
|
||||
agent-browser auth list
|
||||
agent-browser auth show github
|
||||
agent-browser auth delete github
|
||||
```
|
||||
|
||||
### Authentication with State Persistence
|
||||
|
||||
```bash
|
||||
# Login once and save state
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "$USERNAME"
|
||||
agent-browser fill @e2 "$PASSWORD"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
agent-browser state save auth.json
|
||||
|
||||
# Reuse in future sessions
|
||||
agent-browser state load auth.json
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
### Session Persistence
|
||||
|
||||
```bash
|
||||
# Auto-save/restore cookies and localStorage across browser restarts
|
||||
agent-browser --session-name myapp open https://app.example.com/login
|
||||
# ... login flow ...
|
||||
agent-browser close # State auto-saved to ~/.agent-browser/sessions/
|
||||
|
||||
# Next time, state is auto-loaded
|
||||
agent-browser --session-name myapp open https://app.example.com/dashboard
|
||||
|
||||
# Encrypt state at rest
|
||||
export AGENT_BROWSER_ENCRYPTION_KEY=$(openssl rand -hex 32)
|
||||
agent-browser --session-name secure open https://app.example.com
|
||||
|
||||
# Manage saved states
|
||||
agent-browser state list
|
||||
agent-browser state show myapp-default.json
|
||||
agent-browser state clear myapp
|
||||
agent-browser state clean --older-than 7
|
||||
```
|
||||
|
||||
### Data Extraction
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/products
|
||||
agent-browser snapshot -i
|
||||
agent-browser get text @e5 # Get specific element text
|
||||
agent-browser get text body > page.txt # Get all page text
|
||||
|
||||
# JSON output for parsing
|
||||
agent-browser snapshot -i --json
|
||||
agent-browser get text @e1 --json
|
||||
```
|
||||
|
||||
### Parallel Sessions
|
||||
|
||||
```bash
|
||||
agent-browser --session site1 open https://site-a.com
|
||||
agent-browser --session site2 open https://site-b.com
|
||||
|
||||
agent-browser --session site1 snapshot -i
|
||||
agent-browser --session site2 snapshot -i
|
||||
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
### Connect to Existing Chrome
|
||||
|
||||
```bash
|
||||
# Auto-discover running Chrome with remote debugging enabled
|
||||
agent-browser --auto-connect open https://example.com
|
||||
agent-browser --auto-connect snapshot
|
||||
|
||||
# Or with explicit CDP port
|
||||
agent-browser --cdp 9222 snapshot
|
||||
```
|
||||
|
||||
### Color Scheme (Dark Mode)
|
||||
|
||||
```bash
|
||||
# Persistent dark mode via flag (applies to all pages and new tabs)
|
||||
agent-browser --color-scheme dark open https://example.com
|
||||
|
||||
# Or via environment variable
|
||||
AGENT_BROWSER_COLOR_SCHEME=dark agent-browser open https://example.com
|
||||
|
||||
# Or set during session (persists for subsequent commands)
|
||||
agent-browser set media dark
|
||||
```
|
||||
|
||||
### Visual Browser (Debugging)
|
||||
|
||||
```bash
|
||||
agent-browser --headed open https://example.com
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser record start demo.webm # Record session
|
||||
agent-browser profiler start # Start Chrome DevTools profiling
|
||||
agent-browser profiler stop trace.json # Stop and save profile (path optional)
|
||||
```
|
||||
|
||||
Use `AGENT_BROWSER_HEADED=1` to enable headed mode via environment variable. Browser extensions work in both headed and headless mode.
|
||||
|
||||
### Local Files (PDFs, HTML)
|
||||
|
||||
```bash
|
||||
# Open local files with file:// URLs
|
||||
agent-browser --allow-file-access open file:///path/to/document.pdf
|
||||
agent-browser --allow-file-access open file:///path/to/page.html
|
||||
agent-browser screenshot output.png
|
||||
```
|
||||
|
||||
### iOS Simulator (Mobile Safari)
|
||||
|
||||
```bash
|
||||
# List available iOS simulators
|
||||
agent-browser device list
|
||||
|
||||
# Launch Safari on a specific device
|
||||
agent-browser -p ios --device "iPhone 16 Pro" open https://example.com
|
||||
|
||||
# Same workflow as desktop - snapshot, interact, re-snapshot
|
||||
agent-browser -p ios snapshot -i
|
||||
agent-browser -p ios tap @e1 # Tap (alias for click)
|
||||
agent-browser -p ios fill @e2 "text"
|
||||
agent-browser -p ios swipe up # Mobile-specific gesture
|
||||
|
||||
# Take screenshot
|
||||
agent-browser -p ios screenshot mobile.png
|
||||
|
||||
# Close session (shuts down simulator)
|
||||
agent-browser -p ios close
|
||||
```
|
||||
|
||||
**Requirements:** macOS with Xcode, Appium (`npm install -g appium && appium driver install xcuitest`)
|
||||
|
||||
**Real devices:** Works with physical iOS devices if pre-configured. Use `--device "<UDID>"` where UDID is from `xcrun xctrace list devices`.
|
||||
|
||||
## Security
|
||||
|
||||
All security features are opt-in. By default, agent-browser imposes no restrictions on navigation, actions, or output.
|
||||
|
||||
### Content Boundaries (Recommended for AI Agents)
|
||||
|
||||
Enable `--content-boundaries` to wrap page-sourced output in markers that help LLMs distinguish tool output from untrusted page content:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_CONTENT_BOUNDARIES=1
|
||||
agent-browser snapshot
|
||||
# Output:
|
||||
# --- AGENT_BROWSER_PAGE_CONTENT nonce=<hex> origin=https://example.com ---
|
||||
# [accessibility tree]
|
||||
# --- END_AGENT_BROWSER_PAGE_CONTENT nonce=<hex> ---
|
||||
```
|
||||
|
||||
### Domain Allowlist
|
||||
|
||||
Restrict navigation to trusted domains. Wildcards like `*.example.com` also match the bare domain `example.com`. Sub-resource requests, WebSocket, and EventSource connections to non-allowed domains are also blocked. Include CDN domains your target pages depend on:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_ALLOWED_DOMAINS="example.com,*.example.com"
|
||||
agent-browser open https://example.com # OK
|
||||
agent-browser open https://malicious.com # Blocked
|
||||
```
|
||||
|
||||
### Action Policy
|
||||
|
||||
Use a policy file to gate destructive actions:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_ACTION_POLICY=./policy.json
|
||||
```
|
||||
|
||||
Example `policy.json`:
|
||||
```json
|
||||
{"default": "deny", "allow": ["navigate", "snapshot", "click", "scroll", "wait", "get"]}
|
||||
```
|
||||
|
||||
Auth vault operations (`auth login`, etc.) bypass action policy but domain allowlist still applies.
|
||||
|
||||
### Output Limits
|
||||
|
||||
Prevent context flooding from large pages:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_MAX_OUTPUT=50000
|
||||
```
|
||||
|
||||
## Diffing (Verifying Changes)
|
||||
|
||||
Use `diff snapshot` after performing an action to verify it had the intended effect. This compares the current accessibility tree against the last snapshot taken in the session.
|
||||
|
||||
```bash
|
||||
# Typical workflow: snapshot -> action -> diff
|
||||
agent-browser snapshot -i # Take baseline snapshot
|
||||
agent-browser click @e2 # Perform action
|
||||
agent-browser diff snapshot # See what changed (auto-compares to last snapshot)
|
||||
```
|
||||
|
||||
For visual regression testing or monitoring:
|
||||
|
||||
```bash
|
||||
# Save a baseline screenshot, then compare later
|
||||
agent-browser screenshot baseline.png
|
||||
# ... time passes or changes are made ...
|
||||
agent-browser diff screenshot --baseline baseline.png
|
||||
|
||||
# Compare staging vs production
|
||||
agent-browser diff url https://staging.example.com https://prod.example.com --screenshot
|
||||
```
|
||||
|
||||
`diff snapshot` output uses `+` for additions and `-` for removals, similar to git diff. `diff screenshot` produces a diff image with changed pixels highlighted in red, plus a mismatch percentage.
|
||||
|
||||
## Timeouts and Slow Pages
|
||||
|
||||
The default Playwright timeout is 25 seconds for local browsers. This can be overridden with the `AGENT_BROWSER_DEFAULT_TIMEOUT` environment variable (value in milliseconds). For slow websites or large pages, use explicit waits instead of relying on the default timeout:
|
||||
|
||||
```bash
|
||||
# Wait for network activity to settle (best for slow pages)
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Wait for a specific element to appear
|
||||
agent-browser wait "#content"
|
||||
agent-browser wait @e1
|
||||
|
||||
# Wait for a specific URL pattern (useful after redirects)
|
||||
agent-browser wait --url "**/dashboard"
|
||||
|
||||
# Wait for a JavaScript condition
|
||||
agent-browser wait --fn "document.readyState === 'complete'"
|
||||
|
||||
# Wait a fixed duration (milliseconds) as a last resort
|
||||
agent-browser wait 5000
|
||||
```
|
||||
|
||||
When dealing with consistently slow websites, use `wait --load networkidle` after `open` to ensure the page is fully loaded before taking a snapshot. If a specific element is slow to render, wait for it directly with `wait <selector>` or `wait @ref`.
|
||||
|
||||
## Session Management and Cleanup
|
||||
|
||||
When running multiple agents or automations concurrently, always use named sessions to avoid conflicts:
|
||||
|
||||
```bash
|
||||
# Each agent gets its own isolated session
|
||||
agent-browser --session agent1 open site-a.com
|
||||
agent-browser --session agent2 open site-b.com
|
||||
|
||||
# Check active sessions
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
Always close your browser session when done to avoid leaked processes:
|
||||
|
||||
```bash
|
||||
agent-browser close # Close default session
|
||||
agent-browser --session agent1 close # Close specific session
|
||||
```
|
||||
|
||||
If a previous session was not closed properly, the daemon may still be running. Use `agent-browser close` to clean it up before starting new work.
|
||||
|
||||
## Ref Lifecycle (Important)
|
||||
|
||||
Refs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after:
|
||||
|
||||
- Clicking links or buttons that navigate
|
||||
- Form submissions
|
||||
- Dynamic content loading (dropdowns, modals)
|
||||
|
||||
```bash
|
||||
agent-browser click @e5 # Navigates to new page
|
||||
agent-browser snapshot -i # MUST re-snapshot
|
||||
agent-browser click @e1 # Use new refs
|
||||
```
|
||||
|
||||
## Annotated Screenshots (Vision Mode)
|
||||
|
||||
Use `--annotate` to take a screenshot with numbered labels overlaid on interactive elements. Each label `[N]` maps to ref `@eN`. This also caches refs, so you can interact with elements immediately without a separate snapshot.
|
||||
|
||||
```bash
|
||||
agent-browser screenshot --annotate
|
||||
# Output includes the image path and a legend:
|
||||
# [1] @e1 button "Submit"
|
||||
# [2] @e2 link "Home"
|
||||
# [3] @e3 textbox "Email"
|
||||
agent-browser click @e2 # Click using ref from annotated screenshot
|
||||
```
|
||||
|
||||
Use annotated screenshots when:
|
||||
- The page has unlabeled icon buttons or visual-only elements
|
||||
- You need to verify visual layout or styling
|
||||
- Canvas or chart elements are present (invisible to text snapshots)
|
||||
- You need spatial reasoning about element positions
|
||||
|
||||
## Semantic Locators (Alternative to Refs)
|
||||
|
||||
When refs are unavailable or unreliable, use semantic locators:
|
||||
|
||||
```bash
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find placeholder "Search" type "query"
|
||||
agent-browser find testid "submit-btn" click
|
||||
```
|
||||
|
||||
## JavaScript Evaluation (eval)
|
||||
|
||||
Use `eval` to run JavaScript in the browser context. **Shell quoting can corrupt complex expressions** -- use `--stdin` or `-b` to avoid issues.
|
||||
|
||||
```bash
|
||||
# Simple expressions work with regular quoting
|
||||
agent-browser eval 'document.title'
|
||||
agent-browser eval 'document.querySelectorAll("img").length'
|
||||
|
||||
# Complex JS: use --stdin with heredoc (RECOMMENDED)
|
||||
agent-browser eval --stdin <<'EVALEOF'
|
||||
JSON.stringify(
|
||||
Array.from(document.querySelectorAll("img"))
|
||||
.filter(i => !i.alt)
|
||||
.map(i => ({ src: i.src.split("/").pop(), width: i.width }))
|
||||
)
|
||||
EVALEOF
|
||||
|
||||
# Alternative: base64 encoding (avoids all shell escaping issues)
|
||||
agent-browser eval -b "$(echo -n 'Array.from(document.querySelectorAll("a")).map(a => a.href)' | base64)"
|
||||
```
|
||||
|
||||
**Why this matters:** When the shell processes your command, inner double quotes, `!` characters (history expansion), backticks, and `$()` can all corrupt the JavaScript before it reaches agent-browser. The `--stdin` and `-b` flags bypass shell interpretation entirely.
|
||||
|
||||
**Rules of thumb:**
|
||||
- Single-line, no nested quotes -> regular `eval 'expression'` with single quotes is fine
|
||||
- Nested quotes, arrow functions, template literals, or multiline -> use `eval --stdin <<'EVALEOF'`
|
||||
- Programmatic/generated scripts -> use `eval -b` with base64
|
||||
|
||||
## Configuration File
|
||||
|
||||
Create `agent-browser.json` in the project root for persistent settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"headed": true,
|
||||
"proxy": "http://localhost:8080",
|
||||
"profile": "./browser-data"
|
||||
}
|
||||
```
|
||||
|
||||
Priority (lowest to highest): `~/.agent-browser/config.json` < `./agent-browser.json` < env vars < CLI flags. Use `--config <path>` or `AGENT_BROWSER_CONFIG` env var for a custom config file (exits with error if missing/invalid). All CLI options map to camelCase keys (e.g., `--executable-path` -> `"executablePath"`). Boolean flags accept `true`/`false` values (e.g., `--headed false` overrides config). Extensions from user and project configs are merged, not replaced.
|
||||
|
||||
## Deep-Dive Documentation
|
||||
|
||||
| Reference | When to Use |
|
||||
|-----------|-------------|
|
||||
| [references/commands.md](references/commands.md) | Full command reference with all options |
|
||||
| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting |
|
||||
| [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping |
|
||||
| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse |
|
||||
| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation |
|
||||
| [references/profiling.md](references/profiling.md) | Chrome DevTools profiling for performance analysis |
|
||||
| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies |
|
||||
|
||||
## Experimental: Native Mode
|
||||
|
||||
agent-browser has an experimental native Rust daemon that communicates with Chrome directly via CDP, bypassing Node.js and Playwright entirely. It is opt-in and not recommended for production use yet.
|
||||
|
||||
```bash
|
||||
# Enable via flag
|
||||
agent-browser --native open example.com
|
||||
|
||||
# Enable via environment variable (avoids passing --native every time)
|
||||
export AGENT_BROWSER_NATIVE=1
|
||||
agent-browser open example.com
|
||||
```
|
||||
|
||||
The native daemon supports Chromium and Safari (via WebDriver). Firefox and WebKit are not yet supported. All core commands (navigate, snapshot, click, fill, screenshot, cookies, storage, tabs, eval, etc.) work identically in native mode. Use `agent-browser close` before switching between native and default mode within the same session.
|
||||
|
||||
## Ready-to-Use Templates
|
||||
|
||||
| Template | Description |
|
||||
|----------|-------------|
|
||||
| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation |
|
||||
| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state |
|
||||
| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots |
|
||||
|
||||
```bash
|
||||
./templates/form-automation.sh https://example.com/form
|
||||
./templates/authenticated-session.sh https://app.example.com/login
|
||||
./templates/capture-workflow.sh https://example.com ./output
|
||||
```
|
||||
202
src/resources/skills/agent-browser/references/authentication.md
Normal file
202
src/resources/skills/agent-browser/references/authentication.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# Authentication Patterns
|
||||
|
||||
Login flows, session persistence, OAuth, 2FA, and authenticated browsing.
|
||||
|
||||
**Related**: [session-management.md](session-management.md) for state persistence details, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Login Flow](#basic-login-flow)
|
||||
- [Saving Authentication State](#saving-authentication-state)
|
||||
- [Restoring Authentication](#restoring-authentication)
|
||||
- [OAuth / SSO Flows](#oauth--sso-flows)
|
||||
- [Two-Factor Authentication](#two-factor-authentication)
|
||||
- [HTTP Basic Auth](#http-basic-auth)
|
||||
- [Cookie-Based Auth](#cookie-based-auth)
|
||||
- [Token Refresh Handling](#token-refresh-handling)
|
||||
- [Security Best Practices](#security-best-practices)
|
||||
|
||||
## Basic Login Flow
|
||||
|
||||
```bash
|
||||
# Navigate to login page
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Get form elements
|
||||
agent-browser snapshot -i
|
||||
# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Sign In"
|
||||
|
||||
# Fill credentials
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
|
||||
# Submit
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Verify login succeeded
|
||||
agent-browser get url # Should be dashboard, not login
|
||||
```
|
||||
|
||||
## Saving Authentication State
|
||||
|
||||
After logging in, save state for reuse:
|
||||
|
||||
```bash
|
||||
# Login first (see above)
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
|
||||
# Save authenticated state
|
||||
agent-browser state save ./auth-state.json
|
||||
```
|
||||
|
||||
## Restoring Authentication
|
||||
|
||||
Skip login by loading saved state:
|
||||
|
||||
```bash
|
||||
# Load saved auth state
|
||||
agent-browser state load ./auth-state.json
|
||||
|
||||
# Navigate directly to protected page
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
|
||||
# Verify authenticated
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
## OAuth / SSO Flows
|
||||
|
||||
For OAuth redirects:
|
||||
|
||||
```bash
|
||||
# Start OAuth flow
|
||||
agent-browser open https://app.example.com/auth/google
|
||||
|
||||
# Handle redirects automatically
|
||||
agent-browser wait --url "**/accounts.google.com**"
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Fill Google credentials
|
||||
agent-browser fill @e1 "user@gmail.com"
|
||||
agent-browser click @e2 # Next button
|
||||
agent-browser wait 2000
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e3 "password"
|
||||
agent-browser click @e4 # Sign in
|
||||
|
||||
# Wait for redirect back
|
||||
agent-browser wait --url "**/app.example.com**"
|
||||
agent-browser state save ./oauth-state.json
|
||||
```
|
||||
|
||||
## Two-Factor Authentication
|
||||
|
||||
Handle 2FA with manual intervention:
|
||||
|
||||
```bash
|
||||
# Login with credentials
|
||||
agent-browser open https://app.example.com/login --headed # Show browser
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
|
||||
# Wait for user to complete 2FA manually
|
||||
echo "Complete 2FA in the browser window..."
|
||||
agent-browser wait --url "**/dashboard" --timeout 120000
|
||||
|
||||
# Save state after 2FA
|
||||
agent-browser state save ./2fa-state.json
|
||||
```
|
||||
|
||||
## HTTP Basic Auth
|
||||
|
||||
For sites using HTTP Basic Authentication:
|
||||
|
||||
```bash
|
||||
# Set credentials before navigation
|
||||
agent-browser set credentials username password
|
||||
|
||||
# Navigate to protected resource
|
||||
agent-browser open https://protected.example.com/api
|
||||
```
|
||||
|
||||
## Cookie-Based Auth
|
||||
|
||||
Manually set authentication cookies:
|
||||
|
||||
```bash
|
||||
# Set auth cookie
|
||||
agent-browser cookies set session_token "abc123xyz"
|
||||
|
||||
# Navigate to protected page
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
## Token Refresh Handling
|
||||
|
||||
For sessions with expiring tokens:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Wrapper that handles token refresh
|
||||
|
||||
STATE_FILE="./auth-state.json"
|
||||
|
||||
# Try loading existing state
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
agent-browser state load "$STATE_FILE"
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
|
||||
# Check if session is still valid
|
||||
URL=$(agent-browser get url)
|
||||
if [[ "$URL" == *"/login"* ]]; then
|
||||
echo "Session expired, re-authenticating..."
|
||||
# Perform fresh login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "$USERNAME"
|
||||
agent-browser fill @e2 "$PASSWORD"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
agent-browser state save "$STATE_FILE"
|
||||
fi
|
||||
else
|
||||
# First-time login
|
||||
agent-browser open https://app.example.com/login
|
||||
# ... login flow ...
|
||||
fi
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit state files** - They contain session tokens
|
||||
```bash
|
||||
echo "*.auth-state.json" >> .gitignore
|
||||
```
|
||||
|
||||
2. **Use environment variables for credentials**
|
||||
```bash
|
||||
agent-browser fill @e1 "$APP_USERNAME"
|
||||
agent-browser fill @e2 "$APP_PASSWORD"
|
||||
```
|
||||
|
||||
3. **Clean up after automation**
|
||||
```bash
|
||||
agent-browser cookies clear
|
||||
rm -f ./auth-state.json
|
||||
```
|
||||
|
||||
4. **Use short-lived sessions for CI/CD**
|
||||
```bash
|
||||
# Don't persist state in CI
|
||||
agent-browser open https://app.example.com/login
|
||||
# ... login and perform actions ...
|
||||
agent-browser close # Session ends, nothing persisted
|
||||
```
|
||||
263
src/resources/skills/agent-browser/references/commands.md
Normal file
263
src/resources/skills/agent-browser/references/commands.md
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
# Command Reference
|
||||
|
||||
Complete reference for all agent-browser commands. For quick start and common patterns, see SKILL.md.
|
||||
|
||||
## Navigation
|
||||
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
|
||||
# Supports: https://, http://, file://, about:, data://
|
||||
# Auto-prepends https:// if no protocol given
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser (aliases: quit, exit)
|
||||
agent-browser connect 9222 # Connect to browser via CDP port
|
||||
```
|
||||
|
||||
## Snapshot (page analysis)
|
||||
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact output
|
||||
agent-browser snapshot -d 3 # Limit depth to 3
|
||||
agent-browser snapshot -s "#main" # Scope to CSS selector
|
||||
```
|
||||
|
||||
## Interactions (use @refs from snapshot)
|
||||
|
||||
```bash
|
||||
agent-browser click @e1 # Click
|
||||
agent-browser click @e1 --new-tab # Click and open in new tab
|
||||
agent-browser dblclick @e1 # Double-click
|
||||
agent-browser focus @e1 # Focus element
|
||||
agent-browser fill @e2 "text" # Clear and type
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser press Enter # Press key (alias: key)
|
||||
agent-browser press Control+a # Key combination
|
||||
agent-browser keydown Shift # Hold key down
|
||||
agent-browser keyup Shift # Release key
|
||||
agent-browser hover @e1 # Hover
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser uncheck @e1 # Uncheck checkbox
|
||||
agent-browser select @e1 "value" # Select dropdown option
|
||||
agent-browser select @e1 "a" "b" # Select multiple options
|
||||
agent-browser scroll down 500 # Scroll page (default: down 300px)
|
||||
agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto)
|
||||
agent-browser drag @e1 @e2 # Drag and drop
|
||||
agent-browser upload @e1 file.pdf # Upload files
|
||||
```
|
||||
|
||||
## Get Information
|
||||
|
||||
```bash
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get html @e1 # Get innerHTML
|
||||
agent-browser get value @e1 # Get input value
|
||||
agent-browser get attr @e1 href # Get attribute
|
||||
agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get count ".item" # Count matching elements
|
||||
agent-browser get box @e1 # Get bounding box
|
||||
agent-browser get styles @e1 # Get computed styles (font, color, bg, etc.)
|
||||
```
|
||||
|
||||
## Check State
|
||||
|
||||
```bash
|
||||
agent-browser is visible @e1 # Check if visible
|
||||
agent-browser is enabled @e1 # Check if enabled
|
||||
agent-browser is checked @e1 # Check if checked
|
||||
```
|
||||
|
||||
## Screenshots and PDF
|
||||
|
||||
```bash
|
||||
agent-browser screenshot # Save to temporary directory
|
||||
agent-browser screenshot path.png # Save to specific path
|
||||
agent-browser screenshot --full # Full page
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
```
|
||||
|
||||
## Video Recording
|
||||
|
||||
```bash
|
||||
agent-browser record start ./demo.webm # Start recording
|
||||
agent-browser click @e1 # Perform actions
|
||||
agent-browser record stop # Stop and save video
|
||||
agent-browser record restart ./take2.webm # Stop current + start new
|
||||
```
|
||||
|
||||
## Wait
|
||||
|
||||
```bash
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
agent-browser wait --text "Success" # Wait for text (or -t)
|
||||
agent-browser wait --url "**/dashboard" # Wait for URL pattern (or -u)
|
||||
agent-browser wait --load networkidle # Wait for network idle (or -l)
|
||||
agent-browser wait --fn "window.ready" # Wait for JS condition (or -f)
|
||||
```
|
||||
|
||||
## Mouse Control
|
||||
|
||||
```bash
|
||||
agent-browser mouse move 100 200 # Move mouse
|
||||
agent-browser mouse down left # Press button
|
||||
agent-browser mouse up left # Release button
|
||||
agent-browser mouse wheel 100 # Scroll wheel
|
||||
```
|
||||
|
||||
## Semantic Locators (alternative to refs)
|
||||
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find text "Sign In" click --exact # Exact match only
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
agent-browser find placeholder "Search" type "query"
|
||||
agent-browser find alt "Logo" click
|
||||
agent-browser find title "Close" click
|
||||
agent-browser find testid "submit-btn" click
|
||||
agent-browser find first ".item" click
|
||||
agent-browser find last ".item" click
|
||||
agent-browser find nth 2 "a" hover
|
||||
```
|
||||
|
||||
## Browser Settings
|
||||
|
||||
```bash
|
||||
agent-browser set viewport 1920 1080 # Set viewport size
|
||||
agent-browser set device "iPhone 14" # Emulate device
|
||||
agent-browser set geo 37.7749 -122.4194 # Set geolocation (alias: geolocation)
|
||||
agent-browser set offline on # Toggle offline mode
|
||||
agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers
|
||||
agent-browser set credentials user pass # HTTP basic auth (alias: auth)
|
||||
agent-browser set media dark # Emulate color scheme
|
||||
agent-browser set media light reduced-motion # Light mode + reduced motion
|
||||
```
|
||||
|
||||
## Cookies and Storage
|
||||
|
||||
```bash
|
||||
agent-browser cookies # Get all cookies
|
||||
agent-browser cookies set name value # Set cookie
|
||||
agent-browser cookies clear # Clear cookies
|
||||
agent-browser storage local # Get all localStorage
|
||||
agent-browser storage local key # Get specific key
|
||||
agent-browser storage local set k v # Set value
|
||||
agent-browser storage local clear # Clear all
|
||||
```
|
||||
|
||||
## Network
|
||||
|
||||
```bash
|
||||
agent-browser network route <url> # Intercept requests
|
||||
agent-browser network route <url> --abort # Block requests
|
||||
agent-browser network route <url> --body '{}' # Mock response
|
||||
agent-browser network unroute [url] # Remove routes
|
||||
agent-browser network requests # View tracked requests
|
||||
agent-browser network requests --filter api # Filter requests
|
||||
```
|
||||
|
||||
## Tabs and Windows
|
||||
|
||||
```bash
|
||||
agent-browser tab # List tabs
|
||||
agent-browser tab new [url] # New tab
|
||||
agent-browser tab 2 # Switch to tab by index
|
||||
agent-browser tab close # Close current tab
|
||||
agent-browser tab close 2 # Close tab by index
|
||||
agent-browser window new # New window
|
||||
```
|
||||
|
||||
## Frames
|
||||
|
||||
```bash
|
||||
agent-browser frame "#iframe" # Switch to iframe
|
||||
agent-browser frame main # Back to main frame
|
||||
```
|
||||
|
||||
## Dialogs
|
||||
|
||||
```bash
|
||||
agent-browser dialog accept [text] # Accept dialog
|
||||
agent-browser dialog dismiss # Dismiss dialog
|
||||
```
|
||||
|
||||
## JavaScript
|
||||
|
||||
```bash
|
||||
agent-browser eval "document.title" # Simple expressions only
|
||||
agent-browser eval -b "<base64>" # Any JavaScript (base64 encoded)
|
||||
agent-browser eval --stdin # Read script from stdin
|
||||
```
|
||||
|
||||
Use `-b`/`--base64` or `--stdin` for reliable execution. Shell escaping with nested quotes and special characters is error-prone.
|
||||
|
||||
```bash
|
||||
# Base64 encode your script, then:
|
||||
agent-browser eval -b "ZG9jdW1lbnQucXVlcnlTZWxlY3RvcignW3NyYyo9Il9uZXh0Il0nKQ=="
|
||||
|
||||
# Or use stdin with heredoc for multiline scripts:
|
||||
cat <<'EOF' | agent-browser eval --stdin
|
||||
const links = document.querySelectorAll('a');
|
||||
Array.from(links).map(a => a.href);
|
||||
EOF
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
```bash
|
||||
agent-browser state save auth.json # Save cookies, storage, auth state
|
||||
agent-browser state load auth.json # Restore saved state
|
||||
```
|
||||
|
||||
## Global Options
|
||||
|
||||
```bash
|
||||
agent-browser --session <name> ... # Isolated browser session
|
||||
agent-browser --json ... # JSON output for parsing
|
||||
agent-browser --headed ... # Show browser window (not headless)
|
||||
agent-browser --full ... # Full page screenshot (-f)
|
||||
agent-browser --cdp <port> ... # Connect via Chrome DevTools Protocol
|
||||
agent-browser -p <provider> ... # Cloud browser provider (--provider)
|
||||
agent-browser --proxy <url> ... # Use proxy server
|
||||
agent-browser --proxy-bypass <hosts> # Hosts to bypass proxy
|
||||
agent-browser --headers <json> ... # HTTP headers scoped to URL's origin
|
||||
agent-browser --executable-path <p> # Custom browser executable
|
||||
agent-browser --extension <path> ... # Load browser extension (repeatable)
|
||||
agent-browser --ignore-https-errors # Ignore SSL certificate errors
|
||||
agent-browser --help # Show help (-h)
|
||||
agent-browser --version # Show version (-V)
|
||||
agent-browser <command> --help # Show detailed help for a command
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
agent-browser --headed open example.com # Show browser window
|
||||
agent-browser --cdp 9222 snapshot # Connect via CDP port
|
||||
agent-browser connect 9222 # Alternative: connect command
|
||||
agent-browser console # View console messages
|
||||
agent-browser console --clear # Clear console
|
||||
agent-browser errors # View page errors
|
||||
agent-browser errors --clear # Clear errors
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser trace start # Start recording trace
|
||||
agent-browser trace stop trace.zip # Stop and save trace
|
||||
agent-browser profiler start # Start Chrome DevTools profiling
|
||||
agent-browser profiler stop trace.json # Stop and save profile
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
AGENT_BROWSER_SESSION="mysession" # Default session name
|
||||
AGENT_BROWSER_EXECUTABLE_PATH="/path/chrome" # Custom browser path
|
||||
AGENT_BROWSER_EXTENSIONS="/ext1,/ext2" # Comma-separated extension paths
|
||||
AGENT_BROWSER_PROVIDER="browserbase" # Cloud browser provider
|
||||
AGENT_BROWSER_STREAM_PORT="9223" # WebSocket streaming port
|
||||
AGENT_BROWSER_HOME="/path/to/agent-browser" # Custom install location
|
||||
```
|
||||
120
src/resources/skills/agent-browser/references/profiling.md
Normal file
120
src/resources/skills/agent-browser/references/profiling.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Profiling
|
||||
|
||||
Capture Chrome DevTools performance profiles during browser automation for performance analysis.
|
||||
|
||||
**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Profiling](#basic-profiling)
|
||||
- [Profiler Commands](#profiler-commands)
|
||||
- [Categories](#categories)
|
||||
- [Use Cases](#use-cases)
|
||||
- [Output Format](#output-format)
|
||||
- [Viewing Profiles](#viewing-profiles)
|
||||
- [Limitations](#limitations)
|
||||
|
||||
## Basic Profiling
|
||||
|
||||
```bash
|
||||
# Start profiling
|
||||
agent-browser profiler start
|
||||
|
||||
# Perform actions
|
||||
agent-browser navigate https://example.com
|
||||
agent-browser click "#button"
|
||||
agent-browser wait 1000
|
||||
|
||||
# Stop and save
|
||||
agent-browser profiler stop ./trace.json
|
||||
```
|
||||
|
||||
## Profiler Commands
|
||||
|
||||
```bash
|
||||
# Start profiling with default categories
|
||||
agent-browser profiler start
|
||||
|
||||
# Start with custom trace categories
|
||||
agent-browser profiler start --categories "devtools.timeline,v8.execute,blink.user_timing"
|
||||
|
||||
# Stop profiling and save to file
|
||||
agent-browser profiler stop ./trace.json
|
||||
```
|
||||
|
||||
## Categories
|
||||
|
||||
The `--categories` flag accepts a comma-separated list of Chrome trace categories. Default categories include:
|
||||
|
||||
- `devtools.timeline` -- standard DevTools performance traces
|
||||
- `v8.execute` -- time spent running JavaScript
|
||||
- `blink` -- renderer events
|
||||
- `blink.user_timing` -- `performance.mark()` / `performance.measure()` calls
|
||||
- `latencyInfo` -- input-to-latency tracking
|
||||
- `renderer.scheduler` -- task scheduling and execution
|
||||
- `toplevel` -- broad-spectrum basic events
|
||||
|
||||
Several `disabled-by-default-*` categories are also included for detailed timeline, call stack, and V8 CPU profiling data.
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Diagnosing Slow Page Loads
|
||||
|
||||
```bash
|
||||
agent-browser profiler start
|
||||
agent-browser navigate https://app.example.com
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser profiler stop ./page-load-profile.json
|
||||
```
|
||||
|
||||
### Profiling User Interactions
|
||||
|
||||
```bash
|
||||
agent-browser navigate https://app.example.com
|
||||
agent-browser profiler start
|
||||
agent-browser click "#submit"
|
||||
agent-browser wait 2000
|
||||
agent-browser profiler stop ./interaction-profile.json
|
||||
```
|
||||
|
||||
### CI Performance Regression Checks
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
agent-browser profiler start
|
||||
agent-browser navigate https://app.example.com
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser profiler stop "./profiles/build-${BUILD_ID}.json"
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
The output is a JSON file in Chrome Trace Event format:
|
||||
|
||||
```json
|
||||
{
|
||||
"traceEvents": [
|
||||
{ "cat": "devtools.timeline", "name": "RunTask", "ph": "X", "ts": 12345, "dur": 100, ... },
|
||||
...
|
||||
],
|
||||
"metadata": {
|
||||
"clock-domain": "LINUX_CLOCK_MONOTONIC"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `metadata.clock-domain` field is set based on the host platform (Linux or macOS). On Windows it is omitted.
|
||||
|
||||
## Viewing Profiles
|
||||
|
||||
Load the output JSON file in any of these tools:
|
||||
|
||||
- **Chrome DevTools**: Performance panel > Load profile (Ctrl+Shift+I > Performance)
|
||||
- **Perfetto UI**: https://ui.perfetto.dev/ -- drag and drop the JSON file
|
||||
- **Trace Viewer**: `chrome://tracing` in any Chromium browser
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only works with Chromium-based browsers (Chrome, Edge). Not supported on Firefox or WebKit.
|
||||
- Trace data accumulates in memory while profiling is active (capped at 5 million events). Stop profiling promptly after the area of interest.
|
||||
- Data collection on stop has a 30-second timeout. If the browser is unresponsive, the stop command may fail.
|
||||
194
src/resources/skills/agent-browser/references/proxy-support.md
Normal file
194
src/resources/skills/agent-browser/references/proxy-support.md
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
# Proxy Support
|
||||
|
||||
Proxy configuration for geo-testing, rate limiting avoidance, and corporate environments.
|
||||
|
||||
**Related**: [commands.md](commands.md) for global options, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Proxy Configuration](#basic-proxy-configuration)
|
||||
- [Authenticated Proxy](#authenticated-proxy)
|
||||
- [SOCKS Proxy](#socks-proxy)
|
||||
- [Proxy Bypass](#proxy-bypass)
|
||||
- [Common Use Cases](#common-use-cases)
|
||||
- [Verifying Proxy Connection](#verifying-proxy-connection)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Basic Proxy Configuration
|
||||
|
||||
Use the `--proxy` flag or set proxy via environment variable:
|
||||
|
||||
```bash
|
||||
# Via CLI flag
|
||||
agent-browser --proxy "http://proxy.example.com:8080" open https://example.com
|
||||
|
||||
# Via environment variable
|
||||
export HTTP_PROXY="http://proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
|
||||
# HTTPS proxy
|
||||
export HTTPS_PROXY="https://proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
|
||||
# Both
|
||||
export HTTP_PROXY="http://proxy.example.com:8080"
|
||||
export HTTPS_PROXY="http://proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
## Authenticated Proxy
|
||||
|
||||
For proxies requiring authentication:
|
||||
|
||||
```bash
|
||||
# Include credentials in URL
|
||||
export HTTP_PROXY="http://username:password@proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
## SOCKS Proxy
|
||||
|
||||
```bash
|
||||
# SOCKS5 proxy
|
||||
export ALL_PROXY="socks5://proxy.example.com:1080"
|
||||
agent-browser open https://example.com
|
||||
|
||||
# SOCKS5 with auth
|
||||
export ALL_PROXY="socks5://user:pass@proxy.example.com:1080"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
## Proxy Bypass
|
||||
|
||||
Skip proxy for specific domains using `--proxy-bypass` or `NO_PROXY`:
|
||||
|
||||
```bash
|
||||
# Via CLI flag
|
||||
agent-browser --proxy "http://proxy.example.com:8080" --proxy-bypass "localhost,*.internal.com" open https://example.com
|
||||
|
||||
# Via environment variable
|
||||
export NO_PROXY="localhost,127.0.0.1,.internal.company.com"
|
||||
agent-browser open https://internal.company.com # Direct connection
|
||||
agent-browser open https://external.com # Via proxy
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Geo-Location Testing
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Test site from different regions using geo-located proxies
|
||||
|
||||
PROXIES=(
|
||||
"http://us-proxy.example.com:8080"
|
||||
"http://eu-proxy.example.com:8080"
|
||||
"http://asia-proxy.example.com:8080"
|
||||
)
|
||||
|
||||
for proxy in "${PROXIES[@]}"; do
|
||||
export HTTP_PROXY="$proxy"
|
||||
export HTTPS_PROXY="$proxy"
|
||||
|
||||
region=$(echo "$proxy" | grep -oP '^\w+-\w+')
|
||||
echo "Testing from: $region"
|
||||
|
||||
agent-browser --session "$region" open https://example.com
|
||||
agent-browser --session "$region" screenshot "./screenshots/$region.png"
|
||||
agent-browser --session "$region" close
|
||||
done
|
||||
```
|
||||
|
||||
### Rotating Proxies for Scraping
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Rotate through proxy list to avoid rate limiting
|
||||
|
||||
PROXY_LIST=(
|
||||
"http://proxy1.example.com:8080"
|
||||
"http://proxy2.example.com:8080"
|
||||
"http://proxy3.example.com:8080"
|
||||
)
|
||||
|
||||
URLS=(
|
||||
"https://site.com/page1"
|
||||
"https://site.com/page2"
|
||||
"https://site.com/page3"
|
||||
)
|
||||
|
||||
for i in "${!URLS[@]}"; do
|
||||
proxy_index=$((i % ${#PROXY_LIST[@]}))
|
||||
export HTTP_PROXY="${PROXY_LIST[$proxy_index]}"
|
||||
export HTTPS_PROXY="${PROXY_LIST[$proxy_index]}"
|
||||
|
||||
agent-browser open "${URLS[$i]}"
|
||||
agent-browser get text body > "output-$i.txt"
|
||||
agent-browser close
|
||||
|
||||
sleep 1 # Polite delay
|
||||
done
|
||||
```
|
||||
|
||||
### Corporate Network Access
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Access internal sites via corporate proxy
|
||||
|
||||
export HTTP_PROXY="http://corpproxy.company.com:8080"
|
||||
export HTTPS_PROXY="http://corpproxy.company.com:8080"
|
||||
export NO_PROXY="localhost,127.0.0.1,.company.com"
|
||||
|
||||
# External sites go through proxy
|
||||
agent-browser open https://external-vendor.com
|
||||
|
||||
# Internal sites bypass proxy
|
||||
agent-browser open https://intranet.company.com
|
||||
```
|
||||
|
||||
## Verifying Proxy Connection
|
||||
|
||||
```bash
|
||||
# Check your apparent IP
|
||||
agent-browser open https://httpbin.org/ip
|
||||
agent-browser get text body
|
||||
# Should show proxy's IP, not your real IP
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Proxy Connection Failed
|
||||
|
||||
```bash
|
||||
# Test proxy connectivity first
|
||||
curl -x http://proxy.example.com:8080 https://httpbin.org/ip
|
||||
|
||||
# Check if proxy requires auth
|
||||
export HTTP_PROXY="http://user:pass@proxy.example.com:8080"
|
||||
```
|
||||
|
||||
### SSL/TLS Errors Through Proxy
|
||||
|
||||
Some proxies perform SSL inspection. If you encounter certificate errors:
|
||||
|
||||
```bash
|
||||
# For testing only - not recommended for production
|
||||
agent-browser open https://example.com --ignore-https-errors
|
||||
```
|
||||
|
||||
### Slow Performance
|
||||
|
||||
```bash
|
||||
# Use proxy only when necessary
|
||||
export NO_PROXY="*.cdn.com,*.static.com" # Direct CDN access
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use environment variables** - Don't hardcode proxy credentials
|
||||
2. **Set NO_PROXY appropriately** - Avoid routing local traffic through proxy
|
||||
3. **Test proxy before automation** - Verify connectivity with simple requests
|
||||
4. **Handle proxy failures gracefully** - Implement retry logic for unstable proxies
|
||||
5. **Rotate proxies for large scraping jobs** - Distribute load and avoid bans
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
# Session Management
|
||||
|
||||
Multiple isolated browser sessions with state persistence and concurrent browsing.
|
||||
|
||||
**Related**: [authentication.md](authentication.md) for login patterns, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Named Sessions](#named-sessions)
|
||||
- [Session Isolation Properties](#session-isolation-properties)
|
||||
- [Session State Persistence](#session-state-persistence)
|
||||
- [Common Patterns](#common-patterns)
|
||||
- [Default Session](#default-session)
|
||||
- [Session Cleanup](#session-cleanup)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Named Sessions
|
||||
|
||||
Use `--session` flag to isolate browser contexts:
|
||||
|
||||
```bash
|
||||
# Session 1: Authentication flow
|
||||
agent-browser --session auth open https://app.example.com/login
|
||||
|
||||
# Session 2: Public browsing (separate cookies, storage)
|
||||
agent-browser --session public open https://example.com
|
||||
|
||||
# Commands are isolated by session
|
||||
agent-browser --session auth fill @e1 "user@example.com"
|
||||
agent-browser --session public get text body
|
||||
```
|
||||
|
||||
## Session Isolation Properties
|
||||
|
||||
Each session has independent:
|
||||
- Cookies
|
||||
- LocalStorage / SessionStorage
|
||||
- IndexedDB
|
||||
- Cache
|
||||
- Browsing history
|
||||
- Open tabs
|
||||
|
||||
## Session State Persistence
|
||||
|
||||
### Save Session State
|
||||
|
||||
```bash
|
||||
# Save cookies, storage, and auth state
|
||||
agent-browser state save /path/to/auth-state.json
|
||||
```
|
||||
|
||||
### Load Session State
|
||||
|
||||
```bash
|
||||
# Restore saved state
|
||||
agent-browser state load /path/to/auth-state.json
|
||||
|
||||
# Continue with authenticated session
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
### State File Contents
|
||||
|
||||
```json
|
||||
{
|
||||
"cookies": [...],
|
||||
"localStorage": {...},
|
||||
"sessionStorage": {...},
|
||||
"origins": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Authenticated Session Reuse
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Save login state once, reuse many times
|
||||
|
||||
STATE_FILE="/tmp/auth-state.json"
|
||||
|
||||
# Check if we have saved state
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
agent-browser state load "$STATE_FILE"
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
else
|
||||
# Perform login
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "$USERNAME"
|
||||
agent-browser fill @e2 "$PASSWORD"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Save for future use
|
||||
agent-browser state save "$STATE_FILE"
|
||||
fi
|
||||
```
|
||||
|
||||
### Concurrent Scraping
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Scrape multiple sites concurrently
|
||||
|
||||
# Start all sessions
|
||||
agent-browser --session site1 open https://site1.com &
|
||||
agent-browser --session site2 open https://site2.com &
|
||||
agent-browser --session site3 open https://site3.com &
|
||||
wait
|
||||
|
||||
# Extract from each
|
||||
agent-browser --session site1 get text body > site1.txt
|
||||
agent-browser --session site2 get text body > site2.txt
|
||||
agent-browser --session site3 get text body > site3.txt
|
||||
|
||||
# Cleanup
|
||||
agent-browser --session site1 close
|
||||
agent-browser --session site2 close
|
||||
agent-browser --session site3 close
|
||||
```
|
||||
|
||||
### A/B Testing Sessions
|
||||
|
||||
```bash
|
||||
# Test different user experiences
|
||||
agent-browser --session variant-a open "https://app.com?variant=a"
|
||||
agent-browser --session variant-b open "https://app.com?variant=b"
|
||||
|
||||
# Compare
|
||||
agent-browser --session variant-a screenshot /tmp/variant-a.png
|
||||
agent-browser --session variant-b screenshot /tmp/variant-b.png
|
||||
```
|
||||
|
||||
## Default Session
|
||||
|
||||
When `--session` is omitted, commands use the default session:
|
||||
|
||||
```bash
|
||||
# These use the same default session
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser close # Closes default session
|
||||
```
|
||||
|
||||
## Session Cleanup
|
||||
|
||||
```bash
|
||||
# Close specific session
|
||||
agent-browser --session auth close
|
||||
|
||||
# List active sessions
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Name Sessions Semantically
|
||||
|
||||
```bash
|
||||
# GOOD: Clear purpose
|
||||
agent-browser --session github-auth open https://github.com
|
||||
agent-browser --session docs-scrape open https://docs.example.com
|
||||
|
||||
# AVOID: Generic names
|
||||
agent-browser --session s1 open https://github.com
|
||||
```
|
||||
|
||||
### 2. Always Clean Up
|
||||
|
||||
```bash
|
||||
# Close sessions when done
|
||||
agent-browser --session auth close
|
||||
agent-browser --session scrape close
|
||||
```
|
||||
|
||||
### 3. Handle State Files Securely
|
||||
|
||||
```bash
|
||||
# Don't commit state files (contain auth tokens!)
|
||||
echo "*.auth-state.json" >> .gitignore
|
||||
|
||||
# Delete after use
|
||||
rm /tmp/auth-state.json
|
||||
```
|
||||
|
||||
### 4. Timeout Long Sessions
|
||||
|
||||
```bash
|
||||
# Set timeout for automated scripts
|
||||
timeout 60 agent-browser --session long-task get text body
|
||||
```
|
||||
194
src/resources/skills/agent-browser/references/snapshot-refs.md
Normal file
194
src/resources/skills/agent-browser/references/snapshot-refs.md
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
# Snapshot and Refs
|
||||
|
||||
Compact element references that reduce context usage dramatically for AI agents.
|
||||
|
||||
**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [How Refs Work](#how-refs-work)
|
||||
- [Snapshot Command](#the-snapshot-command)
|
||||
- [Using Refs](#using-refs)
|
||||
- [Ref Lifecycle](#ref-lifecycle)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Ref Notation Details](#ref-notation-details)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## How Refs Work
|
||||
|
||||
Traditional approach:
|
||||
```
|
||||
Full DOM/HTML → AI parses → CSS selector → Action (~3000-5000 tokens)
|
||||
```
|
||||
|
||||
agent-browser approach:
|
||||
```
|
||||
Compact snapshot → @refs assigned → Direct interaction (~200-400 tokens)
|
||||
```
|
||||
|
||||
## The Snapshot Command
|
||||
|
||||
```bash
|
||||
# Basic snapshot (shows page structure)
|
||||
agent-browser snapshot
|
||||
|
||||
# Interactive snapshot (-i flag) - RECOMMENDED
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
### Snapshot Output Format
|
||||
|
||||
```
|
||||
Page: Example Site - Home
|
||||
URL: https://example.com
|
||||
|
||||
@e1 [header]
|
||||
@e2 [nav]
|
||||
@e3 [a] "Home"
|
||||
@e4 [a] "Products"
|
||||
@e5 [a] "About"
|
||||
@e6 [button] "Sign In"
|
||||
|
||||
@e7 [main]
|
||||
@e8 [h1] "Welcome"
|
||||
@e9 [form]
|
||||
@e10 [input type="email"] placeholder="Email"
|
||||
@e11 [input type="password"] placeholder="Password"
|
||||
@e12 [button type="submit"] "Log In"
|
||||
|
||||
@e13 [footer]
|
||||
@e14 [a] "Privacy Policy"
|
||||
```
|
||||
|
||||
## Using Refs
|
||||
|
||||
Once you have refs, interact directly:
|
||||
|
||||
```bash
|
||||
# Click the "Sign In" button
|
||||
agent-browser click @e6
|
||||
|
||||
# Fill email input
|
||||
agent-browser fill @e10 "user@example.com"
|
||||
|
||||
# Fill password
|
||||
agent-browser fill @e11 "password123"
|
||||
|
||||
# Submit the form
|
||||
agent-browser click @e12
|
||||
```
|
||||
|
||||
## Ref Lifecycle
|
||||
|
||||
**IMPORTANT**: Refs are invalidated when the page changes!
|
||||
|
||||
```bash
|
||||
# Get initial snapshot
|
||||
agent-browser snapshot -i
|
||||
# @e1 [button] "Next"
|
||||
|
||||
# Click triggers page change
|
||||
agent-browser click @e1
|
||||
|
||||
# MUST re-snapshot to get new refs!
|
||||
agent-browser snapshot -i
|
||||
# @e1 [h1] "Page 2" ← Different element now!
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Snapshot Before Interacting
|
||||
|
||||
```bash
|
||||
# CORRECT
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i # Get refs first
|
||||
agent-browser click @e1 # Use ref
|
||||
|
||||
# WRONG
|
||||
agent-browser open https://example.com
|
||||
agent-browser click @e1 # Ref doesn't exist yet!
|
||||
```
|
||||
|
||||
### 2. Re-Snapshot After Navigation
|
||||
|
||||
```bash
|
||||
agent-browser click @e5 # Navigates to new page
|
||||
agent-browser snapshot -i # Get new refs
|
||||
agent-browser click @e1 # Use new refs
|
||||
```
|
||||
|
||||
### 3. Re-Snapshot After Dynamic Changes
|
||||
|
||||
```bash
|
||||
agent-browser click @e1 # Opens dropdown
|
||||
agent-browser snapshot -i # See dropdown items
|
||||
agent-browser click @e7 # Select item
|
||||
```
|
||||
|
||||
### 4. Snapshot Specific Regions
|
||||
|
||||
For complex pages, snapshot specific areas:
|
||||
|
||||
```bash
|
||||
# Snapshot just the form
|
||||
agent-browser snapshot @e9
|
||||
```
|
||||
|
||||
## Ref Notation Details
|
||||
|
||||
```
|
||||
@e1 [tag type="value"] "text content" placeholder="hint"
|
||||
│ │ │ │ │
|
||||
│ │ │ │ └─ Additional attributes
|
||||
│ │ │ └─ Visible text
|
||||
│ │ └─ Key attributes shown
|
||||
│ └─ HTML tag name
|
||||
└─ Unique ref ID
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
```
|
||||
@e1 [button] "Submit" # Button with text
|
||||
@e2 [input type="email"] # Email input
|
||||
@e3 [input type="password"] # Password input
|
||||
@e4 [a href="/page"] "Link Text" # Anchor link
|
||||
@e5 [select] # Dropdown
|
||||
@e6 [textarea] placeholder="Message" # Text area
|
||||
@e7 [div class="modal"] # Container (when relevant)
|
||||
@e8 [img alt="Logo"] # Image
|
||||
@e9 [checkbox] checked # Checked checkbox
|
||||
@e10 [radio] selected # Selected radio
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Ref not found" Error
|
||||
|
||||
```bash
|
||||
# Ref may have changed - re-snapshot
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
### Element Not Visible in Snapshot
|
||||
|
||||
```bash
|
||||
# Scroll down to reveal element
|
||||
agent-browser scroll down 1000
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Or wait for dynamic content
|
||||
agent-browser wait 1000
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
### Too Many Elements
|
||||
|
||||
```bash
|
||||
# Snapshot specific container
|
||||
agent-browser snapshot @e5
|
||||
|
||||
# Or use get text for content-only extraction
|
||||
agent-browser get text @e5
|
||||
```
|
||||
173
src/resources/skills/agent-browser/references/video-recording.md
Normal file
173
src/resources/skills/agent-browser/references/video-recording.md
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# Video Recording
|
||||
|
||||
Capture browser automation as video for debugging, documentation, or verification.
|
||||
|
||||
**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Recording](#basic-recording)
|
||||
- [Recording Commands](#recording-commands)
|
||||
- [Use Cases](#use-cases)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Output Format](#output-format)
|
||||
- [Limitations](#limitations)
|
||||
|
||||
## Basic Recording
|
||||
|
||||
```bash
|
||||
# Start recording
|
||||
agent-browser record start ./demo.webm
|
||||
|
||||
# Perform actions
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser click @e1
|
||||
agent-browser fill @e2 "test input"
|
||||
|
||||
# Stop and save
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
## Recording Commands
|
||||
|
||||
```bash
|
||||
# Start recording to file
|
||||
agent-browser record start ./output.webm
|
||||
|
||||
# Stop current recording
|
||||
agent-browser record stop
|
||||
|
||||
# Restart with new file (stops current + starts new)
|
||||
agent-browser record restart ./take2.webm
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Debugging Failed Automation
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Record automation for debugging
|
||||
|
||||
agent-browser record start ./debug-$(date +%Y%m%d-%H%M%S).webm
|
||||
|
||||
# Run your automation
|
||||
agent-browser open https://app.example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser click @e1 || {
|
||||
echo "Click failed - check recording"
|
||||
agent-browser record stop
|
||||
exit 1
|
||||
}
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
### Documentation Generation
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Record workflow for documentation
|
||||
|
||||
agent-browser record start ./docs/how-to-login.webm
|
||||
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser wait 1000 # Pause for visibility
|
||||
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "demo@example.com"
|
||||
agent-browser wait 500
|
||||
|
||||
agent-browser fill @e2 "password"
|
||||
agent-browser wait 500
|
||||
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser wait 1000 # Show result
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
### CI/CD Test Evidence
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Record E2E test runs for CI artifacts
|
||||
|
||||
TEST_NAME="${1:-e2e-test}"
|
||||
RECORDING_DIR="./test-recordings"
|
||||
mkdir -p "$RECORDING_DIR"
|
||||
|
||||
agent-browser record start "$RECORDING_DIR/$TEST_NAME-$(date +%s).webm"
|
||||
|
||||
# Run test
|
||||
if run_e2e_test; then
|
||||
echo "Test passed"
|
||||
else
|
||||
echo "Test failed - recording saved"
|
||||
fi
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Add Pauses for Clarity
|
||||
|
||||
```bash
|
||||
# Slow down for human viewing
|
||||
agent-browser click @e1
|
||||
agent-browser wait 500 # Let viewer see result
|
||||
```
|
||||
|
||||
### 2. Use Descriptive Filenames
|
||||
|
||||
```bash
|
||||
# Include context in filename
|
||||
agent-browser record start ./recordings/login-flow-2024-01-15.webm
|
||||
agent-browser record start ./recordings/checkout-test-run-42.webm
|
||||
```
|
||||
|
||||
### 3. Handle Recording in Error Cases
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cleanup() {
|
||||
agent-browser record stop 2>/dev/null || true
|
||||
agent-browser close 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
agent-browser record start ./automation.webm
|
||||
# ... automation steps ...
|
||||
```
|
||||
|
||||
### 4. Combine with Screenshots
|
||||
|
||||
```bash
|
||||
# Record video AND capture key frames
|
||||
agent-browser record start ./flow.webm
|
||||
|
||||
agent-browser open https://example.com
|
||||
agent-browser screenshot ./screenshots/step1-homepage.png
|
||||
|
||||
agent-browser click @e1
|
||||
agent-browser screenshot ./screenshots/step2-after-click.png
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
- Default format: WebM (VP8/VP9 codec)
|
||||
- Compatible with all modern browsers and video players
|
||||
- Compressed but high quality
|
||||
|
||||
## Limitations
|
||||
|
||||
- Recording adds slight overhead to automation
|
||||
- Large recordings can consume significant disk space
|
||||
- Some headless environments may have codec limitations
|
||||
105
src/resources/skills/agent-browser/templates/authenticated-session.sh
Executable file
105
src/resources/skills/agent-browser/templates/authenticated-session.sh
Executable file
|
|
@ -0,0 +1,105 @@
|
|||
#!/bin/bash
|
||||
# Template: Authenticated Session Workflow
|
||||
# Purpose: Login once, save state, reuse for subsequent runs
|
||||
# Usage: ./authenticated-session.sh <login-url> [state-file]
|
||||
#
|
||||
# RECOMMENDED: Use the auth vault instead of this template:
|
||||
# echo "<pass>" | agent-browser auth save myapp --url <login-url> --username <user> --password-stdin
|
||||
# agent-browser auth login myapp
|
||||
# The auth vault stores credentials securely and the LLM never sees passwords.
|
||||
#
|
||||
# Environment variables:
|
||||
# APP_USERNAME - Login username/email
|
||||
# APP_PASSWORD - Login password
|
||||
#
|
||||
# Two modes:
|
||||
# 1. Discovery mode (default): Shows form structure so you can identify refs
|
||||
# 2. Login mode: Performs actual login after you update the refs
|
||||
#
|
||||
# Setup steps:
|
||||
# 1. Run once to see form structure (discovery mode)
|
||||
# 2. Update refs in LOGIN FLOW section below
|
||||
# 3. Set APP_USERNAME and APP_PASSWORD
|
||||
# 4. Delete the DISCOVERY section
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LOGIN_URL="${1:?Usage: $0 <login-url> [state-file]}"
|
||||
STATE_FILE="${2:-./auth-state.json}"
|
||||
|
||||
echo "Authentication workflow: $LOGIN_URL"
|
||||
|
||||
# ================================================================
|
||||
# SAVED STATE: Skip login if valid saved state exists
|
||||
# ================================================================
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
echo "Loading saved state from $STATE_FILE..."
|
||||
if agent-browser --state "$STATE_FILE" open "$LOGIN_URL" 2>/dev/null; then
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
CURRENT_URL=$(agent-browser get url)
|
||||
if [[ "$CURRENT_URL" != *"login"* ]] && [[ "$CURRENT_URL" != *"signin"* ]]; then
|
||||
echo "Session restored successfully"
|
||||
agent-browser snapshot -i
|
||||
exit 0
|
||||
fi
|
||||
echo "Session expired, performing fresh login..."
|
||||
agent-browser close 2>/dev/null || true
|
||||
else
|
||||
echo "Failed to load state, re-authenticating..."
|
||||
fi
|
||||
rm -f "$STATE_FILE"
|
||||
fi
|
||||
|
||||
# ================================================================
|
||||
# DISCOVERY MODE: Shows form structure (delete after setup)
|
||||
# ================================================================
|
||||
echo "Opening login page..."
|
||||
agent-browser open "$LOGIN_URL"
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
echo ""
|
||||
echo "Login form structure:"
|
||||
echo "---"
|
||||
agent-browser snapshot -i
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Note the refs: username=@e?, password=@e?, submit=@e?"
|
||||
echo " 2. Update the LOGIN FLOW section below with your refs"
|
||||
echo " 3. Set: export APP_USERNAME='...' APP_PASSWORD='...'"
|
||||
echo " 4. Delete this DISCOVERY MODE section"
|
||||
echo ""
|
||||
agent-browser close
|
||||
exit 0
|
||||
|
||||
# ================================================================
|
||||
# LOGIN FLOW: Uncomment and customize after discovery
|
||||
# ================================================================
|
||||
# : "${APP_USERNAME:?Set APP_USERNAME environment variable}"
|
||||
# : "${APP_PASSWORD:?Set APP_PASSWORD environment variable}"
|
||||
#
|
||||
# agent-browser open "$LOGIN_URL"
|
||||
# agent-browser wait --load networkidle
|
||||
# agent-browser snapshot -i
|
||||
#
|
||||
# # Fill credentials (update refs to match your form)
|
||||
# agent-browser fill @e1 "$APP_USERNAME"
|
||||
# agent-browser fill @e2 "$APP_PASSWORD"
|
||||
# agent-browser click @e3
|
||||
# agent-browser wait --load networkidle
|
||||
#
|
||||
# # Verify login succeeded
|
||||
# FINAL_URL=$(agent-browser get url)
|
||||
# if [[ "$FINAL_URL" == *"login"* ]] || [[ "$FINAL_URL" == *"signin"* ]]; then
|
||||
# echo "Login failed - still on login page"
|
||||
# agent-browser screenshot /tmp/login-failed.png
|
||||
# agent-browser close
|
||||
# exit 1
|
||||
# fi
|
||||
#
|
||||
# # Save state for future runs
|
||||
# echo "Saving state to $STATE_FILE"
|
||||
# agent-browser state save "$STATE_FILE"
|
||||
# echo "Login successful"
|
||||
# agent-browser snapshot -i
|
||||
69
src/resources/skills/agent-browser/templates/capture-workflow.sh
Executable file
69
src/resources/skills/agent-browser/templates/capture-workflow.sh
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
#!/bin/bash
|
||||
# Template: Content Capture Workflow
|
||||
# Purpose: Extract content from web pages (text, screenshots, PDF)
|
||||
# Usage: ./capture-workflow.sh <url> [output-dir]
|
||||
#
|
||||
# Outputs:
|
||||
# - page-full.png: Full page screenshot
|
||||
# - page-structure.txt: Page element structure with refs
|
||||
# - page-text.txt: All text content
|
||||
# - page.pdf: PDF version
|
||||
#
|
||||
# Optional: Load auth state for protected pages
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_URL="${1:?Usage: $0 <url> [output-dir]}"
|
||||
OUTPUT_DIR="${2:-.}"
|
||||
|
||||
echo "Capturing: $TARGET_URL"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Optional: Load authentication state
|
||||
# if [[ -f "./auth-state.json" ]]; then
|
||||
# echo "Loading authentication state..."
|
||||
# agent-browser state load "./auth-state.json"
|
||||
# fi
|
||||
|
||||
# Navigate to target
|
||||
agent-browser open "$TARGET_URL"
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Get metadata
|
||||
TITLE=$(agent-browser get title)
|
||||
URL=$(agent-browser get url)
|
||||
echo "Title: $TITLE"
|
||||
echo "URL: $URL"
|
||||
|
||||
# Capture full page screenshot
|
||||
agent-browser screenshot --full "$OUTPUT_DIR/page-full.png"
|
||||
echo "Saved: $OUTPUT_DIR/page-full.png"
|
||||
|
||||
# Get page structure with refs
|
||||
agent-browser snapshot -i > "$OUTPUT_DIR/page-structure.txt"
|
||||
echo "Saved: $OUTPUT_DIR/page-structure.txt"
|
||||
|
||||
# Extract all text content
|
||||
agent-browser get text body > "$OUTPUT_DIR/page-text.txt"
|
||||
echo "Saved: $OUTPUT_DIR/page-text.txt"
|
||||
|
||||
# Save as PDF
|
||||
agent-browser pdf "$OUTPUT_DIR/page.pdf"
|
||||
echo "Saved: $OUTPUT_DIR/page.pdf"
|
||||
|
||||
# Optional: Extract specific elements using refs from structure
|
||||
# agent-browser get text @e5 > "$OUTPUT_DIR/main-content.txt"
|
||||
|
||||
# Optional: Handle infinite scroll pages
|
||||
# for i in {1..5}; do
|
||||
# agent-browser scroll down 1000
|
||||
# agent-browser wait 1000
|
||||
# done
|
||||
# agent-browser screenshot --full "$OUTPUT_DIR/page-scrolled.png"
|
||||
|
||||
# Cleanup
|
||||
agent-browser close
|
||||
|
||||
echo ""
|
||||
echo "Capture complete:"
|
||||
ls -la "$OUTPUT_DIR"
|
||||
62
src/resources/skills/agent-browser/templates/form-automation.sh
Executable file
62
src/resources/skills/agent-browser/templates/form-automation.sh
Executable file
|
|
@ -0,0 +1,62 @@
|
|||
#!/bin/bash
|
||||
# Template: Form Automation Workflow
|
||||
# Purpose: Fill and submit web forms with validation
|
||||
# Usage: ./form-automation.sh <form-url>
|
||||
#
|
||||
# This template demonstrates the snapshot-interact-verify pattern:
|
||||
# 1. Navigate to form
|
||||
# 2. Snapshot to get element refs
|
||||
# 3. Fill fields using refs
|
||||
# 4. Submit and verify result
|
||||
#
|
||||
# Customize: Update the refs (@e1, @e2, etc.) based on your form's snapshot output
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FORM_URL="${1:?Usage: $0 <form-url>}"
|
||||
|
||||
echo "Form automation: $FORM_URL"
|
||||
|
||||
# Step 1: Navigate to form
|
||||
agent-browser open "$FORM_URL"
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Step 2: Snapshot to discover form elements
|
||||
echo ""
|
||||
echo "Form structure:"
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Step 3: Fill form fields (customize these refs based on snapshot output)
|
||||
#
|
||||
# Common field types:
|
||||
# agent-browser fill @e1 "John Doe" # Text input
|
||||
# agent-browser fill @e2 "user@example.com" # Email input
|
||||
# agent-browser fill @e3 "SecureP@ss123" # Password input
|
||||
# agent-browser select @e4 "Option Value" # Dropdown
|
||||
# agent-browser check @e5 # Checkbox
|
||||
# agent-browser click @e6 # Radio button
|
||||
# agent-browser fill @e7 "Multi-line text" # Textarea
|
||||
# agent-browser upload @e8 /path/to/file.pdf # File upload
|
||||
#
|
||||
# Uncomment and modify:
|
||||
# agent-browser fill @e1 "Test User"
|
||||
# agent-browser fill @e2 "test@example.com"
|
||||
# agent-browser click @e3 # Submit button
|
||||
|
||||
# Step 4: Wait for submission
|
||||
# agent-browser wait --load networkidle
|
||||
# agent-browser wait --url "**/success" # Or wait for redirect
|
||||
|
||||
# Step 5: Verify result
|
||||
echo ""
|
||||
echo "Result:"
|
||||
agent-browser get url
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Optional: Capture evidence
|
||||
agent-browser screenshot /tmp/form-result.png
|
||||
echo "Screenshot saved: /tmp/form-result.png"
|
||||
|
||||
# Cleanup
|
||||
agent-browser close
|
||||
echo "Done"
|
||||
583
src/resources/skills/best-practices/SKILL.md
Normal file
583
src/resources/skills/best-practices/SKILL.md
Normal file
|
|
@ -0,0 +1,583 @@
|
|||
---
|
||||
name: best-practices
|
||||
description: Apply modern web development best practices for security, compatibility, and code quality. Use when asked to "apply best practices", "security audit", "modernize code", "code quality review", or "check for vulnerabilities".
|
||||
license: MIT
|
||||
metadata:
|
||||
author: web-quality-skills
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Best practices
|
||||
|
||||
Modern web development standards based on Lighthouse best practices audits. Covers security, browser compatibility, and code quality patterns.
|
||||
|
||||
## Security
|
||||
|
||||
### HTTPS everywhere
|
||||
|
||||
**Enforce HTTPS:**
|
||||
```html
|
||||
<!-- ❌ Mixed content -->
|
||||
<img src="http://example.com/image.jpg">
|
||||
<script src="http://cdn.example.com/script.js"></script>
|
||||
|
||||
<!-- ✅ HTTPS only -->
|
||||
<img src="https://example.com/image.jpg">
|
||||
<script src="https://cdn.example.com/script.js"></script>
|
||||
|
||||
<!-- ✅ Protocol-relative (will use page's protocol) -->
|
||||
<img src="//example.com/image.jpg">
|
||||
```
|
||||
|
||||
**HSTS Header:**
|
||||
```
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
||||
```
|
||||
|
||||
### Content Security Policy (CSP)
|
||||
|
||||
```html
|
||||
<!-- Basic CSP via meta tag -->
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self';
|
||||
script-src 'self' https://trusted-cdn.com;
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: https:;
|
||||
connect-src 'self' https://api.example.com;">
|
||||
|
||||
<!-- Better: HTTP header -->
|
||||
```
|
||||
|
||||
**CSP Header (recommended):**
|
||||
```
|
||||
Content-Security-Policy:
|
||||
default-src 'self';
|
||||
script-src 'self' 'nonce-abc123' https://trusted.com;
|
||||
style-src 'self' 'nonce-abc123';
|
||||
img-src 'self' data: https:;
|
||||
connect-src 'self' https://api.example.com;
|
||||
frame-ancestors 'self';
|
||||
base-uri 'self';
|
||||
form-action 'self';
|
||||
```
|
||||
|
||||
**Using nonces for inline scripts:**
|
||||
```html
|
||||
<script nonce="abc123">
|
||||
// This inline script is allowed
|
||||
</script>
|
||||
```
|
||||
|
||||
### Security headers
|
||||
|
||||
```
|
||||
# Prevent clickjacking
|
||||
X-Frame-Options: DENY
|
||||
|
||||
# Prevent MIME type sniffing
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
# Enable XSS filter (legacy browsers)
|
||||
X-XSS-Protection: 1; mode=block
|
||||
|
||||
# Control referrer information
|
||||
Referrer-Policy: strict-origin-when-cross-origin
|
||||
|
||||
# Permissions policy (formerly Feature-Policy)
|
||||
Permissions-Policy: geolocation=(), microphone=(), camera=()
|
||||
```
|
||||
|
||||
### No vulnerable libraries
|
||||
|
||||
```bash
|
||||
# Check for vulnerabilities
|
||||
npm audit
|
||||
yarn audit
|
||||
|
||||
# Auto-fix when possible
|
||||
npm audit fix
|
||||
|
||||
# Check specific package
|
||||
npm ls lodash
|
||||
```
|
||||
|
||||
**Keep dependencies updated:**
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"audit": "npm audit --audit-level=moderate",
|
||||
"update": "npm update && npm audit fix"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Known vulnerable patterns to avoid:**
|
||||
```javascript
|
||||
// ❌ Prototype pollution vulnerable patterns
|
||||
Object.assign(target, userInput);
|
||||
_.merge(target, userInput);
|
||||
|
||||
// ✅ Safer alternatives
|
||||
const safeData = JSON.parse(JSON.stringify(userInput));
|
||||
```
|
||||
|
||||
### Input sanitization
|
||||
|
||||
```javascript
|
||||
// ❌ XSS vulnerable
|
||||
element.innerHTML = userInput;
|
||||
document.write(userInput);
|
||||
|
||||
// ✅ Safe text content
|
||||
element.textContent = userInput;
|
||||
|
||||
// ✅ If HTML needed, sanitize
|
||||
import DOMPurify from 'dompurify';
|
||||
element.innerHTML = DOMPurify.sanitize(userInput);
|
||||
```
|
||||
|
||||
### Secure cookies
|
||||
|
||||
```javascript
|
||||
// ❌ Insecure cookie
|
||||
document.cookie = "session=abc123";
|
||||
|
||||
// ✅ Secure cookie (server-side)
|
||||
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Browser compatibility
|
||||
|
||||
### Doctype declaration
|
||||
|
||||
```html
|
||||
<!-- ❌ Missing or invalid doctype -->
|
||||
<HTML>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
|
||||
|
||||
<!-- ✅ HTML5 doctype -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
```
|
||||
|
||||
### Character encoding
|
||||
|
||||
```html
|
||||
<!-- ❌ Missing or late charset -->
|
||||
<html>
|
||||
<head>
|
||||
<title>Page</title>
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
|
||||
<!-- ✅ Charset as first element in head -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Page</title>
|
||||
</head>
|
||||
```
|
||||
|
||||
### Viewport meta tag
|
||||
|
||||
```html
|
||||
<!-- ❌ Missing viewport -->
|
||||
<head>
|
||||
<title>Page</title>
|
||||
</head>
|
||||
|
||||
<!-- ✅ Responsive viewport -->
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Page</title>
|
||||
</head>
|
||||
```
|
||||
|
||||
### Feature detection
|
||||
|
||||
```javascript
|
||||
// ❌ Browser detection (brittle)
|
||||
if (navigator.userAgent.includes('Chrome')) {
|
||||
// Chrome-specific code
|
||||
}
|
||||
|
||||
// ✅ Feature detection
|
||||
if ('IntersectionObserver' in window) {
|
||||
// Use IntersectionObserver
|
||||
} else {
|
||||
// Fallback
|
||||
}
|
||||
|
||||
// ✅ Using @supports in CSS
|
||||
@supports (display: grid) {
|
||||
.container {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@supports not (display: grid) {
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Polyfills (when needed)
|
||||
|
||||
```html
|
||||
<!-- Load polyfills conditionally -->
|
||||
<script>
|
||||
if (!('fetch' in window)) {
|
||||
document.write('<script src="/polyfills/fetch.js"><\/script>');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Or use polyfill.io -->
|
||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=fetch,IntersectionObserver"></script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deprecated APIs
|
||||
|
||||
### Avoid these
|
||||
|
||||
```javascript
|
||||
// ❌ document.write (blocks parsing)
|
||||
document.write('<script src="..."></script>');
|
||||
|
||||
// ✅ Dynamic script loading
|
||||
const script = document.createElement('script');
|
||||
script.src = '...';
|
||||
document.head.appendChild(script);
|
||||
|
||||
// ❌ Synchronous XHR (blocks main thread)
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, false); // false = synchronous
|
||||
|
||||
// ✅ Async fetch
|
||||
const response = await fetch(url);
|
||||
|
||||
// ❌ Application Cache (deprecated)
|
||||
<html manifest="cache.manifest">
|
||||
|
||||
// ✅ Service Workers
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js');
|
||||
}
|
||||
```
|
||||
|
||||
### Event listener passive
|
||||
|
||||
```javascript
|
||||
// ❌ Non-passive touch/wheel (may block scrolling)
|
||||
element.addEventListener('touchstart', handler);
|
||||
element.addEventListener('wheel', handler);
|
||||
|
||||
// ✅ Passive listeners (allows smooth scrolling)
|
||||
element.addEventListener('touchstart', handler, { passive: true });
|
||||
element.addEventListener('wheel', handler, { passive: true });
|
||||
|
||||
// ✅ If you need preventDefault, be explicit
|
||||
element.addEventListener('touchstart', handler, { passive: false });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Console & errors
|
||||
|
||||
### No console errors
|
||||
|
||||
```javascript
|
||||
// ❌ Errors in production
|
||||
console.log('Debug info'); // Remove in production
|
||||
throw new Error('Unhandled'); // Catch all errors
|
||||
|
||||
// ✅ Proper error handling
|
||||
try {
|
||||
riskyOperation();
|
||||
} catch (error) {
|
||||
// Log to error tracking service
|
||||
errorTracker.captureException(error);
|
||||
// Show user-friendly message
|
||||
showErrorMessage('Something went wrong. Please try again.');
|
||||
}
|
||||
```
|
||||
|
||||
### Error boundaries (React)
|
||||
|
||||
```jsx
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = { hasError: false };
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
errorTracker.captureException(error, { extra: info });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return <FallbackUI />;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### Global error handler
|
||||
|
||||
```javascript
|
||||
// Catch unhandled errors
|
||||
window.addEventListener('error', (event) => {
|
||||
errorTracker.captureException(event.error);
|
||||
});
|
||||
|
||||
// Catch unhandled promise rejections
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
errorTracker.captureException(event.reason);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Source maps
|
||||
|
||||
### Production configuration
|
||||
|
||||
```javascript
|
||||
// ❌ Source maps exposed in production
|
||||
// webpack.config.js
|
||||
module.exports = {
|
||||
devtool: 'source-map', // Exposes source code
|
||||
};
|
||||
|
||||
// ✅ Hidden source maps (uploaded to error tracker)
|
||||
module.exports = {
|
||||
devtool: 'hidden-source-map',
|
||||
};
|
||||
|
||||
// ✅ Or no source maps in production
|
||||
module.exports = {
|
||||
devtool: process.env.NODE_ENV === 'production' ? false : 'source-map',
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance best practices
|
||||
|
||||
### Avoid blocking patterns
|
||||
|
||||
```javascript
|
||||
// ❌ Blocking script
|
||||
<script src="heavy-library.js"></script>
|
||||
|
||||
// ✅ Deferred script
|
||||
<script defer src="heavy-library.js"></script>
|
||||
|
||||
// ❌ Blocking CSS import
|
||||
@import url('other-styles.css');
|
||||
|
||||
// ✅ Link tags (parallel loading)
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="other-styles.css">
|
||||
```
|
||||
|
||||
### Efficient event handlers
|
||||
|
||||
```javascript
|
||||
// ❌ Handler on every element
|
||||
items.forEach(item => {
|
||||
item.addEventListener('click', handleClick);
|
||||
});
|
||||
|
||||
// ✅ Event delegation
|
||||
container.addEventListener('click', (e) => {
|
||||
if (e.target.matches('.item')) {
|
||||
handleClick(e);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Memory management
|
||||
|
||||
```javascript
|
||||
// ❌ Memory leak (never removed)
|
||||
const handler = () => { /* ... */ };
|
||||
window.addEventListener('resize', handler);
|
||||
|
||||
// ✅ Cleanup when done
|
||||
const handler = () => { /* ... */ };
|
||||
window.addEventListener('resize', handler);
|
||||
|
||||
// Later, when component unmounts:
|
||||
window.removeEventListener('resize', handler);
|
||||
|
||||
// ✅ Using AbortController
|
||||
const controller = new AbortController();
|
||||
window.addEventListener('resize', handler, { signal: controller.signal });
|
||||
|
||||
// Cleanup:
|
||||
controller.abort();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code quality
|
||||
|
||||
### Valid HTML
|
||||
|
||||
```html
|
||||
<!-- ❌ Invalid HTML -->
|
||||
<div id="header">
|
||||
<div id="header"> <!-- Duplicate ID -->
|
||||
|
||||
<ul>
|
||||
<div>Item</div> <!-- Invalid child -->
|
||||
</ul>
|
||||
|
||||
<a href="/"><button>Click</button></a> <!-- Invalid nesting -->
|
||||
|
||||
<!-- ✅ Valid HTML -->
|
||||
<header id="site-header">
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
<li>Item</li>
|
||||
</ul>
|
||||
|
||||
<a href="/" class="button">Click</a>
|
||||
```
|
||||
|
||||
### Semantic HTML
|
||||
|
||||
```html
|
||||
<!-- ❌ Non-semantic -->
|
||||
<div class="header">
|
||||
<div class="nav">
|
||||
<div class="nav-item">Home</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="article">
|
||||
<div class="title">Headline</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Semantic HTML5 -->
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<article>
|
||||
<h1>Headline</h1>
|
||||
</article>
|
||||
</main>
|
||||
```
|
||||
|
||||
### Image aspect ratios
|
||||
|
||||
```html
|
||||
<!-- ❌ Distorted images -->
|
||||
<img src="photo.jpg" width="300" height="100">
|
||||
<!-- If actual ratio is 4:3, this squishes the image -->
|
||||
|
||||
<!-- ✅ Preserve aspect ratio -->
|
||||
<img src="photo.jpg" width="300" height="225">
|
||||
<!-- Actual 4:3 dimensions -->
|
||||
|
||||
<!-- ✅ CSS object-fit for flexibility -->
|
||||
<img src="photo.jpg" style="width: 300px; height: 200px; object-fit: cover;">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permissions & privacy
|
||||
|
||||
### Request permissions properly
|
||||
|
||||
```javascript
|
||||
// ❌ Request on page load (bad UX, often denied)
|
||||
navigator.geolocation.getCurrentPosition(success, error);
|
||||
|
||||
// ✅ Request in context, after user action
|
||||
findNearbyButton.addEventListener('click', async () => {
|
||||
// Explain why you need it
|
||||
if (await showPermissionExplanation()) {
|
||||
navigator.geolocation.getCurrentPosition(success, error);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Permissions policy
|
||||
|
||||
```html
|
||||
<!-- Restrict powerful features -->
|
||||
<meta http-equiv="Permissions-Policy"
|
||||
content="geolocation=(), camera=(), microphone=()">
|
||||
|
||||
<!-- Or allow for specific origins -->
|
||||
<meta http-equiv="Permissions-Policy"
|
||||
content="geolocation=(self 'https://maps.example.com')">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Audit checklist
|
||||
|
||||
### Security (critical)
|
||||
- [ ] HTTPS enabled, no mixed content
|
||||
- [ ] No vulnerable dependencies (`npm audit`)
|
||||
- [ ] CSP headers configured
|
||||
- [ ] Security headers present
|
||||
- [ ] No exposed source maps
|
||||
|
||||
### Compatibility
|
||||
- [ ] Valid HTML5 doctype
|
||||
- [ ] Charset declared first in head
|
||||
- [ ] Viewport meta tag present
|
||||
- [ ] No deprecated APIs used
|
||||
- [ ] Passive event listeners for scroll/touch
|
||||
|
||||
### Code quality
|
||||
- [ ] No console errors
|
||||
- [ ] Valid HTML (no duplicate IDs)
|
||||
- [ ] Semantic HTML elements used
|
||||
- [ ] Proper error handling
|
||||
- [ ] Memory cleanup in components
|
||||
|
||||
### UX
|
||||
- [ ] No intrusive interstitials
|
||||
- [ ] Permission requests in context
|
||||
- [ ] Clear error messages
|
||||
- [ ] Appropriate image aspect ratios
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `npm audit` | Dependency vulnerabilities |
|
||||
| [SecurityHeaders.com](https://securityheaders.com) | Header analysis |
|
||||
| [W3C Validator](https://validator.w3.org) | HTML validation |
|
||||
| Lighthouse | Best practices audit |
|
||||
| [Observatory](https://observatory.mozilla.org) | Security scan |
|
||||
|
||||
## References
|
||||
|
||||
- [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security)
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
- [Web Quality Audit](../web-quality-audit/SKILL.md)
|
||||
160
src/resources/skills/code-optimizer/SKILL.md
Normal file
160
src/resources/skills/code-optimizer/SKILL.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
---
|
||||
name: code-optimizer
|
||||
description: >
|
||||
Deep code optimization audit using parallel specialist agents. Each agent hunts for performance
|
||||
anti-patterns, inefficiencies, and suboptimal code using pattern-based detection (Grep/Glob)
|
||||
WITHOUT reading the full source code first — avoiding anchoring bias on existing implementations.
|
||||
Covers ALL optimization domains: database queries, memory leaks, algorithmic complexity,
|
||||
concurrency, bundle size, dead code, I/O & network, rendering/UI, data structures,
|
||||
error handling, caching, build config, security-performance, logging, and infrastructure.
|
||||
Use when asked to: "optimize my code", "find performance issues", "audit code quality",
|
||||
"speed up my app", "find bottlenecks", "code review for performance", "find anti-patterns",
|
||||
"improve code efficiency", "reduce latency", "optimize performance", "code smell detection",
|
||||
"find slow code", "optimize this project", "performance audit", "code optimization".
|
||||
Also triggers on: "optimizar codigo", "encontrar cuellos de botella", "mejorar rendimiento".
|
||||
---
|
||||
|
||||
# Code Optimizer
|
||||
|
||||
Parallel multi-agent code optimization audit. Spawn 13 specialist agents simultaneously, each
|
||||
hunting for a different class of performance problem using pattern-based detection.
|
||||
|
||||
## Critical Principle: No Code Reading Before Analysis
|
||||
|
||||
Agents MUST NOT read source files before searching for patterns. Reading the code first causes
|
||||
anchoring bias — the agent accepts the existing implementation as "reasonable" and misses
|
||||
better alternatives. Instead, each agent:
|
||||
|
||||
1. Read its assigned reference file from `references/` to load detection patterns
|
||||
2. Use Grep/Glob to scan the codebase for anti-patterns
|
||||
3. For each finding, ONLY THEN read the surrounding context (5-10 lines) to confirm the issue
|
||||
4. Propose the optimal solution based on best practices, NOT based on the existing code
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Detect Stack
|
||||
|
||||
Use Glob to identify the project's tech stack:
|
||||
- `**/package.json` → Node.js/JS/TS (check for React, Next.js, Express, etc.)
|
||||
- `**/requirements.txt`, `**/pyproject.toml`, `**/setup.py` → Python
|
||||
- `**/go.mod` → Go
|
||||
- `**/Cargo.toml` → Rust
|
||||
- `**/pom.xml`, `**/build.gradle` → Java
|
||||
- `**/Gemfile` → Ruby
|
||||
- `**/Dockerfile` → Docker
|
||||
- `**/*.sql` → SQL
|
||||
- `**/webpack.config.*`, `**/vite.config.*`, `**/tsconfig.json` → Build tools
|
||||
|
||||
### Step 2: Spawn 13 Parallel Agents
|
||||
|
||||
Launch ALL agents simultaneously using the Agent tool. Each agent receives:
|
||||
- Its domain name and reference file path
|
||||
- The detected tech stack (so it can focus on relevant patterns)
|
||||
- The project root path
|
||||
- Instructions to NOT read code files, only Grep/Glob for patterns
|
||||
|
||||
**Agent definitions** (spawn all 13 in a single message):
|
||||
|
||||
| # | Agent Name | Reference File | Focus |
|
||||
|---|-----------|----------------|-------|
|
||||
| 1 | Database & Queries | `references/database-queries.md` | N+1 queries, SELECT *, missing indexes, ORM misuse, connection pooling |
|
||||
| 2 | Memory & Resources | `references/memory-resources.md` | Memory leaks, unclosed resources, large allocations, string concat in loops |
|
||||
| 3 | Algorithmic Complexity | `references/algorithmic-complexity.md` | O(n^2) patterns, unnecessary iterations, wrong data structures for lookups |
|
||||
| 4 | Concurrency & Async | `references/concurrency-async.md` | Sequential awaits, blocking in async, race conditions, unbounded concurrency |
|
||||
| 5 | Bundle & Dependencies | `references/bundle-dependencies.md` | Heavy imports, unused deps, duplicate libs, missing lazy loading |
|
||||
| 6 | Dead Code & Redundancy | `references/dead-code-redundancy.md` | Unused exports, commented code, dead branches, duplicate logic |
|
||||
| 7 | I/O & Network | `references/io-network.md` | Sequential requests, missing batching, no dedup, missing compression |
|
||||
| 8 | Rendering & UI | `references/rendering-ui.md` | Re-renders, missing virtualization, layout thrashing, animation perf |
|
||||
| 9 | Data Structures | `references/data-structures.md` | Wrong structures, unnecessary copies, inefficient serialization |
|
||||
| 10 | Error & Resilience | `references/error-resilience.md` | Missing timeouts, swallowed errors, no retries, no circuit breakers |
|
||||
| 11 | Caching & Memoization | `references/caching-memoization.md` | Missing memoization, cache without invalidation, redundant API calls |
|
||||
| 12 | Build & Compilation | `references/build-compilation.md` | Dev code in prod, missing optimization flags, slow tests, Docker issues |
|
||||
| 13 | Security-Performance | `references/security-performance.md` | Crypto misuse, missing rate limiting, ReDoS, SQL injection vectors |
|
||||
|
||||
**Optional agents** (spawn if relevant to detected stack):
|
||||
- Logging & Observability (`references/logging-observability.md`) — if logging framework detected
|
||||
- Config & Infrastructure (`references/config-infra.md`) — if Docker/deployment config detected
|
||||
|
||||
### Agent Prompt Template
|
||||
|
||||
Each agent MUST receive this prompt structure:
|
||||
|
||||
```
|
||||
You are a {DOMAIN_NAME} optimization specialist. Your job is to find performance
|
||||
anti-patterns in the codebase at {PROJECT_ROOT}.
|
||||
|
||||
CRITICAL RULES:
|
||||
1. DO NOT read source code files before searching. This avoids anchoring bias.
|
||||
2. First, read your reference file: {SKILL_DIR}/references/{REFERENCE_FILE}
|
||||
3. Use Grep and Glob to search for the patterns described in the reference file.
|
||||
4. Only read 5-10 lines of context around each finding to confirm it's a real issue.
|
||||
5. Skip patterns that don't match the project's stack: {DETECTED_STACK}
|
||||
|
||||
Tech stack detected: {DETECTED_STACK}
|
||||
Project root: {PROJECT_ROOT}
|
||||
|
||||
For each finding, report:
|
||||
- **File**: path:line_number
|
||||
- **Pattern**: what anti-pattern was detected
|
||||
- **Severity**: CRITICAL / HIGH / MEDIUM / LOW
|
||||
- **Current code**: the problematic snippet (keep short)
|
||||
- **Why it's slow**: brief explanation of the performance impact
|
||||
- **Optimal fix**: the recommended solution (code snippet or approach)
|
||||
- **Estimated impact**: qualitative improvement expected (e.g., "10x faster for large lists")
|
||||
|
||||
If you find 0 issues in your domain, report "No issues found" — this is a valid outcome.
|
||||
Sort findings by severity (CRITICAL first).
|
||||
```
|
||||
|
||||
### Step 3: Consolidate Report
|
||||
|
||||
After all agents complete, consolidate their findings into a single prioritized report:
|
||||
|
||||
1. Collect all findings from all agents
|
||||
2. Deduplicate (different agents may flag the same code for different reasons)
|
||||
3. Sort by severity: CRITICAL > HIGH > MEDIUM > LOW
|
||||
4. Group by file (so the user can fix file-by-file)
|
||||
5. Present the final report with:
|
||||
- Executive summary: total findings by severity, top 3 most impactful
|
||||
- Detailed findings table grouped by file
|
||||
- Improvement plan: ordered list of fixes from highest to lowest impact
|
||||
|
||||
### Report Format
|
||||
|
||||
```markdown
|
||||
# Code Optimization Audit Report
|
||||
|
||||
## Executive Summary
|
||||
- **X** critical issues, **Y** high, **Z** medium, **W** low
|
||||
- Top 3 highest-impact fixes:
|
||||
1. [brief description] — [estimated impact]
|
||||
2. [brief description] — [estimated impact]
|
||||
3. [brief description] — [estimated impact]
|
||||
|
||||
## Findings by File
|
||||
|
||||
### `path/to/file.ts`
|
||||
|
||||
| # | Severity | Domain | Pattern | Fix | Impact |
|
||||
|---|----------|--------|---------|-----|--------|
|
||||
| 1 | CRITICAL | Database | N+1 query in loop | Use prefetch_related | 50x fewer queries |
|
||||
| 2 | HIGH | Async | Sequential awaits | Use Promise.all | 3x faster |
|
||||
|
||||
[... for each file with findings ...]
|
||||
|
||||
## Improvement Plan
|
||||
|
||||
Priority-ordered steps to implement the fixes:
|
||||
|
||||
1. **[CRITICAL] Fix N+1 queries in `api/users.py`**
|
||||
- Current: loop queries user.posts for each user
|
||||
- Fix: add prefetch_related('posts') to queryset
|
||||
- Impact: reduces N+1 to 2 queries
|
||||
|
||||
2. **[HIGH] Parallelize API calls in `services/sync.ts`**
|
||||
- Current: 5 sequential await fetch() calls
|
||||
- Fix: Promise.all([fetch1, fetch2, ...])
|
||||
- Impact: ~5x faster sync operation
|
||||
|
||||
[... continue for all findings ...]
|
||||
```
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# Algorithmic Complexity
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### O(n^2) and Worse Patterns
|
||||
```
|
||||
# Nested loops over same/related collections
|
||||
for.*in.*\n.*for.*in (nested for loops)
|
||||
\.forEach\(.*\.forEach\( (nested forEach)
|
||||
\.map\(.*\.map\( (nested map)
|
||||
\.filter\(.*\.includes\( (filter+includes = O(n*m))
|
||||
\.find\(.*inside.*\.map\( (find inside map)
|
||||
\.indexOf\(.*inside.*for (indexOf in loop)
|
||||
\.includes\(.*inside.*for (includes in loop)
|
||||
# Array as lookup table
|
||||
array\.find\(.*=== (use Map/Set instead)
|
||||
array\.some\(.*=== (use Set.has instead)
|
||||
list\.index\( (Python: use dict instead)
|
||||
if.*in\s+list (Python: O(n) lookup in list)
|
||||
```
|
||||
|
||||
### Unnecessary Iterations
|
||||
```
|
||||
\.filter\(.*\.length (filter just to count)
|
||||
\.filter\(.*\[0\] (filter just to get first - use find)
|
||||
\.map\(.*\.filter\( (map then filter - combine or reverse order)
|
||||
\.filter\(.*\.map\(.*\.filter (multiple passes when one suffices)
|
||||
\.sort\(\).*\[0\] (sort to get min/max - use Math.min/max or reduce)
|
||||
\.sort\(\).*\.slice\(0 (sort to get top-k - use partial sort/heap)
|
||||
sorted\(.*\)\[0\] (Python: use min() instead)
|
||||
sorted\(.*\)\[-1\] (Python: use max() instead)
|
||||
\.reverse\(\).*\.forEach (reverse just to iterate backwards)
|
||||
Object\.keys\(.*\.map\(.*Object\.values (iterating keys then accessing values)
|
||||
```
|
||||
|
||||
### Redundant Computation
|
||||
```
|
||||
# Same computation in loop
|
||||
for.*\n.*Math\. (math operations that could be hoisted)
|
||||
for.*\n.*\.length (accessing .length repeatedly - may be fine, check)
|
||||
for.*\n.*document\.querySelector (DOM queries in loops)
|
||||
for.*\n.*JSON\.parse (parsing same JSON repeatedly)
|
||||
for.*\n.*new RegExp\( (creating regex in loop)
|
||||
for.*\n.*new Date\( (creating Date objects in loop for same date)
|
||||
```
|
||||
|
||||
### Inefficient Data Structure Choice
|
||||
```
|
||||
# Using arrays where Set/Map would be better
|
||||
\.push\(.*\.includes\( (array as unique set)
|
||||
\.filter\(.*\.indexOf\( (dedup with filter+indexOf)
|
||||
\[\].*\.find\( (array for lookups)
|
||||
# Using objects where Map would be better
|
||||
\{\}.*\[.*\]\s*= (frequent dynamic key insertion)
|
||||
delete.*\[ (frequent key deletion from object)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Nested loops**: Pre-build lookup Map/Set, use hash-based approaches
|
||||
2. **Filter+includes**: Convert one collection to Set for O(1) lookups
|
||||
3. **Sort for min/max**: Use Math.min/max, reduce, or heap for top-k
|
||||
4. **Multiple passes**: Combine into single reduce/loop
|
||||
5. **Redundant computation**: Hoist invariants out of loops, memoize
|
||||
6. **Array as lookup**: Use Map for key-value, Set for existence checks
|
||||
7. **String matching in loops**: Pre-compile regex, use Map for exact matches
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
# Build & Compilation Optimization
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Unoptimized Build Config
|
||||
```
|
||||
# Webpack
|
||||
mode:\s*['"]development['"] (dev mode in production build)
|
||||
devtool:\s*['"]source-map['"] (full source maps in production)
|
||||
devtool:\s*['"]eval (eval source maps in production)
|
||||
# No code splitting
|
||||
splitChunks.*false (code splitting disabled)
|
||||
# No minification
|
||||
minimize:\s*false (minification disabled)
|
||||
# Missing tree shaking
|
||||
sideEffects.*true (prevents tree shaking)
|
||||
```
|
||||
|
||||
### Development-Only Code in Production
|
||||
```
|
||||
console\.log\( (debug logging)
|
||||
console\.debug\( (debug logging)
|
||||
console\.trace\( (trace logging)
|
||||
debugger; (debugger statement)
|
||||
\.only\( (test.only left in)
|
||||
\.skip\( (test.skip left in)
|
||||
if\s*\(.*process\.env\.NODE_ENV.*development (dev-only code)
|
||||
__DEV__ (React Native dev flag)
|
||||
```
|
||||
|
||||
### Missing Optimization Flags
|
||||
```
|
||||
# TypeScript
|
||||
"strict":\s*false (strict mode disabled)
|
||||
"skipLibCheck":\s*false (slow lib checking)
|
||||
"incremental":\s*false (no incremental compilation)
|
||||
# Python
|
||||
python\s+-O (check if optimized flag used)
|
||||
__debug__ (debug-only code)
|
||||
# Docker
|
||||
FROM.*:latest (unpinned base image)
|
||||
RUN.*pip install(?!.*--no-cache) (pip without --no-cache-dir)
|
||||
RUN.*npm install(?!.*--production) (npm install without --production)
|
||||
COPY\s+\.\s+\. (copying entire context)
|
||||
```
|
||||
|
||||
### Large/Slow Imports at Startup
|
||||
```
|
||||
# Top-level heavy imports that could be lazy
|
||||
import.*tensorflow (heavy ML library at top)
|
||||
import.*pandas (heavy data library at top)
|
||||
import.*matplotlib (heavy viz library at top)
|
||||
import.*scipy (heavy math library at top)
|
||||
from.*import\s+\* (wildcard imports slow startup)
|
||||
# Circular imports
|
||||
ImportError.*circular (circular import errors)
|
||||
```
|
||||
|
||||
### Missing Caching in CI/CD
|
||||
```
|
||||
# No caching steps
|
||||
npm install(?!.*cache) (npm install without cache)
|
||||
pip install(?!.*cache) (pip install without cache)
|
||||
go build(?!.*cache) (go build without cache)
|
||||
docker build(?!.*cache) (docker build without layer cache)
|
||||
```
|
||||
|
||||
### Slow Test Suite
|
||||
```
|
||||
# Real I/O in tests
|
||||
fetch\(.*test (real network calls in tests)
|
||||
requests\.\w+\(.*test (real HTTP in Python tests)
|
||||
open\(.*test (real file I/O in tests)
|
||||
# No test parallelization
|
||||
--runInBand (Jest sequential mode)
|
||||
-p no:xdist (pytest parallelization disabled)
|
||||
# Heavy setup/teardown
|
||||
beforeAll.*database (real DB setup in tests)
|
||||
setUp.*database (real DB in Python tests)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Build config**: Ensure production mode, minification, tree shaking, code splitting
|
||||
2. **Dev code**: Strip console.log/debugger via build plugin (e.g., babel-plugin-transform-remove-console)
|
||||
3. **TypeScript**: Enable strict, incremental, skipLibCheck for faster builds
|
||||
4. **Docker**: Multi-stage builds, .dockerignore, layer caching, pinned versions
|
||||
5. **Lazy imports**: Move heavy imports to function scope where they're needed
|
||||
6. **CI caching**: Cache node_modules, pip cache, go build cache, Docker layers
|
||||
7. **Test speed**: Mock I/O, run tests in parallel, use in-memory DBs for integration tests
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# Bundle Size & Dependencies
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Heavy Imports
|
||||
```
|
||||
import\s+\w+\s+from\s+['"]lodash['"] (full lodash import vs lodash/specific)
|
||||
import\s+\w+\s+from\s+['"]moment['"] (moment.js - use date-fns/dayjs)
|
||||
import\s+\w+\s+from\s+['"]underscore['"] (underscore - mostly native now)
|
||||
import\s+\*\s+as (wildcard imports prevent tree-shaking)
|
||||
require\(['"]lodash['"]\) (CJS lodash import)
|
||||
from\s+pandas\s+import\s+\* (full pandas import)
|
||||
import\s+tensorflow (full TF import)
|
||||
import\s+boto3 (full AWS SDK)
|
||||
```
|
||||
|
||||
### Unused Dependencies
|
||||
```
|
||||
# Check package.json dependencies vs actual imports
|
||||
# Check requirements.txt vs actual imports
|
||||
# Check go.mod vs actual imports
|
||||
import.*from.*['"](\w+)['"] (cross-reference with package.json)
|
||||
```
|
||||
|
||||
### Duplicate Functionality
|
||||
```
|
||||
# Multiple date libraries
|
||||
moment.*\n.*date-fns (both moment and date-fns)
|
||||
moment.*\n.*dayjs (both moment and dayjs)
|
||||
# Multiple HTTP clients
|
||||
axios.*\n.*node-fetch (both axios and fetch)
|
||||
axios.*\n.*got (both axios and got)
|
||||
# Multiple utility libraries
|
||||
lodash.*\n.*underscore (both lodash and underscore)
|
||||
# Multiple state managers
|
||||
redux.*\n.*mobx (both redux and mobx)
|
||||
zustand.*\n.*jotai (multiple state libs)
|
||||
```
|
||||
|
||||
### Dev Dependencies in Production
|
||||
```
|
||||
# devDependencies imported in src/
|
||||
import.*from.*['"](@testing|jest|mocha|chai|sinon|cypress|storybook)
|
||||
# Debug/test code in production
|
||||
console\.log\(
|
||||
console\.debug\(
|
||||
debugger;
|
||||
\.only\( (test.only left in)
|
||||
```
|
||||
|
||||
### Dynamic Imports Missing
|
||||
```
|
||||
# Large components imported statically that could be lazy
|
||||
import.*Modal (modals are great candidates for lazy loading)
|
||||
import.*Chart (charts are heavy)
|
||||
import.*Editor (rich editors are heavy)
|
||||
import.*PDF (PDF libs are heavy)
|
||||
import.*Map (map components are heavy)
|
||||
# Route-level components not lazy loaded
|
||||
import.*Page.*from (page components should often be lazy)
|
||||
```
|
||||
|
||||
### Large Assets
|
||||
```
|
||||
# Check for unoptimized assets
|
||||
\.png['"] (check if could be webp/avif)
|
||||
\.jpg['"] (check if could be webp/avif)
|
||||
\.gif['"] (check if could be video/webp)
|
||||
\.svg['"].*import (SVGs imported as modules - check size)
|
||||
base64 (inline base64 assets)
|
||||
data:image (inline images)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Lodash**: Use `lodash-es/specific` or native equivalents (Array.find, Object.entries, etc.)
|
||||
2. **Moment.js**: Replace with date-fns or dayjs (10x smaller)
|
||||
3. **Wildcard imports**: Use named imports for tree-shaking
|
||||
4. **Unused deps**: Remove from package.json/requirements.txt
|
||||
5. **Dynamic imports**: Use React.lazy/import() for heavy, below-fold components
|
||||
6. **Images**: Convert to WebP/AVIF, use responsive srcset, lazy load below-fold
|
||||
7. **Duplicate libs**: Consolidate to one library per concern
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# Caching & Memoization
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Missing Memoization
|
||||
```
|
||||
# Expensive computations without caching
|
||||
def\s+\w+\(.*\).*:\s*\n.*for.*for (Python: expensive function without @lru_cache)
|
||||
function\s+\w+\(.*\).*\{.*for.*for (JS: expensive function without memoization)
|
||||
# React missing useMemo/useCallback
|
||||
const\s+\w+\s*=\s*\w+\.filter\( (derived data on every render)
|
||||
const\s+\w+\s*=\s*\w+\.map\( (derived data on every render)
|
||||
const\s+\w+\s*=\s*\w+\.reduce\( (derived data on every render)
|
||||
const\s+\w+\s*=\s*\w+\.sort\( (sorting on every render)
|
||||
# Same computation called multiple times
|
||||
(\w+)\(same_args\).*\1\(same_args\) (same function, same args, called twice)
|
||||
```
|
||||
|
||||
### Cache Without Invalidation
|
||||
```
|
||||
cache\s*=\s*\{\} (cache without TTL or max size)
|
||||
_cache\s*=\s*\{\} (module cache without eviction)
|
||||
memo\s*=\s*\{\} (memo without invalidation)
|
||||
\.cache\s*=\s*\{\} (instance cache without cleanup)
|
||||
CACHE_TTL.*=.*(?:86400|3600.*24) (very long TTL - stale data risk)
|
||||
```
|
||||
|
||||
### Redundant API/DB Calls
|
||||
```
|
||||
# Same query executed multiple times
|
||||
\.query\(.*same.*\.query\( (duplicate queries)
|
||||
fetch\(['"]same_url['"]\).*fetch\( (duplicate fetches)
|
||||
# No SWR/stale-while-revalidate
|
||||
useEffect\(.*fetch\(.*\[\] (fetch on every mount without caching)
|
||||
useEffect\(.*axios\.\w+\(.*\[\] (API call on every mount)
|
||||
componentDidMount.*fetch (fetch without caching layer)
|
||||
```
|
||||
|
||||
### Over-Caching
|
||||
```
|
||||
# Caching things that change frequently
|
||||
cache.*user.*session (caching session-specific data)
|
||||
cache.*real.?time (caching real-time data)
|
||||
cache.*current.*time (caching time-dependent data)
|
||||
# Caching large objects
|
||||
cache\[.*\]\s*=\s*.*large (large objects in cache)
|
||||
```
|
||||
|
||||
### Missing HTTP Caching
|
||||
```
|
||||
# API responses without cache headers
|
||||
res\.json\( (check if Cache-Control is set)
|
||||
return\s+Response\( (check if cache headers are set)
|
||||
return\s+JsonResponse\( (Django: check cache headers)
|
||||
# Static assets without long cache
|
||||
express\.static\( (check maxAge setting)
|
||||
nginx.*location.*static (check expires/cache-control)
|
||||
```
|
||||
|
||||
### Computed Properties Recalculated
|
||||
```
|
||||
# Getters that compute on every access
|
||||
get\s+\w+\(\)\s*\{.*return.*\.filter (getter computing on each access)
|
||||
get\s+\w+\(\)\s*\{.*return.*\.map (getter computing on each access)
|
||||
@property\s*\n\s*def.*\n.*for (Python property computing in loop)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Memoization**: Use @lru_cache (Python), useMemo/useCallback (React), _.memoize (JS)
|
||||
2. **Cache invalidation**: Always set TTL and max size; prefer LRU eviction
|
||||
3. **API caching**: Use SWR/React Query for client, Redis/Memcached for server
|
||||
4. **HTTP caching**: Set Cache-Control headers, use ETags, stale-while-revalidate
|
||||
5. **Computed properties**: Cache results with dirty flag or use memoized selectors (reselect)
|
||||
6. **Request deduplication**: Deduplicate identical in-flight requests
|
||||
7. **Multi-level cache**: L1 (in-memory) -> L2 (Redis) -> L3 (DB) for read-heavy workloads
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
# Concurrency & Async Patterns
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Sequential Async (Should Be Parallel)
|
||||
```
|
||||
await.*\n.*await.*\n.*await (multiple sequential awaits that could be parallel)
|
||||
for.*await (sequential await in loop)
|
||||
\.then\(.*\.then\(.*\.then\( (promise chain that could be Promise.all)
|
||||
# Python
|
||||
await.*\n.*await.*\n.*await (sequential awaits)
|
||||
for.*in.*:\n.*await (await in loop)
|
||||
```
|
||||
|
||||
### Missing Parallelization
|
||||
```
|
||||
# Should use Promise.all / asyncio.gather
|
||||
fetch\(.*\n.*fetch\( (sequential fetches)
|
||||
axios\.\w+\(.*\n.*axios\. (sequential HTTP calls)
|
||||
requests\.\w+\(.*\n.*requests\. (Python sequential requests)
|
||||
```
|
||||
|
||||
### Blocking Operations in Async Context
|
||||
```
|
||||
# Node.js sync operations in async code
|
||||
fs\.readFileSync (blocking file read)
|
||||
fs\.writeFileSync (blocking file write)
|
||||
fs\.existsSync (blocking existence check)
|
||||
child_process\.execSync (blocking exec)
|
||||
\.readFileSync\( (any sync file operation)
|
||||
# Python blocking in async
|
||||
time\.sleep\( (use asyncio.sleep instead)
|
||||
requests\. (use aiohttp/httpx instead)
|
||||
open\(.*\.read\(\) (use aiofiles instead)
|
||||
os\.path\.exists (use aio equivalent)
|
||||
```
|
||||
|
||||
### Race Conditions & Thread Safety
|
||||
```
|
||||
# Shared mutable state
|
||||
global\s+\w+.*= (Python global mutation)
|
||||
threading\.Thread.*shared (shared state across threads)
|
||||
# Missing locks
|
||||
\.append\(.*thread (list append without lock)
|
||||
\+=.*without.*lock (increment without lock)
|
||||
# JavaScript
|
||||
let\s+\w+.*=.*\n.*async (mutable let used in async)
|
||||
```
|
||||
|
||||
### Unbounded Concurrency
|
||||
```
|
||||
# No concurrency limit
|
||||
\.map\(.*fetch (unbounded parallel fetches)
|
||||
\.map\(.*axios (unbounded parallel requests)
|
||||
Promise\.all\(.*\.map\( (all items in parallel, no limit)
|
||||
asyncio\.gather\(.*for (all coroutines at once)
|
||||
# Missing backpressure
|
||||
while.*true.*await (infinite async loop without backpressure)
|
||||
```
|
||||
|
||||
### Error Handling in Async
|
||||
```
|
||||
# Unhandled rejections
|
||||
\.then\(.*without.*\.catch (promise without catch)
|
||||
async.*without.*try.*catch (async without error handling)
|
||||
# Swallowed errors
|
||||
catch\s*\(\s*\)\s*\{ (empty catch block)
|
||||
except:\s*$ (bare except)
|
||||
except\s+Exception\s*:.*pass (catch-all with pass)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Sequential awaits**: Use Promise.all/allSettled, asyncio.gather for independent operations
|
||||
2. **Await in loops**: Batch with Promise.all or use p-limit for controlled concurrency
|
||||
3. **Blocking in async**: Replace sync APIs with async equivalents
|
||||
4. **Race conditions**: Use locks, atomic operations, or immutable patterns
|
||||
5. **Unbounded concurrency**: Use semaphores, p-limit, connection pools
|
||||
6. **Error handling**: Always catch async errors, use Promise.allSettled for partial failure tolerance
|
||||
7. **Backpressure**: Use queues, streaming, or batching for producer-consumer patterns
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# Configuration & Infrastructure Inefficiencies
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Missing Connection Pooling
|
||||
```
|
||||
# New connection per request
|
||||
create_engine\(.*(?!.*pool) (SQLAlchemy without pool config)
|
||||
new Pool\(.*(?!.*max) (pg Pool without max connections)
|
||||
mongoose\.connect\(.*(?!.*pool) (Mongoose without pool)
|
||||
DriverManager\.getConnection\( (Java: new connection per call)
|
||||
psycopg2\.connect\(.*(?!.*pool) (psycopg2 without pool)
|
||||
redis\.createClient\(.*per.*request (new Redis client per request)
|
||||
```
|
||||
|
||||
### Missing Environment-Based Config
|
||||
```
|
||||
hardcoded.*url (hardcoded URLs)
|
||||
['"]http://localhost (hardcoded localhost URLs)
|
||||
['"]127\.0\.0\.1 (hardcoded localhost IPs)
|
||||
password\s*=\s*['"] (hardcoded passwords)
|
||||
api.?key\s*=\s*['"] (hardcoded API keys)
|
||||
secret\s*=\s*['"] (hardcoded secrets)
|
||||
port\s*=\s*\d{4} (hardcoded port numbers)
|
||||
```
|
||||
|
||||
### Missing Process Management
|
||||
```
|
||||
# Single-threaded Node.js without clustering
|
||||
app\.listen\(.*(?!.*cluster) (Node without cluster module)
|
||||
# Python without proper WSGI/ASGI workers
|
||||
\.run\(.*debug=True (Flask debug mode)
|
||||
uvicorn\.run\(.*workers=1 (single worker)
|
||||
gunicorn.*-w\s*1\b (single gunicorn worker)
|
||||
```
|
||||
|
||||
### Docker/Container Issues
|
||||
```
|
||||
FROM.*:latest (unpinned image version)
|
||||
RUN.*apt-get.*&&.*apt-get (check if apt cache is cleaned)
|
||||
COPY\s+\.\s+\. (copying entire context - no .dockerignore)
|
||||
RUN.*npm install\b(?!.*--production) (installing devDeps in production)
|
||||
RUN.*pip install\b(?!.*--no-cache) (pip without cache clearing)
|
||||
# Multiple RUN commands that should be combined
|
||||
RUN.*\nRUN.*\nRUN (multiple RUN layers)
|
||||
```
|
||||
|
||||
### Missing Health Checks
|
||||
```
|
||||
# Services without health endpoints
|
||||
app\.(listen|start)\(.*(?!.*health) (server without health check)
|
||||
# Docker without HEALTHCHECK
|
||||
Dockerfile.*(?!.*HEALTHCHECK) (Dockerfile without health check)
|
||||
```
|
||||
|
||||
### Inefficient Polling
|
||||
```
|
||||
setInterval\(.*fetch (polling instead of WebSocket/SSE)
|
||||
setInterval\(.*axios (polling instead of push)
|
||||
while.*sleep.*fetch (polling loop)
|
||||
time\.sleep\(.*requests (Python: polling with sleep)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Connection pooling**: Configure pool_size, max_overflow, pool_recycle
|
||||
2. **Environment config**: Use .env files, config libraries, never hardcode secrets
|
||||
3. **Process management**: Use cluster mode (Node), multiple workers (Python ASGI/WSGI)
|
||||
4. **Docker**: Multi-stage builds, .dockerignore, combine RUN layers, pin versions
|
||||
5. **Health checks**: Add /health endpoint, Docker HEALTHCHECK, readiness/liveness probes
|
||||
6. **Polling -> Push**: Use WebSocket, SSE, or long-polling instead of interval polling
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
# Data Structures & Serialization
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Wrong Data Structure for the Job
|
||||
```
|
||||
# Array used for frequent lookups (should be Map/Set/dict)
|
||||
\.find\(.*=== (linear search - use Map)
|
||||
\.findIndex\(.*=== (linear search for index)
|
||||
\.includes\(.*inside.*loop (O(n) lookup in loop)
|
||||
\.indexOf\(.*inside.*loop (O(n) lookup in loop)
|
||||
if.*in\s+\[ (Python: list membership test)
|
||||
list\.count\( (Python: counting in list)
|
||||
|
||||
# Searching sorted data linearly (should use binary search)
|
||||
\.find\(.*sorted (linear search on sorted array)
|
||||
for.*sorted (iterating sorted data to find)
|
||||
|
||||
# Using objects where Map is better (non-string keys, frequent add/delete)
|
||||
\w+\[\w+\.id\]\s*= (object with dynamic keys from IDs)
|
||||
delete\s+\w+\[ (frequent deletion from object)
|
||||
Object\.keys\(.*\.length (counting object keys - Map.size is O(1))
|
||||
|
||||
# Using array for queue/deque operations
|
||||
\.shift\(\) (Array.shift is O(n) - use proper queue)
|
||||
\.unshift\( (Array.unshift is O(n))
|
||||
```
|
||||
|
||||
### Unnecessary Deep Copies
|
||||
```
|
||||
JSON\.parse\(JSON\.stringify (JSON round-trip for deep clone)
|
||||
\.map\(.*\.map\(.*spread (nested spread for deep copy)
|
||||
\{\.\.\..*\{\.\.\. (nested object spread)
|
||||
structuredClone\(.*inside.*loop (deep cloning in loop)
|
||||
copy\.deepcopy\(.*loop (Python deepcopy in loop)
|
||||
import\s+copy (check if deepcopy is overused)
|
||||
```
|
||||
|
||||
### Inefficient Serialization
|
||||
```
|
||||
# JSON for internal communication (use binary formats)
|
||||
JSON\.stringify.*JSON\.parse.*internal
|
||||
pickle\.dump.*pickle\.load (Python: consider msgpack for cross-language)
|
||||
# Serializing more than needed
|
||||
JSON\.stringify\(.*entire (serializing entire object when subset needed)
|
||||
\.toJSON\(\) (check what's being serialized)
|
||||
# Repeated serialization
|
||||
JSON\.stringify\(.*loop (stringifying in loop)
|
||||
JSON\.parse\(.*loop (parsing in loop)
|
||||
```
|
||||
|
||||
### Unnecessary Object Creation
|
||||
```
|
||||
new Date\(.*inside.*loop (creating Date objects in loop)
|
||||
new RegExp\(.*inside.*loop (compiling regex in loop)
|
||||
new URL\(.*inside.*loop (creating URL objects in loop)
|
||||
\.split\(.*\.join\( (split then join - use replace)
|
||||
\.toString\(\).*\.split\( (unnecessary string conversion)
|
||||
Array\.from\(.*Array\.from\( (double Array.from)
|
||||
```
|
||||
|
||||
### Immutability Overhead
|
||||
```
|
||||
# Excessive spread operators
|
||||
\{\.\.\.state, (spreading large state objects)
|
||||
\[\.\.\.array, (spreading large arrays)
|
||||
\.map\(.*=>.*\{\.\.\. (creating new objects in map with spread)
|
||||
# Immer not used where it should be
|
||||
produce\( (check if immer is used consistently)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Array -> Map/Set**: Use Map for key-value lookups, Set for membership testing
|
||||
2. **Array.shift/unshift**: Use a proper deque/queue implementation
|
||||
3. **Deep copies**: Use structuredClone (modern), or targeted shallow copies
|
||||
4. **Serialization**: Use msgpack/protobuf for internal services, only JSON for external APIs
|
||||
5. **Object creation in loops**: Hoist object creation, reuse instances, use object pools
|
||||
6. **Large state spreads**: Use Immer's produce(), or targeted updates
|
||||
7. **Binary search**: Use on sorted data instead of linear search
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
# Database & Query Optimization
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### N+1 Query Problems
|
||||
```
|
||||
# ORM loops - querying inside iterations
|
||||
for.*in.*\.all\(\)
|
||||
for.*in.*\.filter\(
|
||||
for.*in.*\.objects\.
|
||||
\.prefetch_related (absence of - check if loops exist WITHOUT prefetch)
|
||||
\.select_related (absence of - check if FK access exists WITHOUT select_related)
|
||||
# SQLAlchemy
|
||||
session\.query.*for.*in
|
||||
\.lazy\s*=\s*True
|
||||
# ActiveRecord
|
||||
\.each.*\.where
|
||||
\.map.*\.find
|
||||
# Sequelize / TypeORM
|
||||
findOne.*inside.*map
|
||||
findOne.*inside.*for
|
||||
await.*find.*inside.*loop
|
||||
```
|
||||
|
||||
### Unoptimized Queries
|
||||
```
|
||||
SELECT \*
|
||||
SELECT.*FROM.*WITHOUT.*WHERE (full table scans)
|
||||
LIKE '% (leading wildcard - can't use index)
|
||||
ORDER BY.*RAND()
|
||||
NOT IN.*SELECT (subquery instead of JOIN)
|
||||
DISTINCT.*SELECT \*
|
||||
GROUP BY.*without.*index
|
||||
\.raw\(.*SELECT (raw queries - potential SQL injection too)
|
||||
COUNT\(\*\).*WHERE (count with filter vs indexed count)
|
||||
```
|
||||
|
||||
### Missing Indexes (Heuristic)
|
||||
```
|
||||
WHERE.*=.*AND.*= (composite queries without composite index)
|
||||
ORDER BY.*multiple columns
|
||||
JOIN.*ON.*without index hint
|
||||
\.filter\(.*__in= (IN queries on large sets)
|
||||
```
|
||||
|
||||
### ORM Anti-patterns
|
||||
```
|
||||
\.save\(\).*inside.*loop (batch update instead)
|
||||
\.create\(\).*inside.*loop (bulk_create instead)
|
||||
\.update\(\).*for.*in (queryset.update instead)
|
||||
len\(.*\.all\(\)\) (.count() instead)
|
||||
list\(.*\.all\(\)\) (unnecessary materialization)
|
||||
if.*\.exists\(\).*\.first\(\) (double query)
|
||||
\.values\(\).*\.values\(\) (chained values)
|
||||
```
|
||||
|
||||
### Connection Management
|
||||
```
|
||||
# Missing connection pooling
|
||||
create_engine\(.*pool_size (check if pool is configured)
|
||||
new Pool\( (check pool settings)
|
||||
max_connections (check if reasonable)
|
||||
# Unclosed connections
|
||||
connection\.open.*without.*close
|
||||
cursor.*without.*close
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **N+1 Queries**: Use eager loading (prefetch_related, select_related, JOIN FETCH, include/eager)
|
||||
2. **SELECT ***: Select only needed columns
|
||||
3. **Missing indexes**: Add indexes on frequently filtered/joined columns
|
||||
4. **Loop queries**: Use bulk operations (bulk_create, bulk_update, executemany)
|
||||
5. **Connection pooling**: Configure connection pools with appropriate size
|
||||
6. **Query caching**: Cache frequently-read, rarely-changing data
|
||||
7. **Pagination**: Never load unbounded result sets
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
# Dead Code & Redundancy
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Unused Exports/Functions
|
||||
```
|
||||
export\s+(function|const|class)\s+\w+ (cross-reference: is it imported anywhere?)
|
||||
def\s+\w+\( (cross-reference: is it called anywhere?)
|
||||
public\s+(static\s+)?\w+\s+\w+\( (Java/C# methods - are they called?)
|
||||
func\s+\w+\( (Go functions - are they called?)
|
||||
```
|
||||
|
||||
### Unused Imports
|
||||
```
|
||||
import.*from.*['"].*['"] (cross-reference with usage in file)
|
||||
from\s+\w+\s+import\s+\w+ (Python: check if imported name is used)
|
||||
require\(['"].*['"]\) (CJS: check if result is used)
|
||||
use\s+\w+; (Rust: check if used)
|
||||
```
|
||||
|
||||
### Commented-Out Code
|
||||
```
|
||||
//\s*(function|const|let|var|class|import|return|if|for|while)
|
||||
#\s*(def|class|import|return|if|for|while)
|
||||
/\*[\s\S]*?(function|class|import)[\s\S]*?\*/
|
||||
```
|
||||
|
||||
### Dead Branches
|
||||
```
|
||||
if\s*\(\s*false\s*\) (always-false condition)
|
||||
if\s*\(\s*true\s*\) (always-true condition - dead else)
|
||||
if\s*\(\s*0\s*\) (falsy constant)
|
||||
if\s*\(\s*['"]['"] (empty string - always falsy)
|
||||
TODO.*remove (TODOs indicating dead code)
|
||||
FIXME.*remove
|
||||
HACK.*temporary
|
||||
# Feature flags stuck off
|
||||
FEATURE_.*=\s*false
|
||||
ENABLE_.*=\s*false
|
||||
```
|
||||
|
||||
### Duplicate Logic
|
||||
```
|
||||
# Similar function signatures in same file or nearby files
|
||||
function\s+\w*(get|fetch|load|process|handle)\w*\( (many similar handlers)
|
||||
def\s+\w*(get|fetch|load|process|handle)\w*\( (Python: similar functions)
|
||||
# Copy-paste indicators
|
||||
# Same code block appearing multiple times (use Grep to find identical blocks)
|
||||
```
|
||||
|
||||
### Deprecated/Legacy Code
|
||||
```
|
||||
@deprecated
|
||||
@Deprecated
|
||||
# deprecated
|
||||
\.deprecated
|
||||
DEPRECATED
|
||||
legacy
|
||||
Legacy
|
||||
LEGACY
|
||||
__legacy__
|
||||
_old\b
|
||||
_backup\b
|
||||
_v[0-9]\b (versioned functions like process_v1)
|
||||
```
|
||||
|
||||
### Unreachable Code
|
||||
```
|
||||
return.*\n\s*(var|let|const|function) (code after return)
|
||||
throw.*\n\s*(var|let|const|function) (code after throw)
|
||||
exit\(\).*\n (code after exit)
|
||||
sys\.exit\(.*\n (Python: code after sys.exit)
|
||||
break\s*;\s*\n\s*\w (code after break)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Unused exports**: Remove and verify no external consumers (check all import statements)
|
||||
2. **Unused imports**: Remove with IDE or linting tooling
|
||||
3. **Commented code**: Delete it - version control preserves history
|
||||
4. **Dead branches**: Remove unreachable code, clean up feature flags
|
||||
5. **Duplicate logic**: Extract shared function, use strategy pattern if variants differ slightly
|
||||
6. **Deprecated code**: Plan migration, remove after all callers are updated
|
||||
7. **Unreachable code**: Remove statements after return/throw/exit
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
# Error Handling & Resilience
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Missing Timeouts
|
||||
```
|
||||
fetch\(.*(?!.*timeout) (fetch without timeout)
|
||||
axios\.\w+\(.*(?!.*timeout) (axios without timeout)
|
||||
requests\.\w+\(.*(?!.*timeout) (Python requests without timeout)
|
||||
http\.\w+\(.*(?!.*timeout) (http call without timeout)
|
||||
new Promise\(.*(?!.*setTimeout) (promise without timeout)
|
||||
\.connect\(.*(?!.*timeout) (DB/socket connect without timeout)
|
||||
```
|
||||
|
||||
### Swallowed Errors
|
||||
```
|
||||
catch\s*\(\s*\w*\s*\)\s*\{\s*\} (empty catch block)
|
||||
catch\s*\(\s*\)\s*\{\s*\} (empty catch, no error param)
|
||||
except:\s*$ (bare except)
|
||||
except\s+Exception.*pass (catch-all with pass)
|
||||
except\s+Exception.*continue (catch-all with continue)
|
||||
\.catch\(\s*\(\)\s*=>\s*\{\s*\}\) (empty .catch handler)
|
||||
\.catch\(\s*\(\)\s*=>\s*null\) (swallowing with null)
|
||||
on_error.*pass (error handler that does nothing)
|
||||
```
|
||||
|
||||
### Missing Retries for Transient Failures
|
||||
```
|
||||
# Network calls without retry logic
|
||||
fetch\(.*(?!.*retry) (fetch without retry)
|
||||
axios\.\w+\(.*(?!.*retry) (API call without retry)
|
||||
requests\.\w+\(.*(?!.*retry) (Python request without retry)
|
||||
# Database operations without retry
|
||||
\.query\(.*(?!.*retry) (DB query without retry)
|
||||
\.execute\(.*(?!.*retry) (DB execute without retry)
|
||||
```
|
||||
|
||||
### No Circuit Breaker
|
||||
```
|
||||
# Repeated calls to potentially failing services without circuit breaking
|
||||
while.*retry.*fetch (retry loop without circuit break)
|
||||
MAX_RETRIES.*=.*[5-9]|[1-9]\d+ (high retry count without circuit breaker)
|
||||
```
|
||||
|
||||
### Resource Cleanup on Error
|
||||
```
|
||||
# try without finally for resource cleanup
|
||||
try\s*\{.*open.*(?!.*finally) (open resource without finally)
|
||||
try:.*open\(.*(?!.*finally) (Python: open without finally or context manager)
|
||||
# Async cleanup missing
|
||||
async.*try.*(?!.*finally) (async operation without cleanup)
|
||||
```
|
||||
|
||||
### Cascading Failures
|
||||
```
|
||||
# No fallback/default values
|
||||
\?\?.*undefined (check fallback quality)
|
||||
\|\|.*null (check fallback quality)
|
||||
\.get\(.*,\s*None\) (Python: check if None is appropriate default)
|
||||
# No graceful degradation
|
||||
catch.*throw (catching just to re-throw - no degradation)
|
||||
catch.*return\s+null (returning null on error - caller may not handle)
|
||||
```
|
||||
|
||||
### Logging Without Action
|
||||
```
|
||||
console\.error\(.*(?!.*throw|return|retry) (logging error but not handling it)
|
||||
logger\.error\(.*(?!.*raise|return|retry) (Python: logging without action)
|
||||
print\(.*error.*(?!.*raise|return) (print error without handling)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Timeouts**: Add timeouts to ALL external calls (network, DB, file I/O). Use AbortController for fetch
|
||||
2. **Swallowed errors**: At minimum log errors, prefer explicit handling or re-throwing
|
||||
3. **Retries**: Implement exponential backoff with jitter for transient failures
|
||||
4. **Circuit breakers**: Use circuit breaker pattern for external service calls
|
||||
5. **Resource cleanup**: Use try-finally, context managers, or using statements
|
||||
6. **Graceful degradation**: Return cached/default data instead of failing completely
|
||||
7. **Error propagation**: Don't catch errors you can't handle - let them bubble up
|
||||
89
src/resources/skills/code-optimizer/references/io-network.md
Normal file
89
src/resources/skills/code-optimizer/references/io-network.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# I/O & Network Optimization
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Sequential Requests (Should Be Batched/Parallel)
|
||||
```
|
||||
fetch\(.*\n.*fetch\( (sequential fetch calls)
|
||||
axios\.\w+\(.*\n.*axios\. (sequential axios calls)
|
||||
requests\.\w+\(.*\n.*requests (sequential Python requests)
|
||||
http\.\w+\(.*\n.*http\. (sequential Node http calls)
|
||||
\.get\(.*\n.*\.get\( (sequential GET requests)
|
||||
\.post\(.*\n.*\.post\( (sequential POST requests)
|
||||
```
|
||||
|
||||
### Missing Batching
|
||||
```
|
||||
# Individual API calls in loops
|
||||
for.*\n.*fetch\( (fetch in loop)
|
||||
for.*\n.*axios\. (axios in loop)
|
||||
for.*\n.*requests\. (requests in loop)
|
||||
\.map\(.*fetch (map with individual fetches)
|
||||
\.forEach\(.*fetch (forEach with individual fetches)
|
||||
# Individual DB writes in loops
|
||||
\.save\(\).*for (save in loop - should batch)
|
||||
\.insert\(.*for (insert in loop - should bulk insert)
|
||||
```
|
||||
|
||||
### No Request Deduplication
|
||||
```
|
||||
# Same endpoint called multiple times
|
||||
fetch\(['"]([^'"]+)['"]\) (check for duplicate URLs)
|
||||
axios\.\w+\(['"]([^'"]+)['"] (check for duplicate URLs)
|
||||
useQuery\(.*['"]([^'"]+)['"] (check for duplicate query keys)
|
||||
```
|
||||
|
||||
### Missing Compression
|
||||
```
|
||||
# Large payload without compression
|
||||
Content-Type.*application/json (check if gzip/br enabled)
|
||||
res\.json\( (response without compression middleware)
|
||||
# No compression middleware
|
||||
express\(\).*without.*compression
|
||||
```
|
||||
|
||||
### Inefficient Serialization
|
||||
```
|
||||
JSON\.stringify\(.*large (stringifying large objects)
|
||||
JSON\.parse\(.*JSON\.stringify (deep clone via JSON - use structuredClone)
|
||||
pickle\.dumps\( (Python: consider msgpack/protobuf for performance)
|
||||
yaml\.dump\(.*yaml\.load\( (YAML round-trip - slow for data exchange)
|
||||
```
|
||||
|
||||
### Missing Streaming
|
||||
```
|
||||
\.readFile\( (read entire file vs createReadStream)
|
||||
\.readFileSync\( (sync + entire file)
|
||||
body\.json\(\) (parse entire body vs streaming parser)
|
||||
\.text\(\) (entire response as text)
|
||||
\.json\(\).*large (entire JSON response in memory)
|
||||
response\.data (entire response buffered)
|
||||
```
|
||||
|
||||
### Missing Caching Headers
|
||||
```
|
||||
# API responses without caching
|
||||
res\.json\(.*without.*cache-control
|
||||
res\.send\(.*without.*etag
|
||||
# Static assets without cache headers
|
||||
express\.static\(.*without.*maxAge
|
||||
```
|
||||
|
||||
### Retry Without Backoff
|
||||
```
|
||||
retry.*count (check if exponential backoff exists)
|
||||
while.*retry (retry loop without delay increase)
|
||||
catch.*retry (catch-retry without backoff)
|
||||
MAX_RETRIES (check backoff strategy)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Sequential requests**: Use Promise.all, asyncio.gather, or batch APIs
|
||||
2. **Loop requests**: Batch into single API call or use DataLoader pattern
|
||||
3. **Deduplication**: Use request deduplication (SWR, React Query, custom cache)
|
||||
4. **Compression**: Enable gzip/brotli at server and CDN level
|
||||
5. **Serialization**: Use efficient formats (protobuf, msgpack) for internal services
|
||||
6. **Streaming**: Use streams for large files/responses, NDJSON for large JSON
|
||||
7. **Caching**: Set appropriate Cache-Control, ETag, use stale-while-revalidate
|
||||
8. **Retries**: Implement exponential backoff with jitter
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
# Logging & Observability Performance
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Excessive Logging
|
||||
```
|
||||
console\.log\( (console.log in production code)
|
||||
console\.debug\( (console.debug in production)
|
||||
print\(.*debug (Python print debugging)
|
||||
logger\.debug\(.*inside.*loop (debug logging in hot loop)
|
||||
log\.\w+\(.*inside.*for (any logging in tight loop)
|
||||
console\.log\(JSON\.stringify\( (serializing objects just to log)
|
||||
```
|
||||
|
||||
### Expensive String Formatting in Logs
|
||||
```
|
||||
# String interpolation/formatting when log level is disabled
|
||||
logger\.debug\(f" (Python f-string even when debug disabled)
|
||||
logger\.debug\(.*\.format\( (Python .format() even when debug disabled)
|
||||
logger\.debug\(` (JS template literal even when debug disabled)
|
||||
logger\.debug\(.*\+.*\+ (string concat for debug log)
|
||||
JSON\.stringify\(.*log (JSON stringify for logging)
|
||||
```
|
||||
|
||||
### Missing Structured Logging
|
||||
```
|
||||
console\.log\(["'].*:.*["'] (unstructured string logging)
|
||||
print\(.*["'].*:.*["'] (unstructured print logging)
|
||||
logger\.\w+\(["'].*["'] % (format string logging vs structured)
|
||||
```
|
||||
|
||||
### Synchronous Logging
|
||||
```
|
||||
fs\.writeFileSync.*log (sync file write for logging)
|
||||
fs\.appendFileSync.*log (sync file append for logging)
|
||||
open\(.*log.*\)\.write\( (Python: sync log file write)
|
||||
```
|
||||
|
||||
### Missing Request/Trace IDs
|
||||
```
|
||||
# API handlers without correlation IDs
|
||||
app\.(get|post)\(.*req.*res (check if request ID is propagated)
|
||||
@app\.route\( (check if request ID middleware exists)
|
||||
```
|
||||
|
||||
### Metrics Collection Overhead
|
||||
```
|
||||
# Metrics in hot paths
|
||||
\.observe\(.*inside.*loop (Prometheus observe in loop)
|
||||
\.increment\(.*inside.*loop (counter increment in loop)
|
||||
statsd\..*inside.*loop (StatsD in loop)
|
||||
\.timing\(.*inside.*loop (timing metric in loop)
|
||||
Date\.now\(\).*Date\.now\(\) (manual timing - use proper instrumentation)
|
||||
performance\.now\(\).*performance (manual performance timing)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Console.log**: Remove from production, use proper logger with levels
|
||||
2. **Log formatting**: Use lazy evaluation - logger.debug("msg %s", expensive_value) vs f-strings
|
||||
3. **Structured logging**: Use JSON structured logs for machine parsing
|
||||
4. **Async logging**: Buffer and flush logs asynchronously, don't block request handling
|
||||
5. **Request IDs**: Add correlation ID middleware, propagate through all service calls
|
||||
6. **Metrics**: Pre-aggregate metrics, use histograms instead of per-request timers in hot loops
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# Memory & Resource Management
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Memory Leaks
|
||||
```
|
||||
# Event listeners never removed
|
||||
addEventListener.*without.*removeEventListener
|
||||
\.on\(.*without.*\.off\(
|
||||
\.subscribe\(.*without.*unsubscribe
|
||||
# Timers never cleared
|
||||
setInterval\(.*without.*clearInterval
|
||||
setTimeout\(.*without.*clearTimeout
|
||||
# Global/module-level caches without eviction
|
||||
global.*\[\] (unbounded global arrays)
|
||||
global.*\{\} (unbounded global dicts/objects)
|
||||
module\.exports.*cache.*=.*\{\}
|
||||
_cache\s*=\s*\{\} (module-level cache without LRU/TTL)
|
||||
# Closures retaining references
|
||||
closure.*large.*object
|
||||
# React-specific
|
||||
useEffect.*without.*cleanup
|
||||
useRef.*large.*object
|
||||
```
|
||||
|
||||
### Unclosed Resources
|
||||
```
|
||||
open\(.*without.*close
|
||||
open\(.*without.*with\s (Python: not using context manager)
|
||||
new FileReader\(.*without.*close
|
||||
createReadStream\(.*without.*destroy
|
||||
createWriteStream\(.*without.*end
|
||||
fs\.open\(.*without.*fs\.close
|
||||
new Socket\(.*without.*\.close
|
||||
new WebSocket\(.*without.*\.close
|
||||
acquire\(.*without.*release
|
||||
```
|
||||
|
||||
### Large Allocations
|
||||
```
|
||||
new Array\(\d{5,} (arrays > 10k elements)
|
||||
Buffer\.alloc\(\d{6,} (buffers > 1MB)
|
||||
\.fill\(.*\d{6,} (filling large arrays)
|
||||
\.repeat\(\d{4,} (string repeat large count)
|
||||
JSON\.parse\(.*large (parsing large JSON in memory)
|
||||
\.readFileSync\( (synchronous large file reads)
|
||||
\.readFile\(.*without.*stream (reading whole file vs streaming)
|
||||
```
|
||||
|
||||
### String Concatenation in Loops
|
||||
```
|
||||
\+=.*string.*for
|
||||
\+=.*\".*loop
|
||||
str\s*\+= (Python string concat in loop)
|
||||
\.join\(\[ (check if used correctly)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Event listeners**: Always pair add/remove, use AbortController for bulk cleanup
|
||||
2. **Timers**: Clear intervals in cleanup/unmount, use refs for timer IDs
|
||||
3. **Caches**: Use LRU cache with max size, add TTL, use WeakMap/WeakRef where possible
|
||||
4. **File handling**: Use context managers (Python with), try-finally, or using statements
|
||||
5. **Streams**: Use streaming for large data instead of loading everything in memory
|
||||
6. **String building**: Use StringBuilder/list join pattern instead of concatenation in loops
|
||||
7. **Buffers**: Pool and reuse buffers, use streaming transforms
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
# Rendering & UI Performance
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### React Re-render Issues
|
||||
```
|
||||
# Missing memoization
|
||||
const\s+\w+\s*=\s*\(\s*\)\s*=>.*return\s*\( (inline component definitions)
|
||||
\w+\s*=\s*\{.*\}.*prop= (object literal as prop - new ref every render)
|
||||
\w+\s*=\s*\[.*\].*prop= (array literal as prop)
|
||||
\w+\s*=\s*\(\).*=>.*prop= (arrow function as prop - new ref every render)
|
||||
style=\{\{ (inline style object)
|
||||
# Context causing re-renders
|
||||
useContext\( (check context value stability)
|
||||
<\w+Provider\s+value=\{\{ (new object in Provider value)
|
||||
<\w+Provider\s+value=\{[^}]*\} (unstable provider value)
|
||||
# State management
|
||||
useState\(.*\{ (object state - check if needs splitting)
|
||||
setState\(.*\{\.\.\.state (spreading entire state on each update)
|
||||
```
|
||||
|
||||
### Missing Virtualization
|
||||
```
|
||||
\.map\(.*<\w+ (rendering list items - check list size)
|
||||
{items\.map\( (JSX list rendering - check if >50 items)
|
||||
\.map\(.*return.*<li (list rendering without virtualization)
|
||||
\.map\(.*return.*<tr (table row rendering without virtualization)
|
||||
\.map\(.*return.*<div (div list - check count)
|
||||
v-for= (Vue list rendering)
|
||||
ngFor (Angular list rendering)
|
||||
```
|
||||
|
||||
### Layout Thrashing
|
||||
```
|
||||
offsetWidth.*style\. (read then write in sequence)
|
||||
offsetHeight.*style\. (read then write)
|
||||
getBoundingClientRect.*style (read then write)
|
||||
clientWidth.*className (read then class change)
|
||||
scrollTop.*style (read then write)
|
||||
\.style\..*\.style\. (multiple style writes - batch with class)
|
||||
```
|
||||
|
||||
### Large DOM
|
||||
```
|
||||
document\.createElement.*loop (creating elements in loop)
|
||||
innerHTML\s*\+= (innerHTML concatenation - causes reparse)
|
||||
\.appendChild\(.*loop (appending in loop without fragment)
|
||||
document\.querySelector\(.*loop (DOM query in loop)
|
||||
\$\(.*\).*loop (jQuery selector in loop)
|
||||
```
|
||||
|
||||
### Missing Lazy Loading
|
||||
```
|
||||
<img\s+(?!.*loading) (images without loading="lazy")
|
||||
<iframe\s+(?!.*loading) (iframes without lazy loading)
|
||||
import.*above.*fold (heavy imports for below-fold content)
|
||||
```
|
||||
|
||||
### Animation Performance
|
||||
```
|
||||
# Layout-triggering animations
|
||||
animate.*width (animating width triggers layout)
|
||||
animate.*height (animating height triggers layout)
|
||||
animate.*top (animating top triggers layout)
|
||||
animate.*left (animating left triggers layout)
|
||||
animate.*margin (animating margin triggers layout)
|
||||
transition.*width (transitioning layout properties)
|
||||
transition.*height
|
||||
# Should use transform/opacity instead
|
||||
@keyframes.*\{.*(?:width|height|top|left|margin|padding)
|
||||
```
|
||||
|
||||
### SSR/Hydration Issues
|
||||
```
|
||||
useEffect\(.*\[\].*setState (client-side data fetch causing hydration mismatch)
|
||||
typeof window (window checks indicating SSR issues)
|
||||
document\. (direct document access in components)
|
||||
window\. (direct window access in components)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Re-renders**: Use React.memo, useMemo, useCallback for stable references
|
||||
2. **Context**: Split contexts by update frequency, memoize provider values
|
||||
3. **Virtualization**: Use react-window/react-virtuoso for lists > 50 items
|
||||
4. **Layout thrashing**: Batch reads, then batch writes; use requestAnimationFrame
|
||||
5. **DOM manipulation**: Use DocumentFragment, batch insertions, avoid innerHTML +=
|
||||
6. **Lazy loading**: Add loading="lazy" to images/iframes, use Intersection Observer
|
||||
7. **Animations**: Only animate transform and opacity (GPU-composited properties)
|
||||
8. **SSR**: Pre-fetch data server-side, avoid hydration mismatches
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# Security-Related Performance Issues
|
||||
|
||||
## Grep/Glob Patterns to Detect
|
||||
|
||||
### Cryptographic Misuse
|
||||
```
|
||||
md5\( (MD5 is fast but broken - use bcrypt/argon2 for passwords)
|
||||
sha1\( (SHA1 is weak)
|
||||
\.hashSync\(.*rounds.*[1-5]\b (bcrypt with low rounds)
|
||||
DES\b (DES is obsolete)
|
||||
Math\.random\(\).*token (Math.random for security tokens)
|
||||
Math\.random\(\).*password (Math.random for password generation)
|
||||
random\.random\(\).*secret (Python: insecure random for secrets)
|
||||
```
|
||||
|
||||
### Expensive Security Operations in Hot Paths
|
||||
```
|
||||
bcrypt.*inside.*loop (hashing in loop - expensive by design)
|
||||
jwt\.verify\(.*inside.*loop (JWT verification in loop)
|
||||
encrypt\(.*inside.*loop (encryption in loop)
|
||||
\.hash\(.*inside.*loop (hashing in loop)
|
||||
```
|
||||
|
||||
### Missing Rate Limiting
|
||||
```
|
||||
app\.(get|post|put|delete)\( (routes without rate limiting)
|
||||
@app\.route\( (Flask routes without rate limiting)
|
||||
router\.(get|post|put|delete)\( (Express routes without rate limiting)
|
||||
```
|
||||
|
||||
### SQL Injection Vectors (Also Performance)
|
||||
```
|
||||
f"SELECT.*\{ (Python f-string SQL)
|
||||
f"INSERT.*\{ (Python f-string SQL)
|
||||
`SELECT.*\$\{ (JS template literal SQL)
|
||||
"SELECT.*" \+ \w+ (string concat SQL)
|
||||
'SELECT.*' \+ \w+ (string concat SQL)
|
||||
\.raw\(.*\+ (raw query with concatenation)
|
||||
\.execute\(.*%.*% (Python format string SQL)
|
||||
```
|
||||
|
||||
### ReDoS Vulnerable Patterns
|
||||
```
|
||||
\(\.\*\)\+ (catastrophic backtracking)
|
||||
\(\.\+\)\+ (catastrophic backtracking)
|
||||
\([^)]*\|[^)]*\)\+ (alternation with repetition)
|
||||
\(\[.*\]\+\)\+ (nested quantifiers)
|
||||
new RegExp\(.*user (user input in regex)
|
||||
re\.compile\(.*user (Python: user input in regex)
|
||||
```
|
||||
|
||||
### N+1 Auth Checks
|
||||
```
|
||||
# Checking permissions inside loops
|
||||
\.can\(.*inside.*loop (permission check in loop)
|
||||
\.authorize\(.*inside.*loop (authorization in loop)
|
||||
isAllowed\(.*inside.*loop (permission check in loop)
|
||||
hasPermission\(.*inside.*loop (permission check in loop)
|
||||
```
|
||||
|
||||
## Improvement Strategies
|
||||
|
||||
1. **Crypto**: Use bcrypt/argon2 for passwords, SHA-256+ for hashing, crypto.randomBytes for tokens
|
||||
2. **Hot path crypto**: Cache JWT verification results, batch encrypt/decrypt
|
||||
3. **Rate limiting**: Add rate limiters (express-rate-limit, django-ratelimit, etc.)
|
||||
4. **SQL injection**: Use parameterized queries/prepared statements (also faster due to query plan caching)
|
||||
5. **ReDoS**: Audit regex patterns, use RE2 engine, set regex timeouts
|
||||
6. **Auth batching**: Batch permission checks, pre-load permissions per request
|
||||
441
src/resources/skills/core-web-vitals/SKILL.md
Normal file
441
src/resources/skills/core-web-vitals/SKILL.md
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
---
|
||||
name: core-web-vitals
|
||||
description: Optimize Core Web Vitals (LCP, INP, CLS) for better page experience and search ranking. Use when asked to "improve Core Web Vitals", "fix LCP", "reduce CLS", "optimize INP", "page experience optimization", or "fix layout shifts".
|
||||
license: MIT
|
||||
metadata:
|
||||
author: web-quality-skills
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Core Web Vitals optimization
|
||||
|
||||
Targeted optimization for the three Core Web Vitals metrics that affect Google Search ranking and user experience.
|
||||
|
||||
## The three metrics
|
||||
|
||||
| Metric | Measures | Good | Needs work | Poor |
|
||||
|--------|----------|------|------------|------|
|
||||
| **LCP** | Loading | ≤ 2.5s | 2.5s – 4s | > 4s |
|
||||
| **INP** | Interactivity | ≤ 200ms | 200ms – 500ms | > 500ms |
|
||||
| **CLS** | Visual Stability | ≤ 0.1 | 0.1 – 0.25 | > 0.25 |
|
||||
|
||||
Google measures at the **75th percentile** — 75% of page visits must meet "Good" thresholds.
|
||||
|
||||
---
|
||||
|
||||
## LCP: Largest Contentful Paint
|
||||
|
||||
LCP measures when the largest visible content element renders. Usually this is:
|
||||
- Hero image or video
|
||||
- Large text block
|
||||
- Background image
|
||||
- `<svg>` element
|
||||
|
||||
### Common LCP issues
|
||||
|
||||
**1. Slow server response (TTFB > 800ms)**
|
||||
```
|
||||
Fix: CDN, caching, optimized backend, edge rendering
|
||||
```
|
||||
|
||||
**2. Render-blocking resources**
|
||||
```html
|
||||
<!-- ❌ Blocks rendering -->
|
||||
<link rel="stylesheet" href="/all-styles.css">
|
||||
|
||||
<!-- ✅ Critical CSS inlined, rest deferred -->
|
||||
<style>/* Critical above-fold CSS */</style>
|
||||
<link rel="preload" href="/styles.css" as="style"
|
||||
onload="this.onload=null;this.rel='stylesheet'">
|
||||
```
|
||||
|
||||
**3. Slow resource load times**
|
||||
```html
|
||||
<!-- ❌ No hints, discovered late -->
|
||||
<img src="/hero.jpg" alt="Hero">
|
||||
|
||||
<!-- ✅ Preloaded with high priority -->
|
||||
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
|
||||
<img src="/hero.webp" alt="Hero" fetchpriority="high">
|
||||
```
|
||||
|
||||
**4. Client-side rendering delays**
|
||||
```javascript
|
||||
// ❌ Content loads after JavaScript
|
||||
useEffect(() => {
|
||||
fetch('/api/hero-text').then(r => r.json()).then(setHeroText);
|
||||
}, []);
|
||||
|
||||
// ✅ Server-side or static rendering
|
||||
// Use SSR, SSG, or streaming to send HTML with content
|
||||
export async function getServerSideProps() {
|
||||
const heroText = await fetchHeroText();
|
||||
return { props: { heroText } };
|
||||
}
|
||||
```
|
||||
|
||||
### LCP optimization checklist
|
||||
|
||||
```markdown
|
||||
- [ ] TTFB < 800ms (use CDN, edge caching)
|
||||
- [ ] LCP image preloaded with fetchpriority="high"
|
||||
- [ ] LCP image optimized (WebP/AVIF, correct size)
|
||||
- [ ] Critical CSS inlined (< 14KB)
|
||||
- [ ] No render-blocking JavaScript in <head>
|
||||
- [ ] Fonts don't block text rendering (font-display: swap)
|
||||
- [ ] LCP element in initial HTML (not JS-rendered)
|
||||
```
|
||||
|
||||
### LCP element identification
|
||||
```javascript
|
||||
// Find your LCP element
|
||||
new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
console.log('LCP element:', lastEntry.element);
|
||||
console.log('LCP time:', lastEntry.startTime);
|
||||
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## INP: Interaction to Next Paint
|
||||
|
||||
INP measures responsiveness across ALL interactions (clicks, taps, key presses) during a page visit. It reports the worst interaction (at 98th percentile for high-traffic pages).
|
||||
|
||||
### INP breakdown
|
||||
|
||||
Total INP = **Input Delay** + **Processing Time** + **Presentation Delay**
|
||||
|
||||
| Phase | Target | Optimization |
|
||||
|-------|--------|--------------|
|
||||
| Input Delay | < 50ms | Reduce main thread blocking |
|
||||
| Processing | < 100ms | Optimize event handlers |
|
||||
| Presentation | < 50ms | Minimize rendering work |
|
||||
|
||||
### Common INP issues
|
||||
|
||||
**1. Long tasks blocking main thread**
|
||||
```javascript
|
||||
// ❌ Long synchronous task
|
||||
function processLargeArray(items) {
|
||||
items.forEach(item => expensiveOperation(item));
|
||||
}
|
||||
|
||||
// ✅ Break into chunks with yielding
|
||||
async function processLargeArray(items) {
|
||||
const CHUNK_SIZE = 100;
|
||||
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
|
||||
const chunk = items.slice(i, i + CHUNK_SIZE);
|
||||
chunk.forEach(item => expensiveOperation(item));
|
||||
|
||||
// Yield to main thread
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
// Or use scheduler.yield() when available
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**2. Heavy event handlers**
|
||||
```javascript
|
||||
// ❌ All work in handler
|
||||
button.addEventListener('click', () => {
|
||||
// Heavy computation
|
||||
const result = calculateComplexThing();
|
||||
// DOM updates
|
||||
updateUI(result);
|
||||
// Analytics
|
||||
trackEvent('click');
|
||||
});
|
||||
|
||||
// ✅ Prioritize visual feedback
|
||||
button.addEventListener('click', () => {
|
||||
// Immediate visual feedback
|
||||
button.classList.add('loading');
|
||||
|
||||
// Defer non-critical work
|
||||
requestAnimationFrame(() => {
|
||||
const result = calculateComplexThing();
|
||||
updateUI(result);
|
||||
});
|
||||
|
||||
// Use requestIdleCallback for analytics
|
||||
requestIdleCallback(() => trackEvent('click'));
|
||||
});
|
||||
```
|
||||
|
||||
**3. Third-party scripts**
|
||||
```javascript
|
||||
// ❌ Eagerly loaded, blocks interactions
|
||||
<script src="https://heavy-widget.com/widget.js"></script>
|
||||
|
||||
// ✅ Lazy loaded on interaction or visibility
|
||||
const loadWidget = () => {
|
||||
import('https://heavy-widget.com/widget.js')
|
||||
.then(widget => widget.init());
|
||||
};
|
||||
button.addEventListener('click', loadWidget, { once: true });
|
||||
```
|
||||
|
||||
**4. Excessive re-renders (React/Vue)**
|
||||
```javascript
|
||||
// ❌ Re-renders entire tree
|
||||
function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
return (
|
||||
<div>
|
||||
<Counter count={count} />
|
||||
<ExpensiveComponent /> {/* Re-renders on every count change */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ Memoized expensive components
|
||||
const MemoizedExpensive = React.memo(ExpensiveComponent);
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
return (
|
||||
<div>
|
||||
<Counter count={count} />
|
||||
<MemoizedExpensive />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### INP optimization checklist
|
||||
|
||||
```markdown
|
||||
- [ ] No tasks > 50ms on main thread
|
||||
- [ ] Event handlers complete quickly (< 100ms)
|
||||
- [ ] Visual feedback provided immediately
|
||||
- [ ] Heavy work deferred with requestIdleCallback
|
||||
- [ ] Third-party scripts don't block interactions
|
||||
- [ ] Debounced input handlers where appropriate
|
||||
- [ ] Web Workers for CPU-intensive operations
|
||||
```
|
||||
|
||||
### INP debugging
|
||||
```javascript
|
||||
// Identify slow interactions
|
||||
new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.duration > 200) {
|
||||
console.warn('Slow interaction:', {
|
||||
type: entry.name,
|
||||
duration: entry.duration,
|
||||
processingStart: entry.processingStart,
|
||||
processingEnd: entry.processingEnd,
|
||||
target: entry.target
|
||||
});
|
||||
}
|
||||
}
|
||||
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLS: Cumulative Layout Shift
|
||||
|
||||
CLS measures unexpected layout shifts. A shift occurs when a visible element changes position between frames without user interaction.
|
||||
|
||||
**CLS Formula:** `impact fraction × distance fraction`
|
||||
|
||||
### Common CLS causes
|
||||
|
||||
**1. Images without dimensions**
|
||||
```html
|
||||
<!-- ❌ Causes layout shift when loaded -->
|
||||
<img src="photo.jpg" alt="Photo">
|
||||
|
||||
<!-- ✅ Space reserved -->
|
||||
<img src="photo.jpg" alt="Photo" width="800" height="600">
|
||||
|
||||
<!-- ✅ Or use aspect-ratio -->
|
||||
<img src="photo.jpg" alt="Photo" style="aspect-ratio: 4/3; width: 100%;">
|
||||
```
|
||||
|
||||
**2. Ads, embeds, and iframes**
|
||||
```html
|
||||
<!-- ❌ Unknown size until loaded -->
|
||||
<iframe src="https://ad-network.com/ad"></iframe>
|
||||
|
||||
<!-- ✅ Reserve space with min-height -->
|
||||
<div style="min-height: 250px;">
|
||||
<iframe src="https://ad-network.com/ad" height="250"></iframe>
|
||||
</div>
|
||||
|
||||
<!-- ✅ Or use aspect-ratio container -->
|
||||
<div style="aspect-ratio: 16/9;">
|
||||
<iframe src="https://youtube.com/embed/..."
|
||||
style="width: 100%; height: 100%;"></iframe>
|
||||
</div>
|
||||
```
|
||||
|
||||
**3. Dynamically injected content**
|
||||
```javascript
|
||||
// ❌ Inserts content above viewport
|
||||
notifications.prepend(newNotification);
|
||||
|
||||
// ✅ Insert below viewport or use transform
|
||||
const insertBelow = viewport.bottom < newNotification.top;
|
||||
if (insertBelow) {
|
||||
notifications.prepend(newNotification);
|
||||
} else {
|
||||
// Animate in without shifting
|
||||
newNotification.style.transform = 'translateY(-100%)';
|
||||
notifications.prepend(newNotification);
|
||||
requestAnimationFrame(() => {
|
||||
newNotification.style.transform = '';
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**4. Web fonts causing FOUT**
|
||||
```css
|
||||
/* ❌ Font swap shifts text */
|
||||
@font-face {
|
||||
font-family: 'Custom';
|
||||
src: url('custom.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* ✅ Optional font (no shift if slow) */
|
||||
@font-face {
|
||||
font-family: 'Custom';
|
||||
src: url('custom.woff2') format('woff2');
|
||||
font-display: optional;
|
||||
}
|
||||
|
||||
/* ✅ Or match fallback metrics */
|
||||
@font-face {
|
||||
font-family: 'Custom';
|
||||
src: url('custom.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
size-adjust: 105%; /* Match fallback size */
|
||||
ascent-override: 95%;
|
||||
descent-override: 20%;
|
||||
}
|
||||
```
|
||||
|
||||
**5. Animations triggering layout**
|
||||
```css
|
||||
/* ❌ Animates layout properties */
|
||||
.animate {
|
||||
transition: height 0.3s, width 0.3s;
|
||||
}
|
||||
|
||||
/* ✅ Use transform instead */
|
||||
.animate {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.animate.expanded {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
```
|
||||
|
||||
### CLS optimization checklist
|
||||
|
||||
```markdown
|
||||
- [ ] All images have width/height or aspect-ratio
|
||||
- [ ] All videos/embeds have reserved space
|
||||
- [ ] Ads have min-height containers
|
||||
- [ ] Fonts use font-display: optional or matched metrics
|
||||
- [ ] Dynamic content inserted below viewport
|
||||
- [ ] Animations use transform/opacity only
|
||||
- [ ] No content injected above existing content
|
||||
```
|
||||
|
||||
### CLS debugging
|
||||
```javascript
|
||||
// Track layout shifts
|
||||
new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (!entry.hadRecentInput) {
|
||||
console.log('Layout shift:', entry.value);
|
||||
entry.sources?.forEach(source => {
|
||||
console.log(' Shifted element:', source.node);
|
||||
console.log(' Previous rect:', source.previousRect);
|
||||
console.log(' Current rect:', source.currentRect);
|
||||
});
|
||||
}
|
||||
}
|
||||
}).observe({ type: 'layout-shift', buffered: true });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Measurement tools
|
||||
|
||||
### Lab testing
|
||||
- **Chrome DevTools** → Performance panel, Lighthouse
|
||||
- **WebPageTest** → Detailed waterfall, filmstrip
|
||||
- **Lighthouse CLI** → `npx lighthouse <url>`
|
||||
|
||||
### Field data (real users)
|
||||
- **Chrome User Experience Report (CrUX)** → BigQuery or API
|
||||
- **Search Console** → Core Web Vitals report
|
||||
- **web-vitals library** → Send to your analytics
|
||||
|
||||
```javascript
|
||||
import {onLCP, onINP, onCLS} from 'web-vitals';
|
||||
|
||||
function sendToAnalytics({name, value, rating}) {
|
||||
gtag('event', name, {
|
||||
event_category: 'Web Vitals',
|
||||
value: Math.round(name === 'CLS' ? value * 1000 : value),
|
||||
event_label: rating
|
||||
});
|
||||
}
|
||||
|
||||
onLCP(sendToAnalytics);
|
||||
onINP(sendToAnalytics);
|
||||
onCLS(sendToAnalytics);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Framework quick fixes
|
||||
|
||||
### Next.js
|
||||
```jsx
|
||||
// LCP: Use next/image with priority
|
||||
import Image from 'next/image';
|
||||
<Image src="/hero.jpg" priority fill alt="Hero" />
|
||||
|
||||
// INP: Use dynamic imports
|
||||
const HeavyComponent = dynamic(() => import('./Heavy'), { ssr: false });
|
||||
|
||||
// CLS: Image component handles dimensions automatically
|
||||
```
|
||||
|
||||
### React
|
||||
```jsx
|
||||
// LCP: Preload in head
|
||||
<link rel="preload" href="/hero.jpg" as="image" fetchpriority="high" />
|
||||
|
||||
// INP: Memoize and useTransition
|
||||
const [isPending, startTransition] = useTransition();
|
||||
startTransition(() => setExpensiveState(newValue));
|
||||
|
||||
// CLS: Always specify dimensions in img tags
|
||||
```
|
||||
|
||||
### Vue/Nuxt
|
||||
```vue
|
||||
<!-- LCP: Use nuxt/image with preload -->
|
||||
<NuxtImg src="/hero.jpg" preload loading="eager" />
|
||||
|
||||
<!-- INP: Use async components -->
|
||||
<component :is="() => import('./Heavy.vue')" />
|
||||
|
||||
<!-- CLS: Use aspect-ratio CSS -->
|
||||
<img :style="{ aspectRatio: '16/9' }" />
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [web.dev LCP](https://web.dev/articles/lcp)
|
||||
- [web.dev INP](https://web.dev/articles/inp)
|
||||
- [web.dev CLS](https://web.dev/articles/cls)
|
||||
- [Performance skill](../performance/SKILL.md)
|
||||
208
src/resources/skills/core-web-vitals/references/LCP.md
Normal file
208
src/resources/skills/core-web-vitals/references/LCP.md
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# LCP optimization reference
|
||||
|
||||
## What is LCP?
|
||||
|
||||
Largest Contentful Paint (LCP) measures when the largest content element in the viewport becomes visible. This is typically:
|
||||
|
||||
- An `<img>` element
|
||||
- An `<image>` element inside `<svg>`
|
||||
- A `<video>` element with poster image
|
||||
- An element with a background image via `url()`
|
||||
- A block-level element containing text nodes
|
||||
|
||||
## LCP timeline
|
||||
|
||||
```
|
||||
[ Server Response ][ Resource Load ][ Render ]
|
||||
TTFB Download Paint
|
||||
└─────────────────────────────────────┘
|
||||
LCP Time
|
||||
```
|
||||
|
||||
## Detailed optimizations
|
||||
|
||||
### 1. Server response time (TTFB)
|
||||
|
||||
Target: < 800ms
|
||||
|
||||
**Causes:**
|
||||
- Slow server/database queries
|
||||
- No CDN/edge caching
|
||||
- Inefficient backend code
|
||||
- Cold starts (serverless)
|
||||
|
||||
**Solutions:**
|
||||
```javascript
|
||||
// Use edge functions for dynamic content
|
||||
// Vercel example
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
// Use stale-while-revalidate caching
|
||||
// Cache-Control header
|
||||
res.setHeader('Cache-Control', 's-maxage=60, stale-while-revalidate=300');
|
||||
```
|
||||
|
||||
### 2. Resource load time
|
||||
|
||||
**For images:**
|
||||
```html
|
||||
<!-- Preload LCP image -->
|
||||
<link rel="preload" as="image" href="/hero.webp"
|
||||
imagesrcset="/hero-400.webp 400w, /hero-800.webp 800w"
|
||||
imagesizes="100vw"
|
||||
fetchpriority="high">
|
||||
|
||||
<!-- Modern format with fallback -->
|
||||
<picture>
|
||||
<source srcset="/hero.avif" type="image/avif">
|
||||
<source srcset="/hero.webp" type="image/webp">
|
||||
<img src="/hero.jpg" width="1200" height="600"
|
||||
fetchpriority="high" alt="Hero">
|
||||
</picture>
|
||||
```
|
||||
|
||||
**For text (web fonts):**
|
||||
```css
|
||||
@font-face {
|
||||
font-family: 'Heading';
|
||||
src: url('/fonts/heading.woff2') format('woff2');
|
||||
font-display: swap; /* Show fallback immediately */
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Render blocking resources
|
||||
|
||||
**Critical CSS pattern:**
|
||||
```html
|
||||
<head>
|
||||
<!-- Inline critical CSS -->
|
||||
<style>
|
||||
/* Only above-fold styles, < 14KB */
|
||||
.hero { /* ... */ }
|
||||
.nav { /* ... */ }
|
||||
</style>
|
||||
|
||||
<!-- Defer non-critical CSS -->
|
||||
<link rel="preload" href="/styles.css" as="style"
|
||||
onload="this.onload=null;this.rel='stylesheet'">
|
||||
</head>
|
||||
```
|
||||
|
||||
**Defer JavaScript:**
|
||||
```html
|
||||
<!-- ❌ Blocks parsing -->
|
||||
<script src="/app.js"></script>
|
||||
|
||||
<!-- ✅ Deferred (runs after HTML parsed) -->
|
||||
<script defer src="/app.js"></script>
|
||||
|
||||
<!-- ✅ Module (deferred by default) -->
|
||||
<script type="module" src="/app.mjs"></script>
|
||||
```
|
||||
|
||||
### 4. Client-side rendering
|
||||
|
||||
**Problem:** Content not in initial HTML.
|
||||
|
||||
**Solutions:**
|
||||
|
||||
**Server-side rendering (SSR):**
|
||||
```javascript
|
||||
// Next.js
|
||||
export async function getServerSideProps() {
|
||||
const data = await fetchHeroContent();
|
||||
return { props: { hero: data } };
|
||||
}
|
||||
```
|
||||
|
||||
**Static site generation (SSG):**
|
||||
```javascript
|
||||
// Next.js
|
||||
export async function getStaticProps() {
|
||||
const data = await fetchHeroContent();
|
||||
return { props: { hero: data }, revalidate: 3600 };
|
||||
}
|
||||
```
|
||||
|
||||
**Streaming SSR:**
|
||||
```jsx
|
||||
// React 18+
|
||||
import { Suspense } from 'react';
|
||||
|
||||
function Page() {
|
||||
return (
|
||||
<Suspense fallback={<HeroSkeleton />}>
|
||||
<Hero />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Framework-specific tips
|
||||
|
||||
### Next.js
|
||||
```jsx
|
||||
import Image from 'next/image';
|
||||
|
||||
// LCP image with priority
|
||||
<Image
|
||||
src="/hero.jpg"
|
||||
priority
|
||||
fill
|
||||
sizes="100vw"
|
||||
alt="Hero"
|
||||
/>
|
||||
```
|
||||
|
||||
### Nuxt
|
||||
```vue
|
||||
<NuxtImg
|
||||
src="/hero.jpg"
|
||||
preload
|
||||
loading="eager"
|
||||
sizes="100vw"
|
||||
/>
|
||||
```
|
||||
|
||||
### Astro
|
||||
```astro
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import hero from '../assets/hero.jpg';
|
||||
---
|
||||
<Image
|
||||
src={hero}
|
||||
loading="eager"
|
||||
decoding="sync"
|
||||
alt="Hero"
|
||||
/>
|
||||
```
|
||||
|
||||
## Debugging LCP
|
||||
|
||||
```javascript
|
||||
// Identify LCP element
|
||||
new PerformanceObserver((entryList) => {
|
||||
const entries = entryList.getEntries();
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
|
||||
console.log('LCP:', {
|
||||
element: lastEntry.element,
|
||||
time: lastEntry.startTime,
|
||||
size: lastEntry.size,
|
||||
url: lastEntry.url,
|
||||
renderTime: lastEntry.renderTime,
|
||||
loadTime: lastEntry.loadTime
|
||||
});
|
||||
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
||||
```
|
||||
|
||||
## Common issues
|
||||
|
||||
| Issue | Impact | Fix |
|
||||
|-------|--------|-----|
|
||||
| No preload for LCP image | +500-1000ms | Add `<link rel="preload">` |
|
||||
| Large unoptimized image | +300-800ms | Compress, use WebP/AVIF |
|
||||
| Render-blocking CSS | +200-500ms | Inline critical CSS |
|
||||
| Slow TTFB | +300-2000ms | CDN, edge caching |
|
||||
| Client-rendered content | +500-2000ms | SSR/SSG |
|
||||
122
src/resources/skills/make-interfaces-feel-better/SKILL.md
Normal file
122
src/resources/skills/make-interfaces-feel-better/SKILL.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
name: make-interfaces-feel-better
|
||||
description: Design engineering principles for making interfaces feel polished. Use when building UI components, reviewing frontend code, implementing animations, hover states, shadows, borders, typography, micro-interactions, enter/exit animations, or any visual detail work. Triggers on UI polish, design details, "make it feel better", "feels off", stagger animations, border radius, optical alignment, font smoothing, tabular numbers, image outlines, box shadows.
|
||||
---
|
||||
|
||||
# Details that make interfaces feel better
|
||||
|
||||
Great interfaces rarely come from a single thing. It's usually a collection of small details that compound into a great experience. Apply these principles when building or reviewing UI code.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Category | When to Use |
|
||||
| --- | --- |
|
||||
| [Typography](typography.md) | Text wrapping, font smoothing, tabular numbers |
|
||||
| [Surfaces](surfaces.md) | Border radius, optical alignment, shadows, image outlines, hit areas |
|
||||
| [Animations](animations.md) | Interruptible animations, enter/exit transitions, icon animations, scale on press |
|
||||
| [Performance](performance.md) | Transition specificity, `will-change` usage |
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Concentric Border Radius
|
||||
|
||||
Outer radius = inner radius + padding. Mismatched radii on nested elements is the most common thing that makes interfaces feel off.
|
||||
|
||||
### 2. Optical Over Geometric Alignment
|
||||
|
||||
When geometric centering looks off, align optically. Buttons with icons, play triangles, and asymmetric icons all need manual adjustment.
|
||||
|
||||
### 3. Shadows Over Borders
|
||||
|
||||
Layer multiple transparent `box-shadow` values for natural depth. Shadows adapt to any background; solid borders don't.
|
||||
|
||||
### 4. Interruptible Animations
|
||||
|
||||
Use CSS transitions for interactive state changes — they can be interrupted mid-animation. Reserve keyframes for staged sequences that run once.
|
||||
|
||||
### 5. Split and Stagger Enter Animations
|
||||
|
||||
Don't animate a single container. Break content into semantic chunks and stagger each with ~100ms delay.
|
||||
|
||||
### 6. Subtle Exit Animations
|
||||
|
||||
Use a small fixed `translateY` instead of full height. Exits should be softer than enters.
|
||||
|
||||
### 7. Contextual Icon Animations
|
||||
|
||||
Animate icons with `opacity`, `scale`, and `blur` instead of toggling visibility. Use exactly these values: scale from `0.25` to `1`, opacity from `0` to `1`, blur from `4px` to `0px`. If the project has `motion` or `framer-motion` in `package.json`, use `transition: { type: "spring", duration: 0.3, bounce: 0 }` — bounce must always be `0`. If no motion library is installed, keep both icons in the DOM (one absolute-positioned) and cross-fade with CSS transitions using `cubic-bezier(0.2, 0, 0, 1)` — this gives both enter and exit animations without any dependency.
|
||||
|
||||
### 8. Font Smoothing
|
||||
|
||||
Apply `-webkit-font-smoothing: antialiased` to the root layout on macOS for crisper text.
|
||||
|
||||
### 9. Tabular Numbers
|
||||
|
||||
Use `font-variant-numeric: tabular-nums` for any dynamically updating numbers to prevent layout shift.
|
||||
|
||||
### 10. Text Wrapping
|
||||
|
||||
Use `text-wrap: balance` on headings. Use `text-wrap: pretty` for body text to avoid orphans.
|
||||
|
||||
### 11. Image Outlines
|
||||
|
||||
Add a subtle `1px` outline with low opacity to images for consistent depth.
|
||||
|
||||
### 12. Scale on Press
|
||||
|
||||
A subtle `scale(0.96)` on click gives buttons tactile feedback. Always use `0.96`. Never use a value smaller than `0.95` — anything below feels exaggerated. Add a `static` prop to disable it when motion would be distracting.
|
||||
|
||||
### 13. Skip Animation on Page Load
|
||||
|
||||
Use `initial={false}` on `AnimatePresence` to prevent enter animations on first render. Verify it doesn't break intentional entrance animations.
|
||||
|
||||
### 14. Never Use `transition: all`
|
||||
|
||||
Always specify exact properties: `transition-property: scale, opacity`. Tailwind's `transition-transform` covers `transform, translate, scale, rotate`.
|
||||
|
||||
### 15. Use `will-change` Sparingly
|
||||
|
||||
Only for `transform`, `opacity`, `filter` — properties the GPU can composite. Never use `will-change: all`. Only add when you notice first-frame stutter.
|
||||
|
||||
### 16. Minimum Hit Area
|
||||
|
||||
Interactive elements need at least 40×40px hit area. Extend with a pseudo-element if the visible element is smaller. Never let hit areas of two elements overlap.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Fix |
|
||||
| --- | --- |
|
||||
| Same border radius on parent and child | Calculate `outerRadius = innerRadius + padding` |
|
||||
| Icons look off-center | Adjust optically with padding or fix SVG directly |
|
||||
| Hard borders between sections | Use layered `box-shadow` with transparency |
|
||||
| Jarring enter/exit animations | Split, stagger, and keep exits subtle |
|
||||
| Numbers cause layout shift | Apply `tabular-nums` |
|
||||
| Heavy text on macOS | Apply `antialiased` to root |
|
||||
| Animation plays on page load | Add `initial={false}` to `AnimatePresence` |
|
||||
| `transition: all` on elements | Specify exact properties |
|
||||
| First-frame animation stutter | Add `will-change: transform` (sparingly) |
|
||||
| Tiny hit areas on small controls | Extend with pseudo-element to 40×40px |
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- [ ] Nested rounded elements use concentric border radius
|
||||
- [ ] Icons are optically centered, not just geometrically
|
||||
- [ ] Shadows used instead of borders where appropriate
|
||||
- [ ] Enter animations are split and staggered
|
||||
- [ ] Exit animations are subtle
|
||||
- [ ] Dynamic numbers use tabular-nums
|
||||
- [ ] Font smoothing is applied
|
||||
- [ ] Headings use text-wrap: balance
|
||||
- [ ] Images have subtle outlines
|
||||
- [ ] Buttons use scale on press where appropriate
|
||||
- [ ] AnimatePresence uses `initial={false}` for default-state elements
|
||||
- [ ] No `transition: all` — only specific properties
|
||||
- [ ] `will-change` only on transform/opacity/filter, never `all`
|
||||
- [ ] Interactive elements have at least 40×40px hit area
|
||||
|
||||
## Reference Files
|
||||
|
||||
- [typography.md](typography.md) — Text wrapping, font smoothing, tabular numbers
|
||||
- [surfaces.md](surfaces.md) — Border radius, optical alignment, shadows, image outlines
|
||||
- [animations.md](animations.md) — Interruptible animations, enter/exit transitions, icon animations, scale on press
|
||||
- [performance.md](performance.md) — Transition specificity, `will-change` usage
|
||||
379
src/resources/skills/make-interfaces-feel-better/animations.md
Normal file
379
src/resources/skills/make-interfaces-feel-better/animations.md
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
# Animations
|
||||
|
||||
Interruptible animations, enter/exit transitions, and contextual icon animations.
|
||||
|
||||
## Interruptible Animations
|
||||
|
||||
Users change intent mid-interaction. If animations aren't interruptible, the interface feels broken.
|
||||
|
||||
### CSS Transitions vs. Keyframes
|
||||
|
||||
| | CSS Transitions | CSS Keyframe Animations |
|
||||
| --- | --- | --- |
|
||||
| **Behavior** | Interpolate toward latest state | Run on a fixed timeline |
|
||||
| **Interruptible** | Yes — retargets mid-animation | No — restarts from beginning |
|
||||
| **Use for** | Interactive state changes (hover, toggle, open/close) | Staged sequences that run once (enter animations, loading) |
|
||||
| **Duration** | Adapts to remaining distance | Fixed regardless of state |
|
||||
|
||||
```css
|
||||
/* Good — interruptible transition for a toggle */
|
||||
.drawer {
|
||||
transform: translateX(-100%);
|
||||
transition: transform 200ms ease-out;
|
||||
}
|
||||
.drawer.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Clicking again mid-animation smoothly reverses — no jank */
|
||||
```
|
||||
|
||||
```css
|
||||
/* Bad — keyframe animation for interactive element */
|
||||
.drawer.open {
|
||||
animation: slideIn 200ms ease-out forwards;
|
||||
}
|
||||
|
||||
/* Closing mid-animation snaps or restarts — feels broken */
|
||||
```
|
||||
|
||||
**Rule:** Always prefer CSS transitions for interactive elements. Reserve keyframes for one-shot sequences.
|
||||
|
||||
## Enter Animations: Split and Stagger
|
||||
|
||||
Don't animate a single large container. Break content into semantic chunks and animate each individually.
|
||||
|
||||
### Step by Step
|
||||
|
||||
1. **Split** into logical groups (title, description, buttons)
|
||||
2. **Stagger** with ~100ms delay between groups
|
||||
3. **For titles**, consider splitting into individual words with ~80ms stagger
|
||||
4. **Combine** `opacity`, `blur`, and `translateY` for the enter effect
|
||||
|
||||
### Code Example
|
||||
|
||||
```tsx
|
||||
// Motion (Framer Motion) — staggered enter
|
||||
function PageHeader() {
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
visible: { transition: { staggerChildren: 0.1 } },
|
||||
}}
|
||||
>
|
||||
<motion.h1
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 12, filter: "blur(4px)" },
|
||||
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
|
||||
}}
|
||||
>
|
||||
Welcome
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 12, filter: "blur(4px)" },
|
||||
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
|
||||
}}
|
||||
>
|
||||
A description of the page.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 12, filter: "blur(4px)" },
|
||||
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
|
||||
}}
|
||||
>
|
||||
<Button>Get started</Button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### CSS-Only Stagger
|
||||
|
||||
```css
|
||||
.stagger-item {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
filter: blur(4px);
|
||||
animation: fadeInUp 400ms ease-out forwards;
|
||||
}
|
||||
|
||||
.stagger-item:nth-child(1) { animation-delay: 0ms; }
|
||||
.stagger-item:nth-child(2) { animation-delay: 100ms; }
|
||||
.stagger-item:nth-child(3) { animation-delay: 200ms; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Exit Animations
|
||||
|
||||
Exit animations should be softer and less attention-grabbing than enter animations. The user's focus is moving to the next thing — don't fight for attention.
|
||||
|
||||
### Subtle Exit (Recommended)
|
||||
|
||||
```tsx
|
||||
// Small fixed translateY — indicates direction without drama
|
||||
<motion.div
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: -12,
|
||||
filter: "blur(4px)",
|
||||
transition: { duration: 0.15, ease: "easeIn" },
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</motion.div>
|
||||
```
|
||||
|
||||
### Full Exit (When Context Matters)
|
||||
|
||||
```tsx
|
||||
// Slide fully out — use when spatial context is important
|
||||
// (e.g., a card returning to a list, a drawer closing)
|
||||
<motion.div
|
||||
exit={{
|
||||
opacity: 0,
|
||||
x: "-100%",
|
||||
transition: { duration: 0.2, ease: "easeIn" },
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</motion.div>
|
||||
```
|
||||
|
||||
### Good vs. Bad
|
||||
|
||||
```css
|
||||
/* Good — subtle exit */
|
||||
.item-exit {
|
||||
opacity: 0;
|
||||
transform: translateY(-12px);
|
||||
transition: opacity 150ms ease-in, transform 150ms ease-in;
|
||||
}
|
||||
|
||||
/* Bad — dramatic exit that steals focus */
|
||||
.item-exit {
|
||||
opacity: 0;
|
||||
transform: translateY(-100%) scale(0.5);
|
||||
transition: all 400ms ease-in;
|
||||
}
|
||||
|
||||
/* Bad — no exit animation at all (element just vanishes) */
|
||||
.item-exit {
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Use a small fixed `translateY` (e.g., `-12px`) instead of the full container height
|
||||
- Keep some directional movement to indicate where the element went
|
||||
- Exit duration should be shorter than enter duration (150ms vs 300ms)
|
||||
- Don't remove exit animations entirely — subtle motion preserves context
|
||||
|
||||
## Contextual Icon Animations
|
||||
|
||||
When icons appear or disappear contextually (on hover, on state change), animate them with `opacity`, `scale`, and `blur` rather than just toggling visibility.
|
||||
|
||||
### Motion Example
|
||||
|
||||
```tsx
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
function IconButton({ isActive, icon: Icon }) {
|
||||
return (
|
||||
<button>
|
||||
<AnimatePresence mode="popLayout">
|
||||
<motion.span
|
||||
key={isActive ? "active" : "inactive"}
|
||||
initial={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }}
|
||||
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
|
||||
exit={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }}
|
||||
transition={{ type: "spring", duration: 0.3, bounce: 0 }}
|
||||
>
|
||||
<Icon />
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Transition Approach (No Motion)
|
||||
|
||||
If the project doesn't use Motion (Framer Motion), keep both icons in the DOM and cross-fade them with CSS transitions. Because neither icon unmounts, both enter and exit animate smoothly.
|
||||
|
||||
The trick: one icon is absolutely positioned on top of the other. Toggling state cross-fades them — the entering icon scales up from `0.25` while the exiting icon scales down to `0.25`, both with opacity and blur.
|
||||
|
||||
```tsx
|
||||
function IconButton({ isActive, ActiveIcon, InactiveIcon }) {
|
||||
return (
|
||||
<button>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex items-center justify-center",
|
||||
"transition-[opacity,filter,scale] duration-300",
|
||||
"cubic-bezier(0.2, 0, 0, 1)",
|
||||
isActive
|
||||
? "scale-100 opacity-100 blur-0"
|
||||
: "scale-[0.25] opacity-0 blur-[4px]"
|
||||
)}
|
||||
>
|
||||
<ActiveIcon />
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-[opacity,filter,scale] duration-300",
|
||||
"cubic-bezier(0.2, 0, 0, 1)",
|
||||
isActive
|
||||
? "scale-[0.25] opacity-0 blur-[4px]"
|
||||
: "scale-100 opacity-100 blur-0"
|
||||
)}
|
||||
>
|
||||
<InactiveIcon />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The non-absolute icon (InactiveIcon) defines the layout size. The absolute icon (ActiveIcon) overlays it without affecting flow.
|
||||
|
||||
### Choosing Between Motion and CSS
|
||||
|
||||
| | Motion (Framer Motion) | CSS transitions (both icons in DOM) |
|
||||
| --- | --- | --- |
|
||||
| **Enter animation** | Yes | Yes |
|
||||
| **Exit animation** | Yes (via `AnimatePresence`) | Yes (cross-fade — icon never unmounts) |
|
||||
| **Spring physics** | Yes | No — use `cubic-bezier(0.2, 0, 0, 1)` as approximation |
|
||||
| **When to use** | Project already uses `motion/react` | No motion dependency, or keeping bundle small |
|
||||
|
||||
**Rule:** Check the project's `package.json` for `motion` or `framer-motion`. If present, use the Motion approach. If not, use the CSS cross-fade pattern — don't add a dependency just for icon transitions.
|
||||
|
||||
### When to Animate Icons
|
||||
|
||||
| Animate | Don't animate |
|
||||
| --- | --- |
|
||||
| Icons that appear on hover (action buttons) | Static navigation icons |
|
||||
| State change icons (play → pause, like → liked) | Decorative icons |
|
||||
| Icons in contextual toolbars | Icons that are always visible |
|
||||
| Loading/success state indicators | Icon labels (text next to icon) |
|
||||
|
||||
**Important:** Always use exactly these values for contextual icon animations — do not deviate:
|
||||
- `scale`: `0.25` → `1` (never use `0.5` or `0.6`)
|
||||
- `opacity`: `0` → `1`
|
||||
- `filter`: `"blur(4px)"` → `"blur(0px)"`
|
||||
- `transition`: `{ type: "spring", duration: 0.3, bounce: 0 }` — **bounce must always be `0`**, never `0.1` or any other value
|
||||
|
||||
## Scale on Press
|
||||
|
||||
A subtle scale-down on click gives buttons tactile feedback. Always use `scale(0.96)`. Never use a value smaller than `0.95` — anything below feels exaggerated. Use CSS transitions for interruptibility — if the user releases mid-press, it should smoothly return.
|
||||
|
||||
Not every button needs this. Add a `static` prop to your button component that disables the scale effect when the motion would be distracting.
|
||||
|
||||
### CSS Example
|
||||
|
||||
```css
|
||||
.button {
|
||||
transition-property: scale;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.button:active {
|
||||
scale: 0.96;
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind Example
|
||||
|
||||
```tsx
|
||||
<button className="transition-transform duration-150 ease-out active:scale-[0.96]">
|
||||
Click me
|
||||
</button>
|
||||
```
|
||||
|
||||
### Motion Example
|
||||
|
||||
```tsx
|
||||
<motion.button whileTap={{ scale: 0.96 }}>
|
||||
Click me
|
||||
</motion.button>
|
||||
```
|
||||
|
||||
### Static Prop Pattern
|
||||
|
||||
Extract the scale class into a variable and conditionally apply it based on a `static` prop:
|
||||
|
||||
```tsx
|
||||
const tapScale = "active:not-disabled:scale-[0.96]";
|
||||
|
||||
function Button({ static: isStatic, className, children, ...props }) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"transition-transform duration-150 ease-out",
|
||||
!isStatic && tapScale,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
<Button>Click me</Button> {/* scales on press */}
|
||||
<Button static>Submit</Button> {/* no scale */}
|
||||
```
|
||||
|
||||
## Skip Animation on Page Load
|
||||
|
||||
Use `initial={false}` on `AnimatePresence` to prevent enter animations from firing on first render. Elements that are already in their default state shouldn't animate in on page load — only on subsequent state changes.
|
||||
|
||||
### When It Works
|
||||
|
||||
```tsx
|
||||
// Good — icon doesn't animate in on mount, only on state change
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
<motion.span
|
||||
key={isActive ? "active" : "inactive"}
|
||||
initial={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }}
|
||||
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
|
||||
exit={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }}
|
||||
>
|
||||
<Icon />
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
```
|
||||
|
||||
Works well for: icon swaps, toggles, tabs, segmented controls — anything that has a default state on page load.
|
||||
|
||||
### When It Breaks
|
||||
|
||||
Don't use `initial={false}` when the component relies on its `initial` prop to set up a first-time enter animation, like a staggered page hero or a loading state. In those cases, removing the initial animation skips the entire entrance.
|
||||
|
||||
```tsx
|
||||
// Bad — initial={false} would skip the staggered page enter entirely
|
||||
<AnimatePresence initial={false}>
|
||||
<motion.div initial="hidden" animate="visible" variants={...}>
|
||||
...
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
```
|
||||
|
||||
Verify the component still looks right on a full page refresh before applying this.
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
# Performance
|
||||
|
||||
Transition specificity and GPU compositing hints.
|
||||
|
||||
## Transition Only What Changes
|
||||
|
||||
Never use `transition: all` or Tailwind's `transition` shorthand (which maps to `transition-property: all`). Always specify the exact properties that change.
|
||||
|
||||
### Why
|
||||
|
||||
- `transition: all` forces the browser to watch every property for changes
|
||||
- Causes unexpected transitions on properties you didn't intend to animate (colors, padding, shadows)
|
||||
- Prevents browser optimizations
|
||||
|
||||
### CSS Example
|
||||
|
||||
```css
|
||||
/* Good — only transition what changes */
|
||||
.button {
|
||||
transition-property: scale, background-color;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
/* Bad — transition everything */
|
||||
.button {
|
||||
transition: all 150ms ease-out;
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind
|
||||
|
||||
```tsx
|
||||
// Good — explicit properties
|
||||
<button className="transition-[scale,background-color] duration-150 ease-out">
|
||||
|
||||
// Bad — transition all
|
||||
<button className="transition duration-150 ease-out">
|
||||
```
|
||||
|
||||
### Tailwind `transition-transform` Note
|
||||
|
||||
`transition-transform` in Tailwind maps to `transition-property: transform, translate, scale, rotate` — it covers all transform-related properties, not just `transform`. Use this when you're only animating transforms. For multiple non-transform properties, use the bracket syntax: `transition-[scale,opacity,filter]`.
|
||||
|
||||
## Use `will-change` Sparingly
|
||||
|
||||
`will-change` hints the browser to pre-promote an element to its own GPU compositing layer. Without it, the browser promotes the element only when the animation starts — that one-time layer promotion can cause a micro-stutter on the first frame.
|
||||
|
||||
This particularly helps when an element is changing `scale`, `rotation`, or moving around with `transform`. For other properties, it doesn't help much — the browser can't composite them on the GPU anyway.
|
||||
|
||||
### Rules
|
||||
|
||||
```css
|
||||
/* Good — specific property that benefits from GPU compositing */
|
||||
.animated-card {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Good — multiple compositor-friendly properties */
|
||||
.animated-card {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* Bad — never use will-change: all */
|
||||
.animated-card {
|
||||
will-change: all;
|
||||
}
|
||||
|
||||
/* Bad — properties that can't be GPU-composited anyway */
|
||||
.animated-card {
|
||||
will-change: background-color, padding;
|
||||
}
|
||||
```
|
||||
|
||||
### Useful Properties
|
||||
|
||||
| Property | GPU-compositable | Worth using `will-change` |
|
||||
| --- | --- | --- |
|
||||
| `transform` | Yes | Yes |
|
||||
| `opacity` | Yes | Yes |
|
||||
| `filter` (blur, brightness) | Yes | Yes |
|
||||
| `clip-path` | Yes | Yes |
|
||||
| `top`, `left`, `width`, `height` | No | No |
|
||||
| `background`, `border`, `color` | No | No |
|
||||
|
||||
### When to Skip
|
||||
|
||||
Modern browsers are already good at optimizing on their own. Only add `will-change` when you notice first-frame stutter — Safari in particular benefits from it. Don't add it preemptively to every animated element; each extra compositing layer costs memory.
|
||||
247
src/resources/skills/make-interfaces-feel-better/surfaces.md
Normal file
247
src/resources/skills/make-interfaces-feel-better/surfaces.md
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
# Surfaces
|
||||
|
||||
Border radius, optical alignment, shadows, and image outlines.
|
||||
|
||||
## Concentric Border Radius
|
||||
|
||||
When nesting rounded elements, the outer radius must equal the inner radius plus the padding between them:
|
||||
|
||||
```
|
||||
outerRadius = innerRadius + padding
|
||||
```
|
||||
|
||||
This rule is most useful when nested surfaces are close together. If padding is larger than `24px`, treat the layers as separate surfaces and choose each radius independently instead of forcing strict concentric math.
|
||||
|
||||
### Example
|
||||
|
||||
```css
|
||||
/* Good — concentric radii */
|
||||
.card {
|
||||
border-radius: 20px; /* 12 + 8 */
|
||||
padding: 8px;
|
||||
}
|
||||
.card-inner {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* Bad — same radius on both */
|
||||
.card {
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
}
|
||||
.card-inner {
|
||||
border-radius: 12px;
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind Example
|
||||
|
||||
```tsx
|
||||
// Good — outer radius accounts for padding
|
||||
<div className="rounded-2xl p-2"> {/* 16px radius, 8px padding */}
|
||||
<div className="rounded-lg"> {/* 8px radius = 16 - 8 ✓ */}
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Bad — same radius on both
|
||||
<div className="rounded-xl p-2">
|
||||
<div className="rounded-xl"> {/* same radius, looks off */}
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Mismatched border radii on nested elements is one of the most common things that makes interfaces feel off. Always calculate concentrically.
|
||||
|
||||
## Optical Alignment
|
||||
|
||||
When geometric centering looks off, align optically instead.
|
||||
|
||||
### Buttons with Text + Icon
|
||||
|
||||
Use slightly less padding on the icon side to make the button feel balanced. A reliable rule of thumb is:
|
||||
`icon-side padding = text-side padding - 2px`.
|
||||
|
||||
```css
|
||||
/* Good — less padding on icon side */
|
||||
.button-with-icon {
|
||||
padding-left: 16px;
|
||||
padding-right: 14px; /* icon side = text side - 2px */
|
||||
}
|
||||
|
||||
/* Bad — equal padding looks like icon is pushed too far right */
|
||||
.button-with-icon {
|
||||
padding: 0 16px;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Tailwind
|
||||
<button className="pl-4 pr-3.5 flex items-center gap-2">
|
||||
<span>Continue</span>
|
||||
<ArrowRightIcon />
|
||||
</button>
|
||||
```
|
||||
|
||||
### Play Button Triangles
|
||||
|
||||
Play icons are triangular and their geometric center is not their visual center. Shift slightly right:
|
||||
|
||||
```css
|
||||
/* Good — optically centered */
|
||||
.play-button svg {
|
||||
margin-left: 2px; /* shift right to account for triangle shape */
|
||||
}
|
||||
|
||||
/* Bad — geometrically centered but looks off */
|
||||
.play-button svg {
|
||||
/* no adjustment */
|
||||
}
|
||||
```
|
||||
|
||||
### Asymmetric Icons (Stars, Arrows, Carets)
|
||||
|
||||
Some icons have uneven visual weight. The best fix is adjusting the SVG directly so no extra margin/padding is needed in the component code.
|
||||
|
||||
```tsx
|
||||
// Best — fix in the SVG itself
|
||||
// Adjust the viewBox or path to visually center the icon
|
||||
|
||||
// Fallback — adjust with margin
|
||||
<span className="ml-px">
|
||||
<StarIcon />
|
||||
</span>
|
||||
```
|
||||
|
||||
## Shadows Instead of Borders
|
||||
|
||||
For **buttons, cards, and containers** that use a border for depth or elevation, prefer replacing it with a subtle `box-shadow`. Shadows adapt to any background since they use transparency; solid borders don't. This also helps when using images or multiple colors as backgrounds — solid border colors don't work well on backgrounds other than the ones they were designed for.
|
||||
|
||||
**Do not apply this to dividers** (`border-b`, `border-t`, side borders) or any border whose purpose is layout separation rather than element depth. Those should stay as borders.
|
||||
|
||||
### Shadow as Border (Light Mode)
|
||||
|
||||
The shadow is comprised of three layers. The first acts as a 1px border ring, the second adds subtle lift, and the third provides ambient depth:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--shadow-border:
|
||||
0px 0px 0px 1px rgba(0, 0, 0, 0.06),
|
||||
0px 1px 2px -1px rgba(0, 0, 0, 0.06),
|
||||
0px 2px 4px 0px rgba(0, 0, 0, 0.04);
|
||||
--shadow-border-hover:
|
||||
0px 0px 0px 1px rgba(0, 0, 0, 0.08),
|
||||
0px 1px 2px -1px rgba(0, 0, 0, 0.08),
|
||||
0px 2px 4px 0px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
```
|
||||
|
||||
### Shadow as Border (Dark Mode)
|
||||
|
||||
In dark mode, simplify to a single white ring — layered depth shadows aren't visible on dark backgrounds:
|
||||
|
||||
```css
|
||||
/* Dark mode — adapt to whatever setup the project uses
|
||||
(prefers-color-scheme, class, data attribute, etc.) */
|
||||
--shadow-border: 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
--shadow-border-hover: 0 0 0 1px rgba(255, 255, 255, 0.13);
|
||||
```
|
||||
|
||||
### Usage with Hover Transition
|
||||
|
||||
Apply the variable and add `transition-[box-shadow]` for a smooth hover:
|
||||
|
||||
```css
|
||||
.card {
|
||||
box-shadow: var(--shadow-border);
|
||||
transition-property: box-shadow;
|
||||
transition-duration: 150ms;
|
||||
transition-timing-function: ease-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-border-hover);
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use Shadows vs. Borders
|
||||
|
||||
| Use shadows | Use borders |
|
||||
| --- | --- |
|
||||
| Cards, containers with depth | Dividers between list items |
|
||||
| Buttons with bordered styles | Table cell boundaries |
|
||||
| Elevated elements (dropdowns, modals) | Form input outlines (for accessibility) |
|
||||
| Elements on varied backgrounds | Hairline separators in dense UI |
|
||||
| Hover/focus states for lift effect | |
|
||||
|
||||
## Image Outlines
|
||||
|
||||
Add a subtle `1px` outline with low opacity to images. This creates consistent depth, especially in design systems where other elements use borders or shadows.
|
||||
|
||||
### Light Mode
|
||||
|
||||
```css
|
||||
img {
|
||||
outline: 1px solid rgba(0, 0, 0, 0.1);
|
||||
outline-offset: -1px; /* inset so it doesn't add to layout */
|
||||
}
|
||||
```
|
||||
|
||||
### Dark Mode
|
||||
|
||||
```css
|
||||
img {
|
||||
outline: 1px solid rgba(255, 255, 255, 0.1);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind with Dark Mode
|
||||
|
||||
```tsx
|
||||
<img
|
||||
className="outline outline-1 -outline-offset-1 outline-black/10 dark:outline-white/10"
|
||||
src={src}
|
||||
alt={alt}
|
||||
/>
|
||||
```
|
||||
|
||||
**Why outline instead of border?** `outline` doesn't affect layout (no added width/height), and `outline-offset: -1px` keeps it inset so images stay their intended size.
|
||||
|
||||
## Minimum Hit Area
|
||||
|
||||
Interactive elements should have a minimum hit area of 44×44px (WCAG) or at least 40×40px. If the visible element is smaller (e.g., a 20×20 checkbox), extend the hit area with a pseudo-element.
|
||||
|
||||
### CSS Example
|
||||
|
||||
```css
|
||||
/* Small checkbox with expanded hit area */
|
||||
.checkbox {
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.checkbox::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
```
|
||||
|
||||
### Tailwind Example
|
||||
|
||||
```tsx
|
||||
<button className="relative size-5 after:absolute after:top-1/2 after:left-1/2 after:size-10 after:-translate-1/2">
|
||||
<CheckIcon />
|
||||
</button>
|
||||
```
|
||||
|
||||
### Collision Rule
|
||||
|
||||
If the extended hit area overlaps another interactive element, shrink the pseudo-element — but make it as large as possible without colliding. Two interactive elements should never have overlapping hit areas.
|
||||
123
src/resources/skills/make-interfaces-feel-better/typography.md
Normal file
123
src/resources/skills/make-interfaces-feel-better/typography.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# Typography
|
||||
|
||||
Typography rendering details that make interfaces feel better.
|
||||
|
||||
## Text Wrapping
|
||||
|
||||
### text-wrap: balance
|
||||
|
||||
Distributes text evenly across lines, preventing orphaned words on headings and short text blocks. **Only works on blocks of 6 lines or fewer** (Chromium) or 10 lines or fewer (Firefox) — the balancing algorithm is computationally expensive, so browsers limit it to short text.
|
||||
|
||||
```css
|
||||
/* Good — even line lengths on short text */
|
||||
h1, h2, h3 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* Bad — default wrapping leaves orphans */
|
||||
h1 {
|
||||
/* no text-wrap rule → "Read our
|
||||
blog" instead of balanced lines */
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* Bad — balance on long paragraphs (silently ignored, wastes intent) */
|
||||
.article-body p {
|
||||
text-wrap: balance;
|
||||
}
|
||||
```
|
||||
|
||||
**Tailwind:** `text-balance`
|
||||
|
||||
### text-wrap: pretty
|
||||
|
||||
Optimizes the last line to avoid orphans using a slower algorithm that favors better typography over performance. Unlike `balance`, it works on longer text — use this for body copy where you want to minimize orphans without the 6-line limit.
|
||||
|
||||
```css
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use Which
|
||||
|
||||
| Scenario | Use |
|
||||
| --- | --- |
|
||||
| Headings, titles, short text (≤6 lines) | `text-wrap: balance` |
|
||||
| Body paragraphs, descriptions | `text-wrap: pretty` |
|
||||
| Code blocks, pre-formatted text | Neither — leave default |
|
||||
|
||||
## Font Smoothing (macOS)
|
||||
|
||||
On macOS, text renders heavier than intended by default. Apply antialiased smoothing to the root layout so all text renders crisper and thinner.
|
||||
|
||||
```css
|
||||
/* CSS */
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Tailwind — apply to root layout
|
||||
<html className="antialiased">
|
||||
```
|
||||
|
||||
### Good vs. Bad
|
||||
|
||||
```css
|
||||
/* Good — applied once at the root */
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Bad — applied per-element, inconsistent */
|
||||
.heading {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.body {
|
||||
/* no smoothing → heavier than heading */
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** This only affects macOS rendering. Other platforms ignore these properties, so it's safe to apply universally.
|
||||
|
||||
## Tabular Numbers
|
||||
|
||||
When numbers update dynamically (counters, prices, timers, table columns), use tabular-nums to make all digits equal width. This prevents layout shift as values change.
|
||||
|
||||
```css
|
||||
/* CSS */
|
||||
.counter {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Tailwind
|
||||
<span className="tabular-nums">{count}</span>
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
| Use tabular-nums | Don't use tabular-nums |
|
||||
| --- | --- |
|
||||
| Counters and timers | Static display numbers |
|
||||
| Prices that update | Decorative large numbers |
|
||||
| Table columns with numbers | Phone numbers, zip codes |
|
||||
| Animated number transitions | Version numbers (v2.1.0) |
|
||||
| Scoreboards, dashboards | |
|
||||
|
||||
### Caveat
|
||||
|
||||
Some fonts (like Inter) change the visual appearance of numerals with this property — specifically, the digit `1` becomes wider and centered. This is expected behavior and usually desirable for alignment, but verify it looks right in your specific font.
|
||||
|
||||
```css
|
||||
/* With Inter font:
|
||||
Default: 1234 → proportional, "1" is narrow
|
||||
Tabular: 1234 → all digits equal width, "1" centered */
|
||||
```
|
||||
123
src/resources/skills/react-best-practices/README.md
Normal file
123
src/resources/skills/react-best-practices/README.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# React Best Practices
|
||||
|
||||
A structured repository for creating and maintaining React Best Practices optimized for agents and LLMs.
|
||||
|
||||
## Structure
|
||||
|
||||
- `rules/` - Individual rule files (one per rule)
|
||||
- `_sections.md` - Section metadata (titles, impacts, descriptions)
|
||||
- `_template.md` - Template for creating new rules
|
||||
- `area-description.md` - Individual rule files
|
||||
- `src/` - Build scripts and utilities
|
||||
- `metadata.json` - Document metadata (version, organization, abstract)
|
||||
- __`AGENTS.md`__ - Compiled output (generated)
|
||||
- __`test-cases.json`__ - Test cases for LLM evaluation (generated)
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. Build AGENTS.md from rules:
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
3. Validate rule files:
|
||||
```bash
|
||||
pnpm validate
|
||||
```
|
||||
|
||||
4. Extract test cases:
|
||||
```bash
|
||||
pnpm extract-tests
|
||||
```
|
||||
|
||||
## Creating a New Rule
|
||||
|
||||
1. Copy `rules/_template.md` to `rules/area-description.md`
|
||||
2. Choose the appropriate area prefix:
|
||||
- `async-` for Eliminating Waterfalls (Section 1)
|
||||
- `bundle-` for Bundle Size Optimization (Section 2)
|
||||
- `server-` for Server-Side Performance (Section 3)
|
||||
- `client-` for Client-Side Data Fetching (Section 4)
|
||||
- `rerender-` for Re-render Optimization (Section 5)
|
||||
- `rendering-` for Rendering Performance (Section 6)
|
||||
- `js-` for JavaScript Performance (Section 7)
|
||||
- `advanced-` for Advanced Patterns (Section 8)
|
||||
3. Fill in the frontmatter and content
|
||||
4. Ensure you have clear examples with explanations
|
||||
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
|
||||
|
||||
## Rule File Structure
|
||||
|
||||
Each rule file should follow this structure:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Rule Title Here
|
||||
impact: MEDIUM
|
||||
impactDescription: Optional description
|
||||
tags: tag1, tag2, tag3
|
||||
---
|
||||
|
||||
## Rule Title Here
|
||||
|
||||
Brief explanation of the rule and why it matters.
|
||||
|
||||
**Incorrect (description of what's wrong):**
|
||||
|
||||
```typescript
|
||||
// Bad code example
|
||||
```
|
||||
|
||||
**Correct (description of what's right):**
|
||||
|
||||
```typescript
|
||||
// Good code example
|
||||
```
|
||||
|
||||
Optional explanatory text after examples.
|
||||
|
||||
Reference: [Link](https://example.com)
|
||||
|
||||
## File Naming Convention
|
||||
|
||||
- Files starting with `_` are special (excluded from build)
|
||||
- Rule files: `area-description.md` (e.g., `async-parallel.md`)
|
||||
- Section is automatically inferred from filename prefix
|
||||
- Rules are sorted alphabetically by title within each section
|
||||
- IDs (e.g., 1.1, 1.2) are auto-generated during build
|
||||
|
||||
## Impact Levels
|
||||
|
||||
- `CRITICAL` - Highest priority, major performance gains
|
||||
- `HIGH` - Significant performance improvements
|
||||
- `MEDIUM-HIGH` - Moderate-high gains
|
||||
- `MEDIUM` - Moderate performance improvements
|
||||
- `LOW-MEDIUM` - Low-medium gains
|
||||
- `LOW` - Incremental improvements
|
||||
|
||||
## Scripts
|
||||
|
||||
- `pnpm build` - Compile rules into AGENTS.md
|
||||
- `pnpm validate` - Validate all rule files
|
||||
- `pnpm extract-tests` - Extract test cases for LLM evaluation
|
||||
- `pnpm dev` - Build and validate
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding or modifying rules:
|
||||
|
||||
1. Use the correct filename prefix for your section
|
||||
2. Follow the `_template.md` structure
|
||||
3. Include clear bad/good examples with explanations
|
||||
4. Add appropriate tags
|
||||
5. Run `pnpm build` to regenerate AGENTS.md and test-cases.json
|
||||
6. Rules are automatically sorted by title - no need to manage numbers!
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Originally created by [@shuding](https://x.com/shuding) at [Vercel](https://vercel.com).
|
||||
136
src/resources/skills/react-best-practices/SKILL.md
Normal file
136
src/resources/skills/react-best-practices/SKILL.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
name: vercel-react-best-practices
|
||||
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: vercel
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Vercel React Best Practices
|
||||
|
||||
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 57 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
- Writing new React components or Next.js pages
|
||||
- Implementing data fetching (client or server-side)
|
||||
- Reviewing code for performance issues
|
||||
- Refactoring existing React/Next.js code
|
||||
- Optimizing bundle size or load times
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
|----------|----------|--------|--------|
|
||||
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
|
||||
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
|
||||
| 3 | Server-Side Performance | HIGH | `server-` |
|
||||
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
|
||||
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
|
||||
| 6 | Rendering Performance | MEDIUM | `rendering-` |
|
||||
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
|
||||
| 8 | Advanced Patterns | LOW | `advanced-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. Eliminating Waterfalls (CRITICAL)
|
||||
|
||||
- `async-defer-await` - Move await into branches where actually used
|
||||
- `async-parallel` - Use Promise.all() for independent operations
|
||||
- `async-dependencies` - Use better-all for partial dependencies
|
||||
- `async-api-routes` - Start promises early, await late in API routes
|
||||
- `async-suspense-boundaries` - Use Suspense to stream content
|
||||
|
||||
### 2. Bundle Size Optimization (CRITICAL)
|
||||
|
||||
- `bundle-barrel-imports` - Import directly, avoid barrel files
|
||||
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
|
||||
- `bundle-defer-third-party` - Load analytics/logging after hydration
|
||||
- `bundle-conditional` - Load modules only when feature is activated
|
||||
- `bundle-preload` - Preload on hover/focus for perceived speed
|
||||
|
||||
### 3. Server-Side Performance (HIGH)
|
||||
|
||||
- `server-auth-actions` - Authenticate server actions like API routes
|
||||
- `server-cache-react` - Use React.cache() for per-request deduplication
|
||||
- `server-cache-lru` - Use LRU cache for cross-request caching
|
||||
- `server-dedup-props` - Avoid duplicate serialization in RSC props
|
||||
- `server-serialization` - Minimize data passed to client components
|
||||
- `server-parallel-fetching` - Restructure components to parallelize fetches
|
||||
- `server-after-nonblocking` - Use after() for non-blocking operations
|
||||
|
||||
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
|
||||
|
||||
- `client-swr-dedup` - Use SWR for automatic request deduplication
|
||||
- `client-event-listeners` - Deduplicate global event listeners
|
||||
- `client-passive-event-listeners` - Use passive listeners for scroll
|
||||
- `client-localstorage-schema` - Version and minimize localStorage data
|
||||
|
||||
### 5. Re-render Optimization (MEDIUM)
|
||||
|
||||
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
|
||||
- `rerender-memo` - Extract expensive work into memoized components
|
||||
- `rerender-memo-with-default-value` - Hoist default non-primitive props
|
||||
- `rerender-dependencies` - Use primitive dependencies in effects
|
||||
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
|
||||
- `rerender-derived-state-no-effect` - Derive state during render, not effects
|
||||
- `rerender-functional-setstate` - Use functional setState for stable callbacks
|
||||
- `rerender-lazy-state-init` - Pass function to useState for expensive values
|
||||
- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
|
||||
- `rerender-move-effect-to-event` - Put interaction logic in event handlers
|
||||
- `rerender-transitions` - Use startTransition for non-urgent updates
|
||||
- `rerender-use-ref-transient-values` - Use refs for transient frequent values
|
||||
|
||||
### 6. Rendering Performance (MEDIUM)
|
||||
|
||||
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
|
||||
- `rendering-content-visibility` - Use content-visibility for long lists
|
||||
- `rendering-hoist-jsx` - Extract static JSX outside components
|
||||
- `rendering-svg-precision` - Reduce SVG coordinate precision
|
||||
- `rendering-hydration-no-flicker` - Use inline script for client-only data
|
||||
- `rendering-hydration-suppress-warning` - Suppress expected mismatches
|
||||
- `rendering-activity` - Use Activity component for show/hide
|
||||
- `rendering-conditional-render` - Use ternary, not && for conditionals
|
||||
- `rendering-usetransition-loading` - Prefer useTransition for loading state
|
||||
|
||||
### 7. JavaScript Performance (LOW-MEDIUM)
|
||||
|
||||
- `js-batch-dom-css` - Group CSS changes via classes or cssText
|
||||
- `js-index-maps` - Build Map for repeated lookups
|
||||
- `js-cache-property-access` - Cache object properties in loops
|
||||
- `js-cache-function-results` - Cache function results in module-level Map
|
||||
- `js-cache-storage` - Cache localStorage/sessionStorage reads
|
||||
- `js-combine-iterations` - Combine multiple filter/map into one loop
|
||||
- `js-length-check-first` - Check array length before expensive comparison
|
||||
- `js-early-exit` - Return early from functions
|
||||
- `js-hoist-regexp` - Hoist RegExp creation outside loops
|
||||
- `js-min-max-loop` - Use loop for min/max instead of sort
|
||||
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
|
||||
- `js-tosorted-immutable` - Use toSorted() for immutability
|
||||
|
||||
### 8. Advanced Patterns (LOW)
|
||||
|
||||
- `advanced-event-handler-refs` - Store event handlers in refs
|
||||
- `advanced-init-once` - Initialize app once per app load
|
||||
- `advanced-use-latest` - useLatest for stable callback refs
|
||||
|
||||
## How to Use
|
||||
|
||||
Read individual rule files for detailed explanations and code examples:
|
||||
|
||||
```
|
||||
rules/async-parallel.md
|
||||
rules/bundle-barrel-imports.md
|
||||
```
|
||||
|
||||
Each rule file contains:
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect code example with explanation
|
||||
- Correct code example with explanation
|
||||
- Additional context and references
|
||||
|
||||
## Full Compiled Document
|
||||
|
||||
For the complete guide with all rules expanded: `AGENTS.md`
|
||||
15
src/resources/skills/react-best-practices/metadata.json
Normal file
15
src/resources/skills/react-best-practices/metadata.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"organization": "Vercel Engineering",
|
||||
"date": "January 2026",
|
||||
"abstract": "Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.",
|
||||
"references": [
|
||||
"https://react.dev",
|
||||
"https://nextjs.org",
|
||||
"https://swr.vercel.app",
|
||||
"https://github.com/shuding/better-all",
|
||||
"https://github.com/isaacs/node-lru-cache",
|
||||
"https://vercel.com/blog/how-we-optimized-package-imports-in-next-js",
|
||||
"https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast"
|
||||
]
|
||||
}
|
||||
46
src/resources/skills/react-best-practices/rules/_sections.md
Normal file
46
src/resources/skills/react-best-practices/rules/_sections.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Sections
|
||||
|
||||
This file defines all sections, their ordering, impact levels, and descriptions.
|
||||
The section ID (in parentheses) is the filename prefix used to group rules.
|
||||
|
||||
---
|
||||
|
||||
## 1. Eliminating Waterfalls (async)
|
||||
|
||||
**Impact:** CRITICAL
|
||||
**Description:** Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
|
||||
|
||||
## 2. Bundle Size Optimization (bundle)
|
||||
|
||||
**Impact:** CRITICAL
|
||||
**Description:** Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
|
||||
|
||||
## 3. Server-Side Performance (server)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
|
||||
|
||||
## 4. Client-Side Data Fetching (client)
|
||||
|
||||
**Impact:** MEDIUM-HIGH
|
||||
**Description:** Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
|
||||
|
||||
## 5. Re-render Optimization (rerender)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
|
||||
|
||||
## 6. Rendering Performance (rendering)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Optimizing the rendering process reduces the work the browser needs to do.
|
||||
|
||||
## 7. JavaScript Performance (js)
|
||||
|
||||
**Impact:** LOW-MEDIUM
|
||||
**Description:** Micro-optimizations for hot paths can add up to meaningful improvements.
|
||||
|
||||
## 8. Advanced Patterns (advanced)
|
||||
|
||||
**Impact:** LOW
|
||||
**Description:** Advanced patterns for specific cases that require careful implementation.
|
||||
28
src/resources/skills/react-best-practices/rules/_template.md
Normal file
28
src/resources/skills/react-best-practices/rules/_template.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: Rule Title Here
|
||||
impact: MEDIUM
|
||||
impactDescription: Optional description of impact (e.g., "20-50% improvement")
|
||||
tags: tag1, tag2
|
||||
---
|
||||
|
||||
## Rule Title Here
|
||||
|
||||
**Impact: MEDIUM (optional impact description)**
|
||||
|
||||
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
|
||||
|
||||
**Incorrect (description of what's wrong):**
|
||||
|
||||
```typescript
|
||||
// Bad code example here
|
||||
const bad = example()
|
||||
```
|
||||
|
||||
**Correct (description of what's right):**
|
||||
|
||||
```typescript
|
||||
// Good code example here
|
||||
const good = example()
|
||||
```
|
||||
|
||||
Reference: [Link to documentation or resource](https://example.com)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
title: Store Event Handlers in Refs
|
||||
impact: LOW
|
||||
impactDescription: stable subscriptions
|
||||
tags: advanced, hooks, refs, event-handlers, optimization
|
||||
---
|
||||
|
||||
## Store Event Handlers in Refs
|
||||
|
||||
Store callbacks in refs when used in effects that shouldn't re-subscribe on callback changes.
|
||||
|
||||
**Incorrect (re-subscribes on every render):**
|
||||
|
||||
```tsx
|
||||
function useWindowEvent(event: string, handler: (e) => void) {
|
||||
useEffect(() => {
|
||||
window.addEventListener(event, handler)
|
||||
return () => window.removeEventListener(event, handler)
|
||||
}, [event, handler])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (stable subscription):**
|
||||
|
||||
```tsx
|
||||
function useWindowEvent(event: string, handler: (e) => void) {
|
||||
const handlerRef = useRef(handler)
|
||||
useEffect(() => {
|
||||
handlerRef.current = handler
|
||||
}, [handler])
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e) => handlerRef.current(e)
|
||||
window.addEventListener(event, listener)
|
||||
return () => window.removeEventListener(event, listener)
|
||||
}, [event])
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative: use `useEffectEvent` if you're on latest React:**
|
||||
|
||||
```tsx
|
||||
import { useEffectEvent } from 'react'
|
||||
|
||||
function useWindowEvent(event: string, handler: (e) => void) {
|
||||
const onEvent = useEffectEvent(handler)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(event, onEvent)
|
||||
return () => window.removeEventListener(event, onEvent)
|
||||
}, [event])
|
||||
}
|
||||
```
|
||||
|
||||
`useEffectEvent` provides a cleaner API for the same pattern: it creates a stable function reference that always calls the latest version of the handler.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
title: Initialize App Once, Not Per Mount
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids duplicate init in development
|
||||
tags: initialization, useEffect, app-startup, side-effects
|
||||
---
|
||||
|
||||
## Initialize App Once, Not Per Mount
|
||||
|
||||
Do not put app-wide initialization that must run once per app load inside `useEffect([])` of a component. Components can remount and effects will re-run. Use a module-level guard or top-level init in the entry module instead.
|
||||
|
||||
**Incorrect (runs twice in dev, re-runs on remount):**
|
||||
|
||||
```tsx
|
||||
function Comp() {
|
||||
useEffect(() => {
|
||||
loadFromStorage()
|
||||
checkAuthToken()
|
||||
}, [])
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (once per app load):**
|
||||
|
||||
```tsx
|
||||
let didInit = false
|
||||
|
||||
function Comp() {
|
||||
useEffect(() => {
|
||||
if (didInit) return
|
||||
didInit = true
|
||||
loadFromStorage()
|
||||
checkAuthToken()
|
||||
}, [])
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [Initializing the application](https://react.dev/learn/you-might-not-need-an-effect#initializing-the-application)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
title: useEffectEvent for Stable Callback Refs
|
||||
impact: LOW
|
||||
impactDescription: prevents effect re-runs
|
||||
tags: advanced, hooks, useEffectEvent, refs, optimization
|
||||
---
|
||||
|
||||
## useEffectEvent for Stable Callback Refs
|
||||
|
||||
Access latest values in callbacks without adding them to dependency arrays. Prevents effect re-runs while avoiding stale closures.
|
||||
|
||||
**Incorrect (effect re-runs on every callback change):**
|
||||
|
||||
```tsx
|
||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onSearch(query), 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [query, onSearch])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (using React's useEffectEvent):**
|
||||
|
||||
```tsx
|
||||
import { useEffectEvent } from 'react';
|
||||
|
||||
function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
|
||||
const [query, setQuery] = useState('')
|
||||
const onSearchEvent = useEffectEvent(onSearch)
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => onSearchEvent(query), 300)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [query])
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
title: Prevent Waterfall Chains in API Routes
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: api-routes, server-actions, waterfalls, parallelization
|
||||
---
|
||||
|
||||
## Prevent Waterfall Chains in API Routes
|
||||
|
||||
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
|
||||
|
||||
**Incorrect (config waits for auth, data waits for both):**
|
||||
|
||||
```typescript
|
||||
export async function GET(request: Request) {
|
||||
const session = await auth()
|
||||
const config = await fetchConfig()
|
||||
const data = await fetchData(session.user.id)
|
||||
return Response.json({ data, config })
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (auth and config start immediately):**
|
||||
|
||||
```typescript
|
||||
export async function GET(request: Request) {
|
||||
const sessionPromise = auth()
|
||||
const configPromise = fetchConfig()
|
||||
const session = await sessionPromise
|
||||
const [config, data] = await Promise.all([
|
||||
configPromise,
|
||||
fetchData(session.user.id)
|
||||
])
|
||||
return Response.json({ data, config })
|
||||
}
|
||||
```
|
||||
|
||||
For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
title: Defer Await Until Needed
|
||||
impact: HIGH
|
||||
impactDescription: avoids blocking unused code paths
|
||||
tags: async, await, conditional, optimization
|
||||
---
|
||||
|
||||
## Defer Await Until Needed
|
||||
|
||||
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
|
||||
|
||||
**Incorrect (blocks both branches):**
|
||||
|
||||
```typescript
|
||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||
const userData = await fetchUserData(userId)
|
||||
|
||||
if (skipProcessing) {
|
||||
// Returns immediately but still waited for userData
|
||||
return { skipped: true }
|
||||
}
|
||||
|
||||
// Only this branch uses userData
|
||||
return processUserData(userData)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (only blocks when needed):**
|
||||
|
||||
```typescript
|
||||
async function handleRequest(userId: string, skipProcessing: boolean) {
|
||||
if (skipProcessing) {
|
||||
// Returns immediately without waiting
|
||||
return { skipped: true }
|
||||
}
|
||||
|
||||
// Fetch only when needed
|
||||
const userData = await fetchUserData(userId)
|
||||
return processUserData(userData)
|
||||
}
|
||||
```
|
||||
|
||||
**Another example (early return optimization):**
|
||||
|
||||
```typescript
|
||||
// Incorrect: always fetches permissions
|
||||
async function updateResource(resourceId: string, userId: string) {
|
||||
const permissions = await fetchPermissions(userId)
|
||||
const resource = await getResource(resourceId)
|
||||
|
||||
if (!resource) {
|
||||
return { error: 'Not found' }
|
||||
}
|
||||
|
||||
if (!permissions.canEdit) {
|
||||
return { error: 'Forbidden' }
|
||||
}
|
||||
|
||||
return await updateResourceData(resource, permissions)
|
||||
}
|
||||
|
||||
// Correct: fetches only when needed
|
||||
async function updateResource(resourceId: string, userId: string) {
|
||||
const resource = await getResource(resourceId)
|
||||
|
||||
if (!resource) {
|
||||
return { error: 'Not found' }
|
||||
}
|
||||
|
||||
const permissions = await fetchPermissions(userId)
|
||||
|
||||
if (!permissions.canEdit) {
|
||||
return { error: 'Forbidden' }
|
||||
}
|
||||
|
||||
return await updateResourceData(resource, permissions)
|
||||
}
|
||||
```
|
||||
|
||||
This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
title: Dependency-Based Parallelization
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: async, parallelization, dependencies, better-all
|
||||
---
|
||||
|
||||
## Dependency-Based Parallelization
|
||||
|
||||
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
|
||||
|
||||
**Incorrect (profile waits for config unnecessarily):**
|
||||
|
||||
```typescript
|
||||
const [user, config] = await Promise.all([
|
||||
fetchUser(),
|
||||
fetchConfig()
|
||||
])
|
||||
const profile = await fetchProfile(user.id)
|
||||
```
|
||||
|
||||
**Correct (config and profile run in parallel):**
|
||||
|
||||
```typescript
|
||||
import { all } from 'better-all'
|
||||
|
||||
const { user, config, profile } = await all({
|
||||
async user() { return fetchUser() },
|
||||
async config() { return fetchConfig() },
|
||||
async profile() {
|
||||
return fetchProfile((await this.$.user).id)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**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,28 @@
|
|||
---
|
||||
title: Promise.all() for Independent Operations
|
||||
impact: CRITICAL
|
||||
impactDescription: 2-10× improvement
|
||||
tags: async, parallelization, promises, waterfalls
|
||||
---
|
||||
|
||||
## Promise.all() for Independent Operations
|
||||
|
||||
When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
|
||||
|
||||
**Incorrect (sequential execution, 3 round trips):**
|
||||
|
||||
```typescript
|
||||
const user = await fetchUser()
|
||||
const posts = await fetchPosts()
|
||||
const comments = await fetchComments()
|
||||
```
|
||||
|
||||
**Correct (parallel execution, 1 round trip):**
|
||||
|
||||
```typescript
|
||||
const [user, posts, comments] = await Promise.all([
|
||||
fetchUser(),
|
||||
fetchPosts(),
|
||||
fetchComments()
|
||||
])
|
||||
```
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
title: Strategic Suspense Boundaries
|
||||
impact: HIGH
|
||||
impactDescription: faster initial paint
|
||||
tags: async, suspense, streaming, layout-shift
|
||||
---
|
||||
|
||||
## Strategic Suspense Boundaries
|
||||
|
||||
Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
|
||||
|
||||
**Incorrect (wrapper blocked by data fetching):**
|
||||
|
||||
```tsx
|
||||
async function Page() {
|
||||
const data = await fetchData() // Blocks entire page
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<div>
|
||||
<DataDisplay data={data} />
|
||||
</div>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The entire layout waits for data even though only the middle section needs it.
|
||||
|
||||
**Correct (wrapper shows immediately, data streams in):**
|
||||
|
||||
```tsx
|
||||
function Page() {
|
||||
return (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<div>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<DataDisplay />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function DataDisplay() {
|
||||
const data = await fetchData() // Only blocks this component
|
||||
return <div>{data.content}</div>
|
||||
}
|
||||
```
|
||||
|
||||
Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
|
||||
|
||||
**Alternative (share promise across components):**
|
||||
|
||||
```tsx
|
||||
function Page() {
|
||||
// Start fetch immediately, but don't await
|
||||
const dataPromise = fetchData()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Sidebar</div>
|
||||
<div>Header</div>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<DataDisplay dataPromise={dataPromise} />
|
||||
<DataSummary dataPromise={dataPromise} />
|
||||
</Suspense>
|
||||
<div>Footer</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||
const data = use(dataPromise) // Unwraps the promise
|
||||
return <div>{data.content}</div>
|
||||
}
|
||||
|
||||
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
|
||||
const data = use(dataPromise) // Reuses the same promise
|
||||
return <div>{data.summary}</div>
|
||||
}
|
||||
```
|
||||
|
||||
Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together.
|
||||
|
||||
**When NOT to use this pattern:**
|
||||
|
||||
- Critical data needed for layout decisions (affects positioning)
|
||||
- SEO-critical content above the fold
|
||||
- Small, fast queries where suspense overhead isn't worth it
|
||||
- When you want to avoid layout shift (loading → content jump)
|
||||
|
||||
**Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities.
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
title: Avoid Barrel File Imports
|
||||
impact: CRITICAL
|
||||
impactDescription: 200-800ms import cost, slow builds
|
||||
tags: bundle, imports, tree-shaking, barrel-files, performance
|
||||
---
|
||||
|
||||
## Avoid Barrel File Imports
|
||||
|
||||
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
|
||||
|
||||
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
|
||||
|
||||
**Why tree-shaking doesn't help:** When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
|
||||
|
||||
**Incorrect (imports entire library):**
|
||||
|
||||
```tsx
|
||||
import { Check, X, Menu } from 'lucide-react'
|
||||
// Loads 1,583 modules, takes ~2.8s extra in dev
|
||||
// Runtime cost: 200-800ms on every cold start
|
||||
|
||||
import { Button, TextField } from '@mui/material'
|
||||
// Loads 2,225 modules, takes ~4.2s extra in dev
|
||||
```
|
||||
|
||||
**Correct (imports only what you need):**
|
||||
|
||||
```tsx
|
||||
import Check from 'lucide-react/dist/esm/icons/check'
|
||||
import X from 'lucide-react/dist/esm/icons/x'
|
||||
import Menu from 'lucide-react/dist/esm/icons/menu'
|
||||
// Loads only 3 modules (~2KB vs ~1MB)
|
||||
|
||||
import Button from '@mui/material/Button'
|
||||
import TextField from '@mui/material/TextField'
|
||||
// Loads only what you use
|
||||
```
|
||||
|
||||
**Alternative (Next.js 13.5+):**
|
||||
|
||||
```js
|
||||
// next.config.js - use optimizePackageImports
|
||||
module.exports = {
|
||||
experimental: {
|
||||
optimizePackageImports: ['lucide-react', '@mui/material']
|
||||
}
|
||||
}
|
||||
|
||||
// Then you can keep the ergonomic barrel imports:
|
||||
import { Check, X, Menu } from 'lucide-react'
|
||||
// Automatically transformed to direct imports at build time
|
||||
```
|
||||
|
||||
Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
|
||||
|
||||
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
|
||||
|
||||
Reference: [How we optimized package imports in Next.js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: Conditional Module Loading
|
||||
impact: HIGH
|
||||
impactDescription: loads large data only when needed
|
||||
tags: bundle, conditional-loading, lazy-loading
|
||||
---
|
||||
|
||||
## Conditional Module Loading
|
||||
|
||||
Load large data or modules only when a feature is activated.
|
||||
|
||||
**Example (lazy-load animation frames):**
|
||||
|
||||
```tsx
|
||||
function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch<React.SetStateAction<boolean>> }) {
|
||||
const [frames, setFrames] = useState<Frame[] | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && !frames && typeof window !== 'undefined') {
|
||||
import('./animation-frames.js')
|
||||
.then(mod => setFrames(mod.frames))
|
||||
.catch(() => setEnabled(false))
|
||||
}
|
||||
}, [enabled, frames, setEnabled])
|
||||
|
||||
if (!frames) return <Skeleton />
|
||||
return <Canvas frames={frames} />
|
||||
}
|
||||
```
|
||||
|
||||
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
title: Defer Non-Critical Third-Party Libraries
|
||||
impact: MEDIUM
|
||||
impactDescription: loads after hydration
|
||||
tags: bundle, third-party, analytics, defer
|
||||
---
|
||||
|
||||
## Defer Non-Critical Third-Party Libraries
|
||||
|
||||
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
|
||||
|
||||
**Incorrect (blocks initial bundle):**
|
||||
|
||||
```tsx
|
||||
import { Analytics } from '@vercel/analytics/react'
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (loads after hydration):**
|
||||
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Analytics = dynamic(
|
||||
() => import('@vercel/analytics/react').then(m => m.Analytics),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
<Analytics />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
title: Dynamic Imports for Heavy Components
|
||||
impact: CRITICAL
|
||||
impactDescription: directly affects TTI and LCP
|
||||
tags: bundle, dynamic-import, code-splitting, next-dynamic
|
||||
---
|
||||
|
||||
## Dynamic Imports for Heavy Components
|
||||
|
||||
Use `next/dynamic` to lazy-load large components not needed on initial render.
|
||||
|
||||
**Incorrect (Monaco bundles with main chunk ~300KB):**
|
||||
|
||||
```tsx
|
||||
import { MonacoEditor } from './monaco-editor'
|
||||
|
||||
function CodePanel({ code }: { code: string }) {
|
||||
return <MonacoEditor value={code} />
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (Monaco loads on demand):**
|
||||
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const MonacoEditor = dynamic(
|
||||
() => import('./monaco-editor').then(m => m.MonacoEditor),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
function CodePanel({ code }: { code: string }) {
|
||||
return <MonacoEditor value={code} />
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
title: Preload Based on User Intent
|
||||
impact: MEDIUM
|
||||
impactDescription: reduces perceived latency
|
||||
tags: bundle, preload, user-intent, hover
|
||||
---
|
||||
|
||||
## Preload Based on User Intent
|
||||
|
||||
Preload heavy bundles before they're needed to reduce perceived latency.
|
||||
|
||||
**Example (preload on hover/focus):**
|
||||
|
||||
```tsx
|
||||
function EditorButton({ onClick }: { onClick: () => void }) {
|
||||
const preload = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
void import('./monaco-editor')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onMouseEnter={preload}
|
||||
onFocus={preload}
|
||||
onClick={onClick}
|
||||
>
|
||||
Open Editor
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Example (preload when feature flag is enabled):**
|
||||
|
||||
```tsx
|
||||
function FlagsProvider({ children, flags }: Props) {
|
||||
useEffect(() => {
|
||||
if (flags.editorEnabled && typeof window !== 'undefined') {
|
||||
void import('./monaco-editor').then(mod => mod.init())
|
||||
}
|
||||
}, [flags.editorEnabled])
|
||||
|
||||
return <FlagsContext.Provider value={flags}>
|
||||
{children}
|
||||
</FlagsContext.Provider>
|
||||
}
|
||||
```
|
||||
|
||||
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
title: Deduplicate Global Event Listeners
|
||||
impact: LOW
|
||||
impactDescription: single listener for N components
|
||||
tags: client, swr, event-listeners, subscription
|
||||
---
|
||||
|
||||
## Deduplicate Global Event Listeners
|
||||
|
||||
Use `useSWRSubscription()` to share global event listeners across component instances.
|
||||
|
||||
**Incorrect (N instances = N listeners):**
|
||||
|
||||
```tsx
|
||||
function useKeyboardShortcut(key: string, callback: () => void) {
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && e.key === key) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
}, [key, callback])
|
||||
}
|
||||
```
|
||||
|
||||
When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
|
||||
|
||||
**Correct (N instances = 1 listener):**
|
||||
|
||||
```tsx
|
||||
import useSWRSubscription from 'swr/subscription'
|
||||
|
||||
// Module-level Map to track callbacks per key
|
||||
const keyCallbacks = new Map<string, Set<() => void>>()
|
||||
|
||||
function useKeyboardShortcut(key: string, callback: () => void) {
|
||||
// Register this callback in the Map
|
||||
useEffect(() => {
|
||||
if (!keyCallbacks.has(key)) {
|
||||
keyCallbacks.set(key, new Set())
|
||||
}
|
||||
keyCallbacks.get(key)!.add(callback)
|
||||
|
||||
return () => {
|
||||
const set = keyCallbacks.get(key)
|
||||
if (set) {
|
||||
set.delete(callback)
|
||||
if (set.size === 0) {
|
||||
keyCallbacks.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [key, callback])
|
||||
|
||||
useSWRSubscription('global-keydown', () => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && keyCallbacks.has(e.key)) {
|
||||
keyCallbacks.get(e.key)!.forEach(cb => cb())
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
})
|
||||
}
|
||||
|
||||
function Profile() {
|
||||
// Multiple shortcuts will share the same listener
|
||||
useKeyboardShortcut('p', () => { /* ... */ })
|
||||
useKeyboardShortcut('k', () => { /* ... */ })
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
title: Version and Minimize localStorage Data
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents schema conflicts, reduces storage size
|
||||
tags: client, localStorage, storage, versioning, data-minimization
|
||||
---
|
||||
|
||||
## Version and Minimize localStorage Data
|
||||
|
||||
Add version prefix to keys and store only needed fields. Prevents schema conflicts and accidental storage of sensitive data.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
// No version, stores everything, no error handling
|
||||
localStorage.setItem('userConfig', JSON.stringify(fullUserObject))
|
||||
const data = localStorage.getItem('userConfig')
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
const VERSION = 'v2'
|
||||
|
||||
function saveConfig(config: { theme: string; language: string }) {
|
||||
try {
|
||||
localStorage.setItem(`userConfig:${VERSION}`, JSON.stringify(config))
|
||||
} catch {
|
||||
// Throws in incognito/private browsing, quota exceeded, or disabled
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
try {
|
||||
const data = localStorage.getItem(`userConfig:${VERSION}`)
|
||||
return data ? JSON.parse(data) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Migration from v1 to v2
|
||||
function migrate() {
|
||||
try {
|
||||
const v1 = localStorage.getItem('userConfig:v1')
|
||||
if (v1) {
|
||||
const old = JSON.parse(v1)
|
||||
saveConfig({ theme: old.darkMode ? 'dark' : 'light', language: old.lang })
|
||||
localStorage.removeItem('userConfig:v1')
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
```
|
||||
|
||||
**Store minimal fields from server responses:**
|
||||
|
||||
```typescript
|
||||
// User object has 20+ fields, only store what UI needs
|
||||
function cachePrefs(user: FullUser) {
|
||||
try {
|
||||
localStorage.setItem('prefs:v1', JSON.stringify({
|
||||
theme: user.preferences.theme,
|
||||
notifications: user.preferences.notifications
|
||||
}))
|
||||
} catch {}
|
||||
}
|
||||
```
|
||||
|
||||
**Always wrap in try-catch:** `getItem()` and `setItem()` throw in incognito/private browsing (Safari, Firefox), when quota exceeded, or when disabled.
|
||||
|
||||
**Benefits:** Schema evolution via versioning, reduced storage size, prevents storing tokens/PII/internal flags.
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
title: Use Passive Event Listeners for Scrolling Performance
|
||||
impact: MEDIUM
|
||||
impactDescription: eliminates scroll delay caused by event listeners
|
||||
tags: client, event-listeners, scrolling, performance, touch, wheel
|
||||
---
|
||||
|
||||
## Use Passive Event Listeners for Scrolling Performance
|
||||
|
||||
Add `{ passive: true }` to touch and wheel event listeners to enable immediate scrolling. Browsers normally wait for listeners to finish to check if `preventDefault()` is called, causing scroll delay.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
|
||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
|
||||
|
||||
document.addEventListener('touchstart', handleTouch)
|
||||
document.addEventListener('wheel', handleWheel)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('touchstart', handleTouch)
|
||||
document.removeEventListener('wheel', handleWheel)
|
||||
}
|
||||
}, [])
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const handleTouch = (e: TouchEvent) => console.log(e.touches[0].clientX)
|
||||
const handleWheel = (e: WheelEvent) => console.log(e.deltaY)
|
||||
|
||||
document.addEventListener('touchstart', handleTouch, { passive: true })
|
||||
document.addEventListener('wheel', handleWheel, { passive: true })
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('touchstart', handleTouch)
|
||||
document.removeEventListener('wheel', handleWheel)
|
||||
}
|
||||
}, [])
|
||||
```
|
||||
|
||||
**Use passive when:** tracking/analytics, logging, any listener that doesn't call `preventDefault()`.
|
||||
|
||||
**Don't use passive when:** implementing custom swipe gestures, custom zoom controls, or any listener that needs `preventDefault()`.
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
title: Use SWR for Automatic Deduplication
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: automatic deduplication
|
||||
tags: client, swr, deduplication, data-fetching
|
||||
---
|
||||
|
||||
## Use SWR for Automatic Deduplication
|
||||
|
||||
SWR enables request deduplication, caching, and revalidation across component instances.
|
||||
|
||||
**Incorrect (no deduplication, each instance fetches):**
|
||||
|
||||
```tsx
|
||||
function UserList() {
|
||||
const [users, setUsers] = useState([])
|
||||
useEffect(() => {
|
||||
fetch('/api/users')
|
||||
.then(r => r.json())
|
||||
.then(setUsers)
|
||||
}, [])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (multiple instances share one request):**
|
||||
|
||||
```tsx
|
||||
import useSWR from 'swr'
|
||||
|
||||
function UserList() {
|
||||
const { data: users } = useSWR('/api/users', fetcher)
|
||||
}
|
||||
```
|
||||
|
||||
**For immutable data:**
|
||||
|
||||
```tsx
|
||||
import { useImmutableSWR } from '@/lib/swr'
|
||||
|
||||
function StaticContent() {
|
||||
const { data } = useImmutableSWR('/api/config', fetcher)
|
||||
}
|
||||
```
|
||||
|
||||
**For mutations:**
|
||||
|
||||
```tsx
|
||||
import { useSWRMutation } from 'swr/mutation'
|
||||
|
||||
function UpdateButton() {
|
||||
const { trigger } = useSWRMutation('/api/user', updateUser)
|
||||
return <button onClick={() => trigger()}>Update</button>
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [https://swr.vercel.app](https://swr.vercel.app)
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
---
|
||||
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.
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
title: Cache Repeated Function Calls
|
||||
impact: MEDIUM
|
||||
impactDescription: avoid redundant computation
|
||||
tags: javascript, cache, memoization, performance
|
||||
---
|
||||
|
||||
## Cache Repeated Function Calls
|
||||
|
||||
Use a module-level Map to cache function results when the same function is called repeatedly with the same inputs during render.
|
||||
|
||||
**Incorrect (redundant computation):**
|
||||
|
||||
```typescript
|
||||
function ProjectList({ projects }: { projects: Project[] }) {
|
||||
return (
|
||||
<div>
|
||||
{projects.map(project => {
|
||||
// slugify() called 100+ times for same project names
|
||||
const slug = slugify(project.name)
|
||||
|
||||
return <ProjectCard key={project.id} slug={slug} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (cached results):**
|
||||
|
||||
```typescript
|
||||
// Module-level cache
|
||||
const slugifyCache = new Map<string, string>()
|
||||
|
||||
function cachedSlugify(text: string): string {
|
||||
if (slugifyCache.has(text)) {
|
||||
return slugifyCache.get(text)!
|
||||
}
|
||||
const result = slugify(text)
|
||||
slugifyCache.set(text, result)
|
||||
return result
|
||||
}
|
||||
|
||||
function ProjectList({ projects }: { projects: Project[] }) {
|
||||
return (
|
||||
<div>
|
||||
{projects.map(project => {
|
||||
// Computed only once per unique project name
|
||||
const slug = cachedSlugify(project.name)
|
||||
|
||||
return <ProjectCard key={project.id} slug={slug} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Simpler pattern for single-value functions:**
|
||||
|
||||
```typescript
|
||||
let isLoggedInCache: boolean | null = null
|
||||
|
||||
function isLoggedIn(): boolean {
|
||||
if (isLoggedInCache !== null) {
|
||||
return isLoggedInCache
|
||||
}
|
||||
|
||||
isLoggedInCache = document.cookie.includes('auth=')
|
||||
return isLoggedInCache
|
||||
}
|
||||
|
||||
// Clear cache when auth changes
|
||||
function onAuthChange() {
|
||||
isLoggedInCache = null
|
||||
}
|
||||
```
|
||||
|
||||
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
|
||||
|
||||
Reference: [How we made the Vercel Dashboard twice as fast](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: Cache Property Access in Loops
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces lookups
|
||||
tags: javascript, loops, optimization, caching
|
||||
---
|
||||
|
||||
## Cache Property Access in Loops
|
||||
|
||||
Cache object property lookups in hot paths.
|
||||
|
||||
**Incorrect (3 lookups × N iterations):**
|
||||
|
||||
```typescript
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
process(obj.config.settings.value)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (1 lookup total):**
|
||||
|
||||
```typescript
|
||||
const value = obj.config.settings.value
|
||||
const len = arr.length
|
||||
for (let i = 0; i < len; i++) {
|
||||
process(value)
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
title: Cache Storage API Calls
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces expensive I/O
|
||||
tags: javascript, localStorage, storage, caching, performance
|
||||
---
|
||||
|
||||
## Cache Storage API Calls
|
||||
|
||||
`localStorage`, `sessionStorage`, and `document.cookie` are synchronous and expensive. Cache reads in memory.
|
||||
|
||||
**Incorrect (reads storage on every call):**
|
||||
|
||||
```typescript
|
||||
function getTheme() {
|
||||
return localStorage.getItem('theme') ?? 'light'
|
||||
}
|
||||
// Called 10 times = 10 storage reads
|
||||
```
|
||||
|
||||
**Correct (Map cache):**
|
||||
|
||||
```typescript
|
||||
const storageCache = new Map<string, string | null>()
|
||||
|
||||
function getLocalStorage(key: string) {
|
||||
if (!storageCache.has(key)) {
|
||||
storageCache.set(key, localStorage.getItem(key))
|
||||
}
|
||||
return storageCache.get(key)
|
||||
}
|
||||
|
||||
function setLocalStorage(key: string, value: string) {
|
||||
localStorage.setItem(key, value)
|
||||
storageCache.set(key, value) // keep cache in sync
|
||||
}
|
||||
```
|
||||
|
||||
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just React components.
|
||||
|
||||
**Cookie caching:**
|
||||
|
||||
```typescript
|
||||
let cookieCache: Record<string, string> | null = null
|
||||
|
||||
function getCookie(name: string) {
|
||||
if (!cookieCache) {
|
||||
cookieCache = Object.fromEntries(
|
||||
document.cookie.split('; ').map(c => c.split('='))
|
||||
)
|
||||
}
|
||||
return cookieCache[name]
|
||||
}
|
||||
```
|
||||
|
||||
**Important (invalidate on external changes):**
|
||||
|
||||
If storage can change externally (another tab, server-set cookies), invalidate cache:
|
||||
|
||||
```typescript
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (e.key) storageCache.delete(e.key)
|
||||
})
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
storageCache.clear()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
title: Combine Multiple Array Iterations
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: reduces iterations
|
||||
tags: javascript, arrays, loops, performance
|
||||
---
|
||||
|
||||
## Combine Multiple Array Iterations
|
||||
|
||||
Multiple `.filter()` or `.map()` calls iterate the array multiple times. Combine into one loop.
|
||||
|
||||
**Incorrect (3 iterations):**
|
||||
|
||||
```typescript
|
||||
const admins = users.filter(u => u.isAdmin)
|
||||
const testers = users.filter(u => u.isTester)
|
||||
const inactive = users.filter(u => !u.isActive)
|
||||
```
|
||||
|
||||
**Correct (1 iteration):**
|
||||
|
||||
```typescript
|
||||
const admins: User[] = []
|
||||
const testers: User[] = []
|
||||
const inactive: User[] = []
|
||||
|
||||
for (const user of users) {
|
||||
if (user.isAdmin) admins.push(user)
|
||||
if (user.isTester) testers.push(user)
|
||||
if (!user.isActive) inactive.push(user)
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
title: Early Return from Functions
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids unnecessary computation
|
||||
tags: javascript, functions, optimization, early-return
|
||||
---
|
||||
|
||||
## Early Return from Functions
|
||||
|
||||
Return early when result is determined to skip unnecessary processing.
|
||||
|
||||
**Incorrect (processes all items even after finding answer):**
|
||||
|
||||
```typescript
|
||||
function validateUsers(users: User[]) {
|
||||
let hasError = false
|
||||
let errorMessage = ''
|
||||
|
||||
for (const user of users) {
|
||||
if (!user.email) {
|
||||
hasError = true
|
||||
errorMessage = 'Email required'
|
||||
}
|
||||
if (!user.name) {
|
||||
hasError = true
|
||||
errorMessage = 'Name required'
|
||||
}
|
||||
// Continues checking all users even after error found
|
||||
}
|
||||
|
||||
return hasError ? { valid: false, error: errorMessage } : { valid: true }
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (returns immediately on first error):**
|
||||
|
||||
```typescript
|
||||
function validateUsers(users: User[]) {
|
||||
for (const user of users) {
|
||||
if (!user.email) {
|
||||
return { valid: false, error: 'Email required' }
|
||||
}
|
||||
if (!user.name) {
|
||||
return { valid: false, error: 'Name required' }
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: Hoist RegExp Creation
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids recreation
|
||||
tags: javascript, regexp, optimization, memoization
|
||||
---
|
||||
|
||||
## Hoist RegExp Creation
|
||||
|
||||
Don't create RegExp inside render. Hoist to module scope or memoize with `useMemo()`.
|
||||
|
||||
**Incorrect (new RegExp every render):**
|
||||
|
||||
```tsx
|
||||
function Highlighter({ text, query }: Props) {
|
||||
const regex = new RegExp(`(${query})`, 'gi')
|
||||
const parts = text.split(regex)
|
||||
return <>{parts.map((part, i) => ...)}</>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (memoize or hoist):**
|
||||
|
||||
```tsx
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
function Highlighter({ text, query }: Props) {
|
||||
const regex = useMemo(
|
||||
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
|
||||
[query]
|
||||
)
|
||||
const parts = text.split(regex)
|
||||
return <>{parts.map((part, i) => ...)}</>
|
||||
}
|
||||
```
|
||||
|
||||
**Warning (global regex has mutable state):**
|
||||
|
||||
Global regex (`/g`) has mutable `lastIndex` state:
|
||||
|
||||
```typescript
|
||||
const regex = /foo/g
|
||||
regex.test('foo') // true, lastIndex = 3
|
||||
regex.test('foo') // false, lastIndex = 0
|
||||
```
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
title: Build Index Maps for Repeated Lookups
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: 1M ops to 2K ops
|
||||
tags: javascript, map, indexing, optimization, performance
|
||||
---
|
||||
|
||||
## Build Index Maps for Repeated Lookups
|
||||
|
||||
Multiple `.find()` calls by the same key should use a Map.
|
||||
|
||||
**Incorrect (O(n) per lookup):**
|
||||
|
||||
```typescript
|
||||
function processOrders(orders: Order[], users: User[]) {
|
||||
return orders.map(order => ({
|
||||
...order,
|
||||
user: users.find(u => u.id === order.userId)
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (O(1) per lookup):**
|
||||
|
||||
```typescript
|
||||
function processOrders(orders: Order[], users: User[]) {
|
||||
const userById = new Map(users.map(u => [u.id, u]))
|
||||
|
||||
return orders.map(order => ({
|
||||
...order,
|
||||
user: userById.get(order.userId)
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
Build map once (O(n)), then all lookups are O(1).
|
||||
For 1000 orders × 1000 users: 1M ops → 2K ops.
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
title: Early Length Check for Array Comparisons
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: avoids expensive operations when lengths differ
|
||||
tags: javascript, arrays, performance, optimization, comparison
|
||||
---
|
||||
|
||||
## Early Length Check for Array Comparisons
|
||||
|
||||
When comparing arrays with expensive operations (sorting, deep equality, serialization), check lengths first. If lengths differ, the arrays cannot be equal.
|
||||
|
||||
In real-world applications, this optimization is especially valuable when the comparison runs in hot paths (event handlers, render loops).
|
||||
|
||||
**Incorrect (always runs expensive comparison):**
|
||||
|
||||
```typescript
|
||||
function hasChanges(current: string[], original: string[]) {
|
||||
// Always sorts and joins, even when lengths differ
|
||||
return current.sort().join() !== original.sort().join()
|
||||
}
|
||||
```
|
||||
|
||||
Two O(n log n) sorts run even when `current.length` is 5 and `original.length` is 100. There is also overhead of joining the arrays and comparing the strings.
|
||||
|
||||
**Correct (O(1) length check first):**
|
||||
|
||||
```typescript
|
||||
function hasChanges(current: string[], original: string[]) {
|
||||
// Early return if lengths differ
|
||||
if (current.length !== original.length) {
|
||||
return true
|
||||
}
|
||||
// Only sort when lengths match
|
||||
const currentSorted = current.toSorted()
|
||||
const originalSorted = original.toSorted()
|
||||
for (let i = 0; i < currentSorted.length; i++) {
|
||||
if (currentSorted[i] !== originalSorted[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
This new approach is more efficient because:
|
||||
- It avoids the overhead of sorting and joining the arrays when lengths differ
|
||||
- It avoids consuming memory for the joined strings (especially important for large arrays)
|
||||
- It avoids mutating the original arrays
|
||||
- It returns early when a difference is found
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
title: Use Loop for Min/Max Instead of Sort
|
||||
impact: LOW
|
||||
impactDescription: O(n) instead of O(n log n)
|
||||
tags: javascript, arrays, performance, sorting, algorithms
|
||||
---
|
||||
|
||||
## Use Loop for Min/Max Instead of Sort
|
||||
|
||||
Finding the smallest or largest element only requires a single pass through the array. Sorting is wasteful and slower.
|
||||
|
||||
**Incorrect (O(n log n) - sort to find latest):**
|
||||
|
||||
```typescript
|
||||
interface Project {
|
||||
id: string
|
||||
name: string
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
function getLatestProject(projects: Project[]) {
|
||||
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
return sorted[0]
|
||||
}
|
||||
```
|
||||
|
||||
Sorts the entire array just to find the maximum value.
|
||||
|
||||
**Incorrect (O(n log n) - sort for oldest and newest):**
|
||||
|
||||
```typescript
|
||||
function getOldestAndNewest(projects: Project[]) {
|
||||
const sorted = [...projects].sort((a, b) => a.updatedAt - b.updatedAt)
|
||||
return { oldest: sorted[0], newest: sorted[sorted.length - 1] }
|
||||
}
|
||||
```
|
||||
|
||||
Still sorts unnecessarily when only min/max are needed.
|
||||
|
||||
**Correct (O(n) - single loop):**
|
||||
|
||||
```typescript
|
||||
function getLatestProject(projects: Project[]) {
|
||||
if (projects.length === 0) return null
|
||||
|
||||
let latest = projects[0]
|
||||
|
||||
for (let i = 1; i < projects.length; i++) {
|
||||
if (projects[i].updatedAt > latest.updatedAt) {
|
||||
latest = projects[i]
|
||||
}
|
||||
}
|
||||
|
||||
return latest
|
||||
}
|
||||
|
||||
function getOldestAndNewest(projects: Project[]) {
|
||||
if (projects.length === 0) return { oldest: null, newest: null }
|
||||
|
||||
let oldest = projects[0]
|
||||
let newest = projects[0]
|
||||
|
||||
for (let i = 1; i < projects.length; i++) {
|
||||
if (projects[i].updatedAt < oldest.updatedAt) oldest = projects[i]
|
||||
if (projects[i].updatedAt > newest.updatedAt) newest = projects[i]
|
||||
}
|
||||
|
||||
return { oldest, newest }
|
||||
}
|
||||
```
|
||||
|
||||
Single pass through the array, no copying, no sorting.
|
||||
|
||||
**Alternative (Math.min/Math.max for small arrays):**
|
||||
|
||||
```typescript
|
||||
const numbers = [5, 2, 8, 1, 9]
|
||||
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.
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
title: Use Set/Map for O(1) Lookups
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: O(n) to O(1)
|
||||
tags: javascript, set, map, data-structures, performance
|
||||
---
|
||||
|
||||
## Use Set/Map for O(1) Lookups
|
||||
|
||||
Convert arrays to Set/Map for repeated membership checks.
|
||||
|
||||
**Incorrect (O(n) per check):**
|
||||
|
||||
```typescript
|
||||
const allowedIds = ['a', 'b', 'c', ...]
|
||||
items.filter(item => allowedIds.includes(item.id))
|
||||
```
|
||||
|
||||
**Correct (O(1) per check):**
|
||||
|
||||
```typescript
|
||||
const allowedIds = new Set(['a', 'b', 'c', ...])
|
||||
items.filter(item => allowedIds.has(item.id))
|
||||
```
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
title: Use toSorted() Instead of sort() for Immutability
|
||||
impact: MEDIUM-HIGH
|
||||
impactDescription: prevents mutation bugs in React state
|
||||
tags: javascript, arrays, immutability, react, state, mutation
|
||||
---
|
||||
|
||||
## Use toSorted() Instead of sort() for Immutability
|
||||
|
||||
`.sort()` mutates the array in place, which can cause bugs with React state and props. Use `.toSorted()` to create a new sorted array without mutation.
|
||||
|
||||
**Incorrect (mutates original array):**
|
||||
|
||||
```typescript
|
||||
function UserList({ users }: { users: User[] }) {
|
||||
// Mutates the users prop array!
|
||||
const sorted = useMemo(
|
||||
() => users.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[users]
|
||||
)
|
||||
return <div>{sorted.map(renderUser)}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (creates new array):**
|
||||
|
||||
```typescript
|
||||
function UserList({ users }: { users: User[] }) {
|
||||
// Creates new sorted array, original unchanged
|
||||
const sorted = useMemo(
|
||||
() => users.toSorted((a, b) => a.name.localeCompare(b.name)),
|
||||
[users]
|
||||
)
|
||||
return <div>{sorted.map(renderUser)}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters in React:**
|
||||
|
||||
1. Props/state mutations break React's immutability model - React expects props and state to be treated as read-only
|
||||
2. Causes stale closure bugs - Mutating arrays inside closures (callbacks, effects) can lead to unexpected behavior
|
||||
|
||||
**Browser support (fallback for older browsers):**
|
||||
|
||||
`.toSorted()` is available in all modern browsers (Chrome 110+, Safari 16+, Firefox 115+, Node.js 20+). For older environments, use spread operator:
|
||||
|
||||
```typescript
|
||||
// Fallback for older browsers
|
||||
const sorted = [...items].sort((a, b) => a.value - b.value)
|
||||
```
|
||||
|
||||
**Other immutable array methods:**
|
||||
|
||||
- `.toSorted()` - immutable sort
|
||||
- `.toReversed()` - immutable reverse
|
||||
- `.toSpliced()` - immutable splice
|
||||
- `.with()` - immutable element replacement
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
title: Use Activity Component for Show/Hide
|
||||
impact: MEDIUM
|
||||
impactDescription: preserves state/DOM
|
||||
tags: rendering, activity, visibility, state-preservation
|
||||
---
|
||||
|
||||
## Use Activity Component for Show/Hide
|
||||
|
||||
Use React's `<Activity>` to preserve state/DOM for expensive components that frequently toggle visibility.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { Activity } from 'react'
|
||||
|
||||
function Dropdown({ isOpen }: Props) {
|
||||
return (
|
||||
<Activity mode={isOpen ? 'visible' : 'hidden'}>
|
||||
<ExpensiveMenu />
|
||||
</Activity>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Avoids expensive re-renders and state loss.
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
title: Animate SVG Wrapper Instead of SVG Element
|
||||
impact: LOW
|
||||
impactDescription: enables hardware acceleration
|
||||
tags: rendering, svg, css, animation, performance
|
||||
---
|
||||
|
||||
## Animate SVG Wrapper Instead of SVG Element
|
||||
|
||||
Many browsers don't have hardware acceleration for CSS3 animations on SVG elements. Wrap SVG in a `<div>` and animate the wrapper instead.
|
||||
|
||||
**Incorrect (animating SVG directly - no hardware acceleration):**
|
||||
|
||||
```tsx
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<svg
|
||||
className="animate-spin"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (animating wrapper div - hardware accelerated):**
|
||||
|
||||
```tsx
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<div className="animate-spin">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
This applies to all CSS transforms and transitions (`transform`, `opacity`, `translate`, `scale`, `rotate`). The wrapper div allows browsers to use GPU acceleration for smoother animations.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
title: Use Explicit Conditional Rendering
|
||||
impact: LOW
|
||||
impactDescription: prevents rendering 0 or NaN
|
||||
tags: rendering, conditional, jsx, falsy-values
|
||||
---
|
||||
|
||||
## Use Explicit Conditional Rendering
|
||||
|
||||
Use explicit ternary operators (`? :`) instead of `&&` for conditional rendering when the condition can be `0`, `NaN`, or other falsy values that render.
|
||||
|
||||
**Incorrect (renders "0" when count is 0):**
|
||||
|
||||
```tsx
|
||||
function Badge({ count }: { count: number }) {
|
||||
return (
|
||||
<div>
|
||||
{count && <span className="badge">{count}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// When count = 0, renders: <div>0</div>
|
||||
// When count = 5, renders: <div><span class="badge">5</span></div>
|
||||
```
|
||||
|
||||
**Correct (renders nothing when count is 0):**
|
||||
|
||||
```tsx
|
||||
function Badge({ count }: { count: number }) {
|
||||
return (
|
||||
<div>
|
||||
{count > 0 ? <span className="badge">{count}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// When count = 0, renders: <div></div>
|
||||
// When count = 5, renders: <div><span class="badge">5</span></div>
|
||||
```
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
title: CSS content-visibility for Long Lists
|
||||
impact: HIGH
|
||||
impactDescription: faster initial render
|
||||
tags: rendering, css, content-visibility, long-lists
|
||||
---
|
||||
|
||||
## CSS content-visibility for Long Lists
|
||||
|
||||
Apply `content-visibility: auto` to defer off-screen rendering.
|
||||
|
||||
**CSS:**
|
||||
|
||||
```css
|
||||
.message-item {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 0 80px;
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
function MessageList({ messages }: { messages: Message[] }) {
|
||||
return (
|
||||
<div className="overflow-y-auto h-screen">
|
||||
{messages.map(msg => (
|
||||
<div key={msg.id} className="message-item">
|
||||
<Avatar user={msg.author} />
|
||||
<div>{msg.content}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
For 1000 messages, browser skips layout/paint for ~990 off-screen items (10× faster initial render).
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
title: Hoist Static JSX Elements
|
||||
impact: LOW
|
||||
impactDescription: avoids re-creation
|
||||
tags: rendering, jsx, static, optimization
|
||||
---
|
||||
|
||||
## Hoist Static JSX Elements
|
||||
|
||||
Extract static JSX outside components to avoid re-creation.
|
||||
|
||||
**Incorrect (recreates element every render):**
|
||||
|
||||
```tsx
|
||||
function LoadingSkeleton() {
|
||||
return <div className="animate-pulse h-20 bg-gray-200" />
|
||||
}
|
||||
|
||||
function Container() {
|
||||
return (
|
||||
<div>
|
||||
{loading && <LoadingSkeleton />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (reuses same element):**
|
||||
|
||||
```tsx
|
||||
const loadingSkeleton = (
|
||||
<div className="animate-pulse h-20 bg-gray-200" />
|
||||
)
|
||||
|
||||
function Container() {
|
||||
return (
|
||||
<div>
|
||||
{loading && loadingSkeleton}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
This is especially helpful for large and static SVG nodes, which can be expensive to recreate on every render.
|
||||
|
||||
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler automatically hoists static JSX elements and optimizes component re-renders, making manual hoisting unnecessary.
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
title: Prevent Hydration Mismatch Without Flickering
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids visual flicker and hydration errors
|
||||
tags: rendering, ssr, hydration, localStorage, flicker
|
||||
---
|
||||
|
||||
## Prevent Hydration Mismatch Without Flickering
|
||||
|
||||
When rendering content that depends on client-side storage (localStorage, cookies), avoid both SSR breakage and post-hydration flickering by injecting a synchronous script that updates the DOM before React hydrates.
|
||||
|
||||
**Incorrect (breaks SSR):**
|
||||
|
||||
```tsx
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
// localStorage is not available on server - throws error
|
||||
const theme = localStorage.getItem('theme') || 'light'
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Server-side rendering will fail because `localStorage` is undefined.
|
||||
|
||||
**Incorrect (visual flickering):**
|
||||
|
||||
```tsx
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState('light')
|
||||
|
||||
useEffect(() => {
|
||||
// Runs after hydration - causes visible flash
|
||||
const stored = localStorage.getItem('theme')
|
||||
if (stored) {
|
||||
setTheme(stored)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Component first renders with default value (`light`), then updates after hydration, causing a visible flash of incorrect content.
|
||||
|
||||
**Correct (no flicker, no hydration mismatch):**
|
||||
|
||||
```tsx
|
||||
function ThemeWrapper({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<div id="theme-wrapper">
|
||||
{children}
|
||||
</div>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
try {
|
||||
var theme = localStorage.getItem('theme') || 'light';
|
||||
var el = document.getElementById('theme-wrapper');
|
||||
if (el) el.className = theme;
|
||||
} catch (e) {}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The inline script executes synchronously before showing the element, ensuring the DOM already has the correct value. No flickering, no hydration mismatch.
|
||||
|
||||
This pattern is especially useful for theme toggles, user preferences, authentication states, and any client-only data that should render immediately without flashing default values.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
title: Suppress Expected Hydration Mismatches
|
||||
impact: LOW-MEDIUM
|
||||
impactDescription: avoids noisy hydration warnings for known differences
|
||||
tags: rendering, hydration, ssr, nextjs
|
||||
---
|
||||
|
||||
## Suppress Expected Hydration Mismatches
|
||||
|
||||
In SSR frameworks (e.g., Next.js), some values are intentionally different on server vs client (random IDs, dates, locale/timezone formatting). For these *expected* mismatches, wrap the dynamic text in an element with `suppressHydrationWarning` to prevent noisy warnings. Do not use this to hide real bugs. Don’t overuse it.
|
||||
|
||||
**Incorrect (known mismatch warnings):**
|
||||
|
||||
```tsx
|
||||
function Timestamp() {
|
||||
return <span>{new Date().toLocaleString()}</span>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (suppress expected mismatch only):**
|
||||
|
||||
```tsx
|
||||
function Timestamp() {
|
||||
return (
|
||||
<span suppressHydrationWarning>
|
||||
{new Date().toLocaleString()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: Optimize SVG Precision
|
||||
impact: LOW
|
||||
impactDescription: reduces file size
|
||||
tags: rendering, svg, optimization, svgo
|
||||
---
|
||||
|
||||
## Optimize SVG Precision
|
||||
|
||||
Reduce SVG coordinate precision to decrease file size. The optimal precision depends on the viewBox size, but in general reducing precision should be considered.
|
||||
|
||||
**Incorrect (excessive precision):**
|
||||
|
||||
```svg
|
||||
<path d="M 10.293847 20.847362 L 30.938472 40.192837" />
|
||||
```
|
||||
|
||||
**Correct (1 decimal place):**
|
||||
|
||||
```svg
|
||||
<path d="M 10.3 20.8 L 30.9 40.2" />
|
||||
```
|
||||
|
||||
**Automate with SVGO:**
|
||||
|
||||
```bash
|
||||
npx svgo --precision=1 --multipass icon.svg
|
||||
```
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
title: Use useTransition Over Manual Loading States
|
||||
impact: LOW
|
||||
impactDescription: reduces re-renders and improves code clarity
|
||||
tags: rendering, transitions, useTransition, loading, state
|
||||
---
|
||||
|
||||
## Use useTransition Over Manual Loading States
|
||||
|
||||
Use `useTransition` instead of manual `useState` for loading states. This provides built-in `isPending` state and automatically manages transitions.
|
||||
|
||||
**Incorrect (manual loading state):**
|
||||
|
||||
```tsx
|
||||
function SearchResults() {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleSearch = async (value: string) => {
|
||||
setIsLoading(true)
|
||||
setQuery(value)
|
||||
const data = await fetchResults(value)
|
||||
setResults(data)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<input onChange={(e) => handleSearch(e.target.value)} />
|
||||
{isLoading && <Spinner />}
|
||||
<ResultsList results={results} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (useTransition with built-in pending state):**
|
||||
|
||||
```tsx
|
||||
import { useTransition, useState } from 'react'
|
||||
|
||||
function SearchResults() {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState([])
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setQuery(value) // Update input immediately
|
||||
|
||||
startTransition(async () => {
|
||||
// Fetch and update results
|
||||
const data = await fetchResults(value)
|
||||
setResults(data)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<input onChange={(e) => handleSearch(e.target.value)} />
|
||||
{isPending && <Spinner />}
|
||||
<ResultsList results={results} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- **Automatic pending state**: No need to manually manage `setIsLoading(true/false)`
|
||||
- **Error resilience**: Pending state correctly resets even if the transition throws
|
||||
- **Better responsiveness**: Keeps the UI responsive during updates
|
||||
- **Interrupt handling**: New transitions automatically cancel pending ones
|
||||
|
||||
Reference: [useTransition](https://react.dev/reference/react/useTransition)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
title: Defer State Reads to Usage Point
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids unnecessary subscriptions
|
||||
tags: rerender, searchParams, localStorage, optimization
|
||||
---
|
||||
|
||||
## Defer State Reads to Usage Point
|
||||
|
||||
Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
|
||||
|
||||
**Incorrect (subscribes to all searchParams changes):**
|
||||
|
||||
```tsx
|
||||
function ShareButton({ chatId }: { chatId: string }) {
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const handleShare = () => {
|
||||
const ref = searchParams.get('ref')
|
||||
shareChat(chatId, { ref })
|
||||
}
|
||||
|
||||
return <button onClick={handleShare}>Share</button>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (reads on demand, no subscription):**
|
||||
|
||||
```tsx
|
||||
function ShareButton({ chatId }: { chatId: string }) {
|
||||
const handleShare = () => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const ref = params.get('ref')
|
||||
shareChat(chatId, { ref })
|
||||
}
|
||||
|
||||
return <button onClick={handleShare}>Share</button>
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: Narrow Effect Dependencies
|
||||
impact: LOW
|
||||
impactDescription: minimizes effect re-runs
|
||||
tags: rerender, useEffect, dependencies, optimization
|
||||
---
|
||||
|
||||
## Narrow Effect Dependencies
|
||||
|
||||
Specify primitive dependencies instead of objects to minimize effect re-runs.
|
||||
|
||||
**Incorrect (re-runs on any user field change):**
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
console.log(user.id)
|
||||
}, [user])
|
||||
```
|
||||
|
||||
**Correct (re-runs only when id changes):**
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
console.log(user.id)
|
||||
}, [user.id])
|
||||
```
|
||||
|
||||
**For derived state, compute outside effect:**
|
||||
|
||||
```tsx
|
||||
// Incorrect: runs on width=767, 766, 765...
|
||||
useEffect(() => {
|
||||
if (width < 768) {
|
||||
enableMobileMode()
|
||||
}
|
||||
}, [width])
|
||||
|
||||
// Correct: runs only on boolean transition
|
||||
const isMobile = width < 768
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
enableMobileMode()
|
||||
}
|
||||
}, [isMobile])
|
||||
```
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
title: Calculate Derived State During Rendering
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids redundant renders and state drift
|
||||
tags: rerender, derived-state, useEffect, state
|
||||
---
|
||||
|
||||
## Calculate Derived State During Rendering
|
||||
|
||||
If a value can be computed from current props/state, do not store it in state or update it in an effect. Derive it during render to avoid extra renders and state drift. Do not set state in effects solely in response to prop changes; prefer derived values or keyed resets instead.
|
||||
|
||||
**Incorrect (redundant state and effect):**
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const [firstName, setFirstName] = useState('First')
|
||||
const [lastName, setLastName] = useState('Last')
|
||||
const [fullName, setFullName] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
setFullName(firstName + ' ' + lastName)
|
||||
}, [firstName, lastName])
|
||||
|
||||
return <p>{fullName}</p>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (derive during render):**
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const [firstName, setFirstName] = useState('First')
|
||||
const [lastName, setLastName] = useState('Last')
|
||||
const fullName = firstName + ' ' + lastName
|
||||
|
||||
return <p>{fullName}</p>
|
||||
}
|
||||
```
|
||||
|
||||
References: [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
title: Subscribe to Derived State
|
||||
impact: MEDIUM
|
||||
impactDescription: reduces re-render frequency
|
||||
tags: rerender, derived-state, media-query, optimization
|
||||
---
|
||||
|
||||
## Subscribe to Derived State
|
||||
|
||||
Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
|
||||
|
||||
**Incorrect (re-renders on every pixel change):**
|
||||
|
||||
```tsx
|
||||
function Sidebar() {
|
||||
const width = useWindowWidth() // updates continuously
|
||||
const isMobile = width < 768
|
||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (re-renders only when boolean changes):**
|
||||
|
||||
```tsx
|
||||
function Sidebar() {
|
||||
const isMobile = useMediaQuery('(max-width: 767px)')
|
||||
return <nav className={isMobile ? 'mobile' : 'desktop'} />
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
---
|
||||
title: Use Functional setState Updates
|
||||
impact: MEDIUM
|
||||
impactDescription: prevents stale closures and unnecessary callback recreations
|
||||
tags: react, hooks, useState, useCallback, callbacks, closures
|
||||
---
|
||||
|
||||
## Use Functional setState Updates
|
||||
|
||||
When updating state based on the current state value, use the functional update form of setState instead of directly referencing the state variable. This prevents stale closures, eliminates unnecessary dependencies, and creates stable callback references.
|
||||
|
||||
**Incorrect (requires state as dependency):**
|
||||
|
||||
```tsx
|
||||
function TodoList() {
|
||||
const [items, setItems] = useState(initialItems)
|
||||
|
||||
// Callback must depend on items, recreated on every items change
|
||||
const addItems = useCallback((newItems: Item[]) => {
|
||||
setItems([...items, ...newItems])
|
||||
}, [items]) // ❌ items dependency causes recreations
|
||||
|
||||
// Risk of stale closure if dependency is forgotten
|
||||
const removeItem = useCallback((id: string) => {
|
||||
setItems(items.filter(item => item.id !== id))
|
||||
}, []) // ❌ Missing items dependency - will use stale items!
|
||||
|
||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
||||
}
|
||||
```
|
||||
|
||||
The first callback is recreated every time `items` changes, which can cause child components to re-render unnecessarily. The second callback has a stale closure bug—it will always reference the initial `items` value.
|
||||
|
||||
**Correct (stable callbacks, no stale closures):**
|
||||
|
||||
```tsx
|
||||
function TodoList() {
|
||||
const [items, setItems] = useState(initialItems)
|
||||
|
||||
// Stable callback, never recreated
|
||||
const addItems = useCallback((newItems: Item[]) => {
|
||||
setItems(curr => [...curr, ...newItems])
|
||||
}, []) // ✅ No dependencies needed
|
||||
|
||||
// Always uses latest state, no stale closure risk
|
||||
const removeItem = useCallback((id: string) => {
|
||||
setItems(curr => curr.filter(item => item.id !== id))
|
||||
}, []) // ✅ Safe and stable
|
||||
|
||||
return <ItemsEditor items={items} onAdd={addItems} onRemove={removeItem} />
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
1. **Stable callback references** - Callbacks don't need to be recreated when state changes
|
||||
2. **No stale closures** - Always operates on the latest state value
|
||||
3. **Fewer dependencies** - Simplifies dependency arrays and reduces memory leaks
|
||||
4. **Prevents bugs** - Eliminates the most common source of React closure bugs
|
||||
|
||||
**When to use functional updates:**
|
||||
|
||||
- Any setState that depends on the current state value
|
||||
- Inside useCallback/useMemo when state is needed
|
||||
- Event handlers that reference state
|
||||
- Async operations that update state
|
||||
|
||||
**When direct updates are fine:**
|
||||
|
||||
- Setting state to a static value: `setCount(0)`
|
||||
- Setting state from props/arguments only: `setName(newName)`
|
||||
- State doesn't depend on previous value
|
||||
|
||||
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, the compiler can automatically optimize some cases, but functional updates are still recommended for correctness and to prevent stale closure bugs.
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
title: Use Lazy State Initialization
|
||||
impact: MEDIUM
|
||||
impactDescription: wasted computation on every render
|
||||
tags: react, hooks, useState, performance, initialization
|
||||
---
|
||||
|
||||
## Use Lazy State Initialization
|
||||
|
||||
Pass a function to `useState` for expensive initial values. Without the function form, the initializer runs on every render even though the value is only used once.
|
||||
|
||||
**Incorrect (runs on every render):**
|
||||
|
||||
```tsx
|
||||
function FilteredList({ items }: { items: Item[] }) {
|
||||
// buildSearchIndex() runs on EVERY render, even after initialization
|
||||
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items))
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
// When query changes, buildSearchIndex runs again unnecessarily
|
||||
return <SearchResults index={searchIndex} query={query} />
|
||||
}
|
||||
|
||||
function UserProfile() {
|
||||
// JSON.parse runs on every render
|
||||
const [settings, setSettings] = useState(
|
||||
JSON.parse(localStorage.getItem('settings') || '{}')
|
||||
)
|
||||
|
||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (runs only once):**
|
||||
|
||||
```tsx
|
||||
function FilteredList({ items }: { items: Item[] }) {
|
||||
// buildSearchIndex() runs ONLY on initial render
|
||||
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items))
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
return <SearchResults index={searchIndex} query={query} />
|
||||
}
|
||||
|
||||
function UserProfile() {
|
||||
// JSON.parse runs only on initial render
|
||||
const [settings, setSettings] = useState(() => {
|
||||
const stored = localStorage.getItem('settings')
|
||||
return stored ? JSON.parse(stored) : {}
|
||||
})
|
||||
|
||||
return <SettingsForm settings={settings} onChange={setSettings} />
|
||||
}
|
||||
```
|
||||
|
||||
Use lazy initialization when computing initial values from localStorage/sessionStorage, building data structures (indexes, maps), reading from the DOM, or performing heavy transformations.
|
||||
|
||||
For simple primitives (`useState(0)`), direct references (`useState(props.value)`), or cheap literals (`useState({})`), the function form is unnecessary.
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
|
||||
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 />
|
||||
```
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
title: Extract to Memoized Components
|
||||
impact: MEDIUM
|
||||
impactDescription: enables early returns
|
||||
tags: rerender, memo, useMemo, optimization
|
||||
---
|
||||
|
||||
## Extract to Memoized Components
|
||||
|
||||
Extract expensive work into memoized components to enable early returns before computation.
|
||||
|
||||
**Incorrect (computes avatar even when loading):**
|
||||
|
||||
```tsx
|
||||
function Profile({ user, loading }: Props) {
|
||||
const avatar = useMemo(() => {
|
||||
const id = computeAvatarId(user)
|
||||
return <Avatar id={id} />
|
||||
}, [user])
|
||||
|
||||
if (loading) return <Skeleton />
|
||||
return <div>{avatar}</div>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (skips computation when loading):**
|
||||
|
||||
```tsx
|
||||
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
|
||||
const id = useMemo(() => computeAvatarId(user), [user])
|
||||
return <Avatar id={id} />
|
||||
})
|
||||
|
||||
function Profile({ user, loading }: Props) {
|
||||
if (loading) return <Skeleton />
|
||||
return (
|
||||
<div>
|
||||
<UserAvatar user={user} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, manual memoization with `memo()` and `useMemo()` is not necessary. The compiler automatically optimizes re-renders.
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: Put Interaction Logic in Event Handlers
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids effect re-runs and duplicate side effects
|
||||
tags: rerender, useEffect, events, side-effects, dependencies
|
||||
---
|
||||
|
||||
## Put Interaction Logic in Event Handlers
|
||||
|
||||
If a side effect is triggered by a specific user action (submit, click, drag), run it in that event handler. Do not model the action as state + effect; it makes effects re-run on unrelated changes and can duplicate the action.
|
||||
|
||||
**Incorrect (event modeled as state + effect):**
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
useEffect(() => {
|
||||
if (submitted) {
|
||||
post('/api/register')
|
||||
showToast('Registered', theme)
|
||||
}
|
||||
}, [submitted, theme])
|
||||
|
||||
return <button onClick={() => setSubmitted(true)}>Submit</button>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (do it in the handler):**
|
||||
|
||||
```tsx
|
||||
function Form() {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
function handleSubmit() {
|
||||
post('/api/register')
|
||||
showToast('Registered', theme)
|
||||
}
|
||||
|
||||
return <button onClick={handleSubmit}>Submit</button>
|
||||
}
|
||||
```
|
||||
|
||||
Reference: [Should this code move to an event handler?](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler)
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
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
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
title: Use Transitions for Non-Urgent Updates
|
||||
impact: MEDIUM
|
||||
impactDescription: maintains UI responsiveness
|
||||
tags: rerender, transitions, startTransition, performance
|
||||
---
|
||||
|
||||
## Use Transitions for Non-Urgent Updates
|
||||
|
||||
Mark frequent, non-urgent state updates as transitions to maintain UI responsiveness.
|
||||
|
||||
**Incorrect (blocks UI on every scroll):**
|
||||
|
||||
```tsx
|
||||
function ScrollTracker() {
|
||||
const [scrollY, setScrollY] = useState(0)
|
||||
useEffect(() => {
|
||||
const handler = () => setScrollY(window.scrollY)
|
||||
window.addEventListener('scroll', handler, { passive: true })
|
||||
return () => window.removeEventListener('scroll', handler)
|
||||
}, [])
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (non-blocking updates):**
|
||||
|
||||
```tsx
|
||||
import { startTransition } from 'react'
|
||||
|
||||
function ScrollTracker() {
|
||||
const [scrollY, setScrollY] = useState(0)
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
startTransition(() => setScrollY(window.scrollY))
|
||||
}
|
||||
window.addEventListener('scroll', handler, { passive: true })
|
||||
return () => window.removeEventListener('scroll', handler)
|
||||
}, [])
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
title: Use useRef for Transient Values
|
||||
impact: MEDIUM
|
||||
impactDescription: avoids unnecessary re-renders on frequent updates
|
||||
tags: rerender, useref, state, performance
|
||||
---
|
||||
|
||||
## Use useRef for Transient Values
|
||||
|
||||
When a value changes frequently and you don't want a re-render on every update (e.g., mouse trackers, intervals, transient flags), store it in `useRef` instead of `useState`. Keep component state for UI; use refs for temporary DOM-adjacent values. Updating a ref does not trigger a re-render.
|
||||
|
||||
**Incorrect (renders every update):**
|
||||
|
||||
```tsx
|
||||
function Tracker() {
|
||||
const [lastX, setLastX] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent) => setLastX(e.clientX)
|
||||
window.addEventListener('mousemove', onMove)
|
||||
return () => window.removeEventListener('mousemove', onMove)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: lastX,
|
||||
width: 8,
|
||||
height: 8,
|
||||
background: 'black',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (no re-render for tracking):**
|
||||
|
||||
```tsx
|
||||
function Tracker() {
|
||||
const lastXRef = useRef(0)
|
||||
const dotRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const onMove = (e: MouseEvent) => {
|
||||
lastXRef.current = e.clientX
|
||||
const node = dotRef.current
|
||||
if (node) {
|
||||
node.style.transform = `translateX(${e.clientX}px)`
|
||||
}
|
||||
}
|
||||
window.addEventListener('mousemove', onMove)
|
||||
return () => window.removeEventListener('mousemove', onMove)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dotRef}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 8,
|
||||
height: 8,
|
||||
background: 'black',
|
||||
transform: 'translateX(0px)',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
title: Use after() for Non-Blocking Operations
|
||||
impact: MEDIUM
|
||||
impactDescription: faster response times
|
||||
tags: server, async, logging, analytics, side-effects
|
||||
---
|
||||
|
||||
## Use after() for Non-Blocking Operations
|
||||
|
||||
Use Next.js's `after()` to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
|
||||
|
||||
**Incorrect (blocks response):**
|
||||
|
||||
```tsx
|
||||
import { logUserAction } from '@/app/utils'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Perform mutation
|
||||
await updateDatabase(request)
|
||||
|
||||
// Logging blocks the response
|
||||
const userAgent = request.headers.get('user-agent') || 'unknown'
|
||||
await logUserAction({ userAgent })
|
||||
|
||||
return new Response(JSON.stringify({ status: 'success' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (non-blocking):**
|
||||
|
||||
```tsx
|
||||
import { after } from 'next/server'
|
||||
import { headers, cookies } from 'next/headers'
|
||||
import { logUserAction } from '@/app/utils'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// Perform mutation
|
||||
await updateDatabase(request)
|
||||
|
||||
// Log after response is sent
|
||||
after(async () => {
|
||||
const userAgent = (await headers()).get('user-agent') || 'unknown'
|
||||
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
|
||||
|
||||
logUserAction({ sessionCookie, userAgent })
|
||||
})
|
||||
|
||||
return new Response(JSON.stringify({ status: 'success' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The response is sent immediately while logging happens in the background.
|
||||
|
||||
**Common use cases:**
|
||||
|
||||
- Analytics tracking
|
||||
- Audit logging
|
||||
- Sending notifications
|
||||
- Cache invalidation
|
||||
- Cleanup tasks
|
||||
|
||||
**Important notes:**
|
||||
|
||||
- `after()` runs even if the response fails or redirects
|
||||
- Works in Server Actions, Route Handlers, and Server Components
|
||||
|
||||
Reference: [https://nextjs.org/docs/app/api-reference/functions/after](https://nextjs.org/docs/app/api-reference/functions/after)
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
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)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
title: Cross-Request LRU Caching
|
||||
impact: HIGH
|
||||
impactDescription: caches across requests
|
||||
tags: server, cache, lru, cross-request
|
||||
---
|
||||
|
||||
## Cross-Request LRU Caching
|
||||
|
||||
`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
import { LRUCache } from 'lru-cache'
|
||||
|
||||
const cache = new LRUCache<string, any>({
|
||||
max: 1000,
|
||||
ttl: 5 * 60 * 1000 // 5 minutes
|
||||
})
|
||||
|
||||
export async function getUser(id: string) {
|
||||
const cached = cache.get(id)
|
||||
if (cached) return cached
|
||||
|
||||
const user = await db.user.findUnique({ where: { id } })
|
||||
cache.set(id, user)
|
||||
return user
|
||||
}
|
||||
|
||||
// Request 1: DB query, result cached
|
||||
// Request 2: cache hit, no DB query
|
||||
```
|
||||
|
||||
Use when sequential user actions hit multiple endpoints needing the same data within seconds.
|
||||
|
||||
**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** LRU caching is especially effective because multiple concurrent requests can share the same function instance and cache. This means the cache persists across requests without needing external storage like Redis.
|
||||
|
||||
**In traditional serverless:** Each invocation runs in isolation, so consider Redis for cross-process caching.
|
||||
|
||||
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
title: Per-Request Deduplication with React.cache()
|
||||
impact: MEDIUM
|
||||
impactDescription: deduplicates within request
|
||||
tags: server, cache, react-cache, deduplication
|
||||
---
|
||||
|
||||
## Per-Request Deduplication with React.cache()
|
||||
|
||||
Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```typescript
|
||||
import { cache } from 'react'
|
||||
|
||||
export const getCurrentUser = cache(async () => {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return null
|
||||
return await db.user.findUnique({
|
||||
where: { id: session.user.id }
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
|
||||
|
||||
**Avoid inline objects as arguments:**
|
||||
|
||||
`React.cache()` uses shallow equality (`Object.is`) to determine cache hits. Inline objects create new references each call, preventing cache hits.
|
||||
|
||||
**Incorrect (always cache miss):**
|
||||
|
||||
```typescript
|
||||
const getUser = cache(async (params: { uid: number }) => {
|
||||
return await db.user.findUnique({ where: { id: params.uid } })
|
||||
})
|
||||
|
||||
// Each call creates new object, never hits cache
|
||||
getUser({ uid: 1 })
|
||||
getUser({ uid: 1 }) // Cache miss, runs query again
|
||||
```
|
||||
|
||||
**Correct (cache hit):**
|
||||
|
||||
```typescript
|
||||
const getUser = cache(async (uid: number) => {
|
||||
return await db.user.findUnique({ where: { id: uid } })
|
||||
})
|
||||
|
||||
// Primitive args use value equality
|
||||
getUser(1)
|
||||
getUser(1) // Cache hit, returns cached result
|
||||
```
|
||||
|
||||
If you must pass objects, pass the same reference:
|
||||
|
||||
```typescript
|
||||
const params = { uid: 1 }
|
||||
getUser(params) // Query runs
|
||||
getUser(params) // Cache hit (same reference)
|
||||
```
|
||||
|
||||
**Next.js-Specific Note:**
|
||||
|
||||
In Next.js, the `fetch` API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request, so you don't need `React.cache()` for `fetch` calls. However, `React.cache()` is still essential for other async tasks:
|
||||
|
||||
- Database queries (Prisma, Drizzle, etc.)
|
||||
- Heavy computations
|
||||
- Authentication checks
|
||||
- File system operations
|
||||
- Any non-fetch async work
|
||||
|
||||
Use `React.cache()` to deduplicate these operations across your component tree.
|
||||
|
||||
Reference: [React.cache documentation](https://react.dev/reference/react/cache)
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
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.
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
title: Parallel Data Fetching with Component Composition
|
||||
impact: CRITICAL
|
||||
impactDescription: eliminates server-side waterfalls
|
||||
tags: server, rsc, parallel-fetching, composition
|
||||
---
|
||||
|
||||
## Parallel Data Fetching with Component Composition
|
||||
|
||||
React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
|
||||
|
||||
**Incorrect (Sidebar waits for Page's fetch to complete):**
|
||||
|
||||
```tsx
|
||||
export default async function Page() {
|
||||
const header = await fetchHeader()
|
||||
return (
|
||||
<div>
|
||||
<div>{header}</div>
|
||||
<Sidebar />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function Sidebar() {
|
||||
const items = await fetchSidebarItems()
|
||||
return <nav>{items.map(renderItem)}</nav>
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (both fetch simultaneously):**
|
||||
|
||||
```tsx
|
||||
async function Header() {
|
||||
const data = await fetchHeader()
|
||||
return <div>{data}</div>
|
||||
}
|
||||
|
||||
async function Sidebar() {
|
||||
const items = await fetchSidebarItems()
|
||||
return <nav>{items.map(renderItem)}</nav>
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Sidebar />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Alternative with children prop:**
|
||||
|
||||
```tsx
|
||||
async function Header() {
|
||||
const data = await fetchHeader()
|
||||
return <div>{data}</div>
|
||||
}
|
||||
|
||||
async function Sidebar() {
|
||||
const items = await fetchSidebarItems()
|
||||
return <nav>{items.map(renderItem)}</nav>
|
||||
}
|
||||
|
||||
function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Layout>
|
||||
<Sidebar />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
title: Minimize Serialization at RSC Boundaries
|
||||
impact: HIGH
|
||||
impactDescription: reduces data transfer size
|
||||
tags: server, rsc, serialization, props
|
||||
---
|
||||
|
||||
## Minimize Serialization at RSC Boundaries
|
||||
|
||||
The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so **size matters a lot**. Only pass fields that the client actually uses.
|
||||
|
||||
**Incorrect (serializes all 50 fields):**
|
||||
|
||||
```tsx
|
||||
async function Page() {
|
||||
const user = await fetchUser() // 50 fields
|
||||
return <Profile user={user} />
|
||||
}
|
||||
|
||||
'use client'
|
||||
function Profile({ user }: { user: User }) {
|
||||
return <div>{user.name}</div> // uses 1 field
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (serializes only 1 field):**
|
||||
|
||||
```tsx
|
||||
async function Page() {
|
||||
const user = await fetchUser()
|
||||
return <Profile name={user.name} />
|
||||
}
|
||||
|
||||
'use client'
|
||||
function Profile({ name }: { name: string }) {
|
||||
return <div>{name}</div>
|
||||
}
|
||||
```
|
||||
253
src/resources/skills/userinterface-wiki/SKILL.md
Normal file
253
src/resources/skills/userinterface-wiki/SKILL.md
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
---
|
||||
name: userinterface-wiki
|
||||
description: UI/UX best practices for web interfaces. Use when reviewing animations, CSS, audio, typography, UX patterns, prefetching, or icon implementations. Covers 11 categories from animation principles to typography. Outputs file:line findings.
|
||||
license: MIT
|
||||
metadata:
|
||||
author: raphael-salaja
|
||||
version: "3.0.0"
|
||||
---
|
||||
|
||||
# User Interface Wiki
|
||||
|
||||
Comprehensive UI/UX best practices guide for web interfaces. Contains 152 rules across 12 categories, prioritized by impact to guide automated code review and generation.
|
||||
|
||||
## When to Apply
|
||||
|
||||
Reference these guidelines when:
|
||||
- Implementing or reviewing animations (CSS transitions, Motion/Framer Motion)
|
||||
- Choosing between springs, easing curves, or no animation
|
||||
- Working with AnimatePresence and exit animations
|
||||
- Writing CSS with pseudo-elements or View Transitions API
|
||||
- Adding audio feedback or procedural sound to UI
|
||||
- Building morphing icon components
|
||||
- Animating container width/height with dynamic content
|
||||
- Designing UI that respects cognitive psychology (Fitts's, Hick's, Miller's laws)
|
||||
- Implementing predictive prefetching for perceived performance
|
||||
- Setting up typography, OpenType features, or numeric formatting
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefixes |
|
||||
|----------|----------|--------|----------|
|
||||
| 1 | Animation Principles | CRITICAL | `timing-`, `physics-`, `staging-` |
|
||||
| 2 | Timing Functions | HIGH | `spring-`, `easing-`, `duration-`, `none-` |
|
||||
| 3 | Exit Animations | HIGH | `exit-`, `presence-`, `mode-`, `nested-` |
|
||||
| 4 | CSS Pseudo Elements | MEDIUM | `pseudo-`, `transition-`, `native-` |
|
||||
| 5 | Audio Feedback | MEDIUM | `a11y-`, `appropriate-`, `impl-`, `weight-` |
|
||||
| 6 | Sound Synthesis | MEDIUM | `context-`, `envelope-`, `design-`, `param-` |
|
||||
| 7 | Morphing Icons | LOW | `morphing-` |
|
||||
| 8 | Container Animation | MEDIUM | `container-` |
|
||||
| 9 | Laws of UX | HIGH | `ux-` |
|
||||
| 10 | Predictive Prefetching | MEDIUM | `prefetch-` |
|
||||
| 11 | Typography | MEDIUM | `type-` |
|
||||
| 12 | Visual Design | HIGH | `visual-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. Animation Principles (CRITICAL)
|
||||
|
||||
- `timing-under-300ms` - User animations must complete within 300ms
|
||||
- `timing-consistent` - Similar elements use identical timing values
|
||||
- `timing-no-entrance-context-menu` - Context menus: no entrance animation, exit only
|
||||
- `easing-natural-decay` - Use exponential ramps for natural decay, not linear
|
||||
- `easing-no-linear-motion` - Linear easing only for progress indicators
|
||||
- `physics-active-state` - Interactive elements need :active scale transform
|
||||
- `physics-subtle-deformation` - Squash/stretch in 0.95-1.05 range
|
||||
- `physics-spring-for-overshoot` - Springs for overshoot-and-settle, not easing
|
||||
- `physics-no-excessive-stagger` - Stagger delays under 50ms per item
|
||||
- `staging-one-focal-point` - One prominent animation at a time
|
||||
- `staging-dim-background` - Dim modal/dialog backgrounds
|
||||
- `staging-z-index-hierarchy` - Animated elements respect z-index layers
|
||||
|
||||
### 2. Timing Functions (HIGH)
|
||||
|
||||
- `spring-for-gestures` - Gesture-driven motion (drag, flick) must use springs
|
||||
- `spring-for-interruptible` - Interruptible motion must use springs
|
||||
- `spring-preserves-velocity` - Springs preserve input energy on release
|
||||
- `spring-params-balanced` - Avoid excessive oscillation in spring params
|
||||
- `easing-for-state-change` - System state changes use easing curves
|
||||
- `easing-entrance-ease-out` - Entrances use ease-out
|
||||
- `easing-exit-ease-in` - Exits use ease-in
|
||||
- `easing-transition-ease-in-out` - View transitions use ease-in-out
|
||||
- `easing-linear-only-progress` - Linear only for progress/time representation
|
||||
- `duration-press-hover` - Press/hover: 120-180ms
|
||||
- `duration-small-state` - Small state changes: 180-260ms
|
||||
- `duration-max-300ms` - User-initiated max 300ms
|
||||
- `duration-shorten-before-curve` - Fix slow feel with shorter duration, not curve
|
||||
- `none-high-frequency` - No animation for high-frequency interactions
|
||||
- `none-keyboard-navigation` - Keyboard navigation instant, no animation
|
||||
- `none-context-menu-entrance` - Context menus: no entrance, exit only
|
||||
|
||||
### 3. Exit Animations (HIGH)
|
||||
|
||||
- `exit-requires-wrapper` - Conditional motion elements need AnimatePresence wrapper
|
||||
- `exit-prop-required` - Elements in AnimatePresence need exit prop
|
||||
- `exit-key-required` - Dynamic lists need unique keys, not index
|
||||
- `exit-matches-initial` - Exit mirrors initial for symmetry
|
||||
- `presence-hook-in-child` - useIsPresent in child, not parent
|
||||
- `presence-safe-to-remove` - Call safeToRemove after async cleanup
|
||||
- `presence-disable-interactions` - Disable interactions on exiting elements
|
||||
- `mode-wait-doubles-duration` - Mode "wait" doubles duration; halve timing
|
||||
- `mode-sync-layout-conflict` - Mode "sync" causes layout conflicts
|
||||
- `mode-pop-layout-for-lists` - Use popLayout for list reordering
|
||||
- `nested-propagate-required` - Nested AnimatePresence needs propagate prop
|
||||
- `nested-consistent-timing` - Coordinate parent-child exit durations
|
||||
|
||||
### 4. CSS Pseudo Elements (MEDIUM)
|
||||
|
||||
- `pseudo-content-required` - ::before/::after need content property
|
||||
- `pseudo-over-dom-node` - Pseudo-elements over extra DOM nodes for decoration
|
||||
- `pseudo-position-relative-parent` - Parent needs position: relative
|
||||
- `pseudo-z-index-layering` - Z-index for correct pseudo-element layering
|
||||
- `pseudo-hit-target-expansion` - Negative inset for larger hit targets
|
||||
- `pseudo-marker-styling` - Use ::marker for custom list bullet styles
|
||||
- `pseudo-first-line-styling` - Use ::first-line for typographic treatments
|
||||
- `transition-name-required` - View transitions need view-transition-name
|
||||
- `transition-name-unique` - Each transition name unique during transition
|
||||
- `transition-name-cleanup` - Remove transition name after completion
|
||||
- `transition-over-js-library` - Prefer View Transitions API over JS libraries
|
||||
- `transition-style-pseudo-elements` - Style ::view-transition-group for custom animations
|
||||
- `native-backdrop-styling` - Use ::backdrop for dialog backgrounds
|
||||
- `native-placeholder-styling` - Use ::placeholder for input styling
|
||||
- `native-selection-styling` - Use ::selection for text selection styling
|
||||
|
||||
### 5. Audio Feedback (MEDIUM)
|
||||
|
||||
- `a11y-visual-equivalent` - Every sound must have a visual equivalent
|
||||
- `a11y-toggle-setting` - Provide toggle to disable sounds
|
||||
- `a11y-reduced-motion-check` - Respect prefers-reduced-motion for sound
|
||||
- `a11y-volume-control` - Allow independent volume adjustment
|
||||
- `appropriate-no-high-frequency` - No sound on typing or keyboard nav
|
||||
- `appropriate-confirmations-only` - Sound for payments, uploads, submissions
|
||||
- `appropriate-errors-warnings` - Sound for errors that can't be overlooked
|
||||
- `appropriate-no-decorative` - No sound on hover or decorative moments
|
||||
- `appropriate-no-punishing` - Inform, don't punish with harsh sounds
|
||||
- `impl-preload-audio` - Preload audio files to avoid delay
|
||||
- `impl-default-subtle` - Default volume subtle (0.3), not loud
|
||||
- `impl-reset-current-time` - Reset currentTime before replay
|
||||
- `weight-match-action` - Sound weight matches action importance
|
||||
- `weight-duration-matches-action` - Sound duration matches action duration
|
||||
|
||||
### 6. Sound Synthesis (MEDIUM)
|
||||
|
||||
- `context-reuse-single` - Reuse single AudioContext, don't create per sound
|
||||
- `context-resume-suspended` - Resume suspended AudioContext before playing
|
||||
- `context-cleanup-nodes` - Disconnect audio nodes after playback
|
||||
- `envelope-exponential-decay` - Exponential ramps for natural decay
|
||||
- `envelope-no-zero-target` - Exponential ramps target 0.001, not 0
|
||||
- `envelope-set-initial-value` - Set initial value before ramping
|
||||
- `design-noise-for-percussion` - Filtered noise for clicks/taps
|
||||
- `design-oscillator-for-tonal` - Oscillators with pitch sweep for tonal sounds
|
||||
- `design-filter-for-character` - Bandpass filter to shape percussive sounds
|
||||
- `param-click-duration` - Click sounds: 5-15ms duration
|
||||
- `param-filter-frequency-range` - Click filter: 3000-6000Hz
|
||||
- `param-reasonable-gain` - Gain under 1.0 to prevent clipping
|
||||
- `param-q-value-range` - Filter Q: 2-5 for focused but natural
|
||||
|
||||
### 7. Morphing Icons (LOW)
|
||||
|
||||
- `morphing-three-lines` - Every icon uses exactly 3 SVG lines
|
||||
- `morphing-use-collapsed` - Unused lines use collapsed constant
|
||||
- `morphing-consistent-viewbox` - All icons share same viewBox (14x14)
|
||||
- `morphing-group-variants` - Rotational variants share group and base lines
|
||||
- `morphing-spring-rotation` - Spring physics for grouped icon rotation
|
||||
- `morphing-reduced-motion` - Respect prefers-reduced-motion
|
||||
- `morphing-jump-non-grouped` - Instant rotation jump between non-grouped icons
|
||||
- `morphing-strokelinecap-round` - Round stroke line caps
|
||||
- `morphing-aria-hidden` - Icon SVGs are aria-hidden
|
||||
|
||||
### 8. Container Animation (MEDIUM)
|
||||
|
||||
- `container-two-div-pattern` - Outer animated div, inner measured div; never same element
|
||||
- `container-guard-initial-zero` - Guard bounds === 0 on initial render, fall back to "auto"
|
||||
- `container-use-resize-observer` - Use ResizeObserver for measurement, not getBoundingClientRect
|
||||
- `container-overflow-hidden` - Set overflow: hidden on animated container during transitions
|
||||
- `container-no-excessive-use` - Use sparingly: buttons, accordions, interactive elements
|
||||
- `container-callback-ref` - Use callback ref (not useRef) for measurement hooks
|
||||
- `container-transition-delay` - Add small delay for natural catching-up feel
|
||||
|
||||
### 9. Laws of UX (HIGH)
|
||||
|
||||
- `ux-fitts-target-size` - Size interactive targets for easy clicking (min 32px)
|
||||
- `ux-fitts-hit-area` - Expand hit areas with invisible padding or pseudo-elements
|
||||
- `ux-hicks-minimize-choices` - Minimize choices to reduce decision time
|
||||
- `ux-millers-chunking` - Chunk data into groups of 5-9 for scannability
|
||||
- `ux-doherty-under-400ms` - Respond within 400ms to feel instant
|
||||
- `ux-doherty-perceived-speed` - Fake speed with skeletons, optimistic UI, progress indicators
|
||||
- `ux-postels-accept-messy-input` - Accept messy input, output clean data
|
||||
- `ux-progressive-disclosure` - Show what matters now, reveal complexity later
|
||||
- `ux-jakobs-familiar-patterns` - Use familiar UI patterns users know from other sites
|
||||
- `ux-aesthetic-usability` - Visual polish increases perceived usability
|
||||
- `ux-proximity-grouping` - Group related elements spatially with tighter spacing
|
||||
- `ux-similarity-consistency` - Similar elements should look alike
|
||||
- `ux-common-region-boundaries` - Use boundaries to group related content
|
||||
- `ux-von-restorff-emphasis` - Make important elements visually distinct
|
||||
- `ux-serial-position` - Place key items first or last in sequences
|
||||
- `ux-peak-end-finish-strong` - End experiences with clear success states
|
||||
- `ux-teslers-complexity` - Move complexity to the system, not the user
|
||||
- `ux-goal-gradient-progress` - Show progress toward completion
|
||||
- `ux-zeigarnik-show-incomplete` - Show incomplete state to drive completion
|
||||
- `ux-pragnanz-simplify` - Simplify complex visuals into clear forms
|
||||
- `ux-pareto-prioritize-features` - Prioritize the critical 20% of features
|
||||
- `ux-cognitive-load-reduce` - Minimize extraneous cognitive load
|
||||
- `ux-uniform-connectedness` - Visually connect related elements with lines or frames
|
||||
|
||||
### 10. Predictive Prefetching (MEDIUM)
|
||||
|
||||
- `prefetch-trajectory-over-hover` - Trajectory prediction over hover; reclaims 100-200ms
|
||||
- `prefetch-not-everything` - Prefetch by intent, not viewport; avoid wasted bandwidth
|
||||
- `prefetch-hit-slop` - Use hitSlop to trigger predictions earlier
|
||||
- `prefetch-touch-fallback` - Fall back gracefully on touch devices (no cursor)
|
||||
- `prefetch-keyboard-tab` - Prefetch on keyboard navigation when focus approaches
|
||||
- `prefetch-use-selectively` - Use predictive prefetching where latency is noticeable
|
||||
|
||||
### 11. Typography (MEDIUM)
|
||||
|
||||
- `type-tabular-nums-for-data` - Tabular numbers for columns, dashboards, pricing
|
||||
- `type-oldstyle-nums-for-prose` - Oldstyle numbers blend into body text
|
||||
- `type-slashed-zero` - Slashed zero in code-adjacent UIs
|
||||
- `type-opentype-contextual-alternates` - Keep calt enabled for contextual glyph adjustment
|
||||
- `type-disambiguation-stylistic-set` - Enable ss02 to distinguish I/l/1 and 0/O
|
||||
- `type-optical-sizing-auto` - Leave font-optical-sizing auto for size-adaptive glyphs
|
||||
- `type-antialiased-on-retina` - Antialiased font smoothing on retina displays
|
||||
- `type-text-wrap-balance-headings` - text-wrap: balance on headings for even lines
|
||||
- `type-underline-offset` - Offset underlines below descenders
|
||||
- `type-no-font-synthesis` - Disable font-synthesis to prevent faux bold/italic
|
||||
- `type-font-display-swap` - Use font-display: swap to avoid invisible text during load
|
||||
- `type-variable-weight-continuous` - Use continuous weight values (100-900) with variable fonts
|
||||
- `type-text-wrap-pretty` - text-wrap: pretty for body text to reduce orphans
|
||||
- `type-justify-with-hyphens` - Pair text-align: justify with hyphens: auto
|
||||
- `type-letter-spacing-uppercase` - Add letter-spacing to uppercase and small-caps text
|
||||
- `type-proper-fractions` - Use diagonal-fractions for proper typographic fractions
|
||||
|
||||
### 12. Visual Design (HIGH)
|
||||
|
||||
- `visual-concentric-radius` - Inner radius = outer radius minus padding for nested elements
|
||||
- `visual-layered-shadows` - Layer multiple shadows for realistic depth
|
||||
- `visual-shadow-direction` - All shadows share same offset direction (single light source)
|
||||
- `visual-no-pure-black-shadow` - Use neutral colors, not pure black, for shadows
|
||||
- `visual-shadow-matches-elevation` - Shadow size indicates elevation in consistent scale
|
||||
- `visual-animate-shadow-pseudo` - Animate shadow via pseudo-element opacity for performance
|
||||
- `visual-consistent-spacing-scale` - Use a consistent spacing scale, not arbitrary values
|
||||
- `visual-border-alpha-colors` - Semi-transparent borders adapt to any background
|
||||
- `visual-button-shadow-anatomy` - Six-layer shadow anatomy for polished buttons
|
||||
|
||||
## How to Use
|
||||
|
||||
Read individual rule files for detailed explanations and code examples:
|
||||
|
||||
```
|
||||
rules/timing-under-300ms.md
|
||||
rules/spring-for-gestures.md
|
||||
rules/ux-doherty-under-400ms.md
|
||||
rules/type-tabular-nums-for-data.md
|
||||
```
|
||||
|
||||
Each rule file contains:
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect code example with explanation
|
||||
- Correct code example with explanation
|
||||
|
||||
## Full Compiled Document
|
||||
|
||||
For the complete guide with all rules expanded: `AGENTS.md`
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue