diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 14f024b8..b7dd9905 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -334,7 +334,7 @@ }, "policy": { "installation": "AVAILABLE", - "authentication": "ON_INSTALL" + "authentication": "ON_USE" }, "category": "Coding" }, diff --git a/plugins/build-web-apps/.app.json b/plugins/build-web-apps/.app.json index ceeb10a7..16ad3cf0 100644 --- a/plugins/build-web-apps/.app.json +++ b/plugins/build-web-apps/.app.json @@ -2,9 +2,6 @@ "apps": { "stripe": { "id": "connector_690ab09fa43c8191bca40280e4563238" - }, - "vercel": { - "id": "connector_690a90ec05c881918afb6a55dc9bbaa1" } } } diff --git a/plugins/build-web-apps/.codex-plugin/plugin.json b/plugins/build-web-apps/.codex-plugin/plugin.json index 53572321..ee939bd5 100644 --- a/plugins/build-web-apps/.codex-plugin/plugin.json +++ b/plugins/build-web-apps/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "build-web-apps", "version": "0.1.0", - "description": "Build web apps with workflows for UI reviews, React improvements, deployment, payments, and database design.", + "description": "Build web apps with frontend asset design, browser testing, UI components, payments, and database guidance.", "author": { "name": "OpenAI", "email": "support@openai.com", @@ -13,20 +13,22 @@ "keywords": [ "build-web-apps", "build", - "react", - "vercel", + "product-apps", + "frontend", + "image-generation", + "browser-testing", "stripe", "supabase", "shadcn", "full-stack", - "ui-review" + "frontend-qa" ], "skills": "./skills/", "apps": "./.app.json", "interface": { "displayName": "Build Web Apps", - "shortDescription": "Build, review, ship, and scale web apps across UI, React, deployment, payments, and databases", - "longDescription": "Use Build Web Apps to review and improve a web app's UI, apply React and Next.js guidance, deploy projects to Vercel, wire up Stripe payments, and design or tune Postgres schemas and queries, with connected Vercel and Stripe apps plus bundled database guidance.", + "shortDescription": "Build frontend-focused web apps with generated assets, browser testing, payments, and databases", + "longDescription": "Use Build Web Apps to create frontend application surfaces with Codex-generated visual assets, verify them with the Browser plugin and built-in app browser, compose shadcn/ui, wire Stripe payments, and design or tune Supabase/Postgres data flows.", "developerName": "OpenAI", "category": "Coding", "capabilities": [ @@ -37,7 +39,9 @@ "websiteURL": "https://openai.com/", "privacyPolicyURL": "https://openai.com/policies/privacy-policy/", "termsOfServiceURL": "https://openai.com/policies/terms-of-use/", - "defaultPrompt": "Review this app's UI, improve the React implementation, wire up payments or database changes, and help deploy it", + "defaultPrompt": [ + "Build a frontend app with generated visual assets and browser verification." + ], "brandColor": "#111111", "composerIcon": "./assets/build-web-apps-small.svg", "logo": "./assets/app-icon.png", diff --git a/plugins/build-web-apps/README.md b/plugins/build-web-apps/README.md index 5c59210e..66992b77 100644 --- a/plugins/build-web-apps/README.md +++ b/plugins/build-web-apps/README.md @@ -1,63 +1,14 @@ # Build Web Apps Plugin -This plugin packages builder-oriented workflows in `plugins/build-web-apps`. +Builder workflows for frontend apps, shadcn/ui, Stripe, and Supabase/Postgres. -It currently includes these skills: +## Skills -- `deploy-to-vercel` -- `react-best-practices` +- `frontend-app-builder` - `shadcn-best-practices` - `stripe-best-practices` - `supabase-best-practices` -- `web-design-guidelines` -It is scaffolded to use these connected apps: +## Purpose -- `stripe` -- `vercel` - -## What It Covers - -- deployment and hosting operations through the Vercel app -- React and Next.js performance guidance sourced from Vercel best practices -- shadcn/ui composition, styling, and component usage guidance -- Stripe integration design across payments, subscriptions, Connect, and Treasury -- Supabase/Postgres schema, performance, and RLS best practices -- UI review guidance against web interface design guidelines -- end-to-end product building workflows that span frontend, backend, payments, - and deployment - -## Plugin Structure - -The plugin now lives at: - -- `plugins/build-web-apps/` - -with this shape: - -- `.codex-plugin/plugin.json` - - required plugin manifest - - defines plugin metadata and points Codex at the plugin contents - -- `.app.json` - - plugin-local app dependency manifest - - points Codex at the connected Stripe and Vercel apps used by the bundled - workflows - -- `agents/` - - plugin-level agent metadata - - currently includes `agents/openai.yaml` for the OpenAI surface - -- `skills/` - - the actual skill payload - - currently includes deployment, UI, payments, and database-focused skills - -## Notes - -This plugin is app-backed through `.app.json` and currently combines: - -- Vercel deployment workflows -- React and Next.js optimization guidance -- shadcn/ui frontend implementation guidance -- Stripe integration guidance -- web design and UI review guidance +Use for web app builds that need frontend implementation with generated visual assets and browser testing, plus focused shadcn/ui, Stripe, or Supabase/Postgres guidance when those areas are needed. diff --git a/plugins/build-web-apps/agents/openai.yaml b/plugins/build-web-apps/agents/openai.yaml index f692536b..3c9d2d98 100644 --- a/plugins/build-web-apps/agents/openai.yaml +++ b/plugins/build-web-apps/agents/openai.yaml @@ -1,15 +1,11 @@ interface: display_name: "Build Web Apps" - short_description: "Build, review, ship, and scale web apps across UI, React, deployment, payments, and databases" + short_description: "Build frontend-focused web apps" icon_small: "./assets/build-web-apps-small.svg" icon_large: "./assets/app-icon.png" - default_prompt: "Use Build Web Apps to review a web app's UI, improve the React implementation, wire up payments or database changes, and help deploy it." + default_prompt: "Build a frontend app with generated visual assets and browser verification." dependencies: tools: - type: "app" value: "stripe" - description: "Connected Stripe app" - - type: "app" - value: "vercel" - description: "Connected Vercel app" diff --git a/plugins/build-web-apps/skills/deploy-to-vercel/Archive.zip b/plugins/build-web-apps/skills/deploy-to-vercel/Archive.zip deleted file mode 100644 index 2945baff..00000000 Binary files a/plugins/build-web-apps/skills/deploy-to-vercel/Archive.zip and /dev/null differ diff --git a/plugins/build-web-apps/skills/deploy-to-vercel/SKILL.md b/plugins/build-web-apps/skills/deploy-to-vercel/SKILL.md deleted file mode 100644 index 60f8d7ae..00000000 --- a/plugins/build-web-apps/skills/deploy-to-vercel/SKILL.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -name: deploy-to-vercel -description: Deploy applications and websites to Vercel. Use when the user requests deployment actions like "deploy my app", "deploy and give me the link", "push this live", or "create a preview deployment". -metadata: - author: vercel - version: "3.0.0" ---- - -# Deploy to Vercel - -Deploy any project to Vercel. **Always deploy as preview** (not production) unless the user explicitly asks for production. - -The goal is to get the user into the best long-term setup: their project linked to Vercel with git-push deploys. Every method below tries to move the user closer to that state. - -## Step 1: Gather Project State - -Run all four checks before deciding which method to use: - -```bash -# 1. Check for a git remote -git remote get-url origin 2>/dev/null - -# 2. Check if locally linked to a Vercel project (either file means linked) -cat .vercel/project.json 2>/dev/null || cat .vercel/repo.json 2>/dev/null - -# 3. Check if the Vercel CLI is installed and authenticated -vercel whoami 2>/dev/null - -# 4. List available teams (if authenticated) -vercel teams list --format json 2>/dev/null -``` - -### Team selection - -If the user belongs to multiple teams, present all available team slugs as a bulleted list and ask which one to deploy to. Once the user picks a team, proceed immediately to the next step — do not ask for additional confirmation. - -Pass the team slug via `--scope` on all subsequent CLI commands (`vercel deploy`, `vercel link`, `vercel inspect`, etc.): - -```bash -vercel deploy [path] -y --no-wait --scope -``` - -If the project is already linked (`.vercel/project.json` or `.vercel/repo.json` exists), the `orgId` in those files determines the team — no need to ask again. If there is only one team (or just a personal account), skip the prompt and use it directly. - -**About the `.vercel/` directory:** A linked project has either: -- `.vercel/project.json` — created by `vercel link` (single project linking). Contains `projectId` and `orgId`. -- `.vercel/repo.json` — created by `vercel link --repo` (repo-based linking). Contains `orgId`, `remoteName`, and a `projects` array mapping directories to Vercel project IDs. - -Either file means the project is linked. Check for both. - -**Do NOT** use `vercel project inspect`, `vercel ls`, or `vercel link` to detect state in an unlinked directory — without a `.vercel/` config, they will interactively prompt (or with `--yes`, silently link as a side-effect). Only `vercel whoami` is safe to run anywhere. - -## Step 2: Choose a Deploy Method - -### Linked (`.vercel/` exists) + has git remote → Git Push - -This is the ideal state. The project is linked and has git integration. - -1. **Ask the user before pushing.** Never push without explicit approval: - ``` - This project is connected to Vercel via git. I can commit and push to - trigger a deployment. Want me to proceed? - ``` - -2. **Commit and push:** - ```bash - git add . - git commit -m "deploy: " - git push - ``` - Vercel automatically builds from the push. Non-production branches get preview deployments; the production branch (usually `main`) gets a production deployment. - -3. **Retrieve the preview URL.** If the CLI is authenticated: - ```bash - sleep 5 - vercel ls --format json - ``` - The JSON output has a `deployments` array. Find the latest entry — its `url` field is the preview URL. - - If the CLI is not authenticated, tell the user to check the Vercel dashboard or the commit status checks on their git provider for the preview URL. - ---- - -### Linked (`.vercel/` exists) + no git remote → `vercel deploy` - -The project is linked but there's no git repo. Deploy directly with the CLI. - -```bash -vercel deploy [path] -y --no-wait -``` - -Use `--no-wait` so the CLI returns immediately with the deployment URL instead of blocking until the build finishes (builds can take a while). Then check on the deployment status with: - -```bash -vercel inspect -``` - -For production deploys (only if user explicitly asks): -```bash -vercel deploy [path] --prod -y --no-wait -``` - ---- - -### Not linked + CLI is authenticated → Link first, then deploy - -The CLI is working but the project isn't linked yet. This is the opportunity to get the user into the best state. - -1. **Ask the user which team to deploy to.** Present the team slugs from Step 1 as a bulleted list. If there's only one team (or just a personal account), skip this step. - -2. **Once a team is selected, proceed directly to linking.** Tell the user what will happen but do not ask for separate confirmation: - ``` - Linking this project to on Vercel. This will create a Vercel - project to deploy to and enable automatic deployments on future git pushes. - ``` - -3. **If a git remote exists**, use repo-based linking with the selected team scope: - ```bash - vercel link --repo --scope - ``` - This reads the git remote URL and matches it to existing Vercel projects that deploy from that repo. It creates `.vercel/repo.json`. This is much more reliable than `vercel link` (without `--repo`), which tries to match by directory name and often fails when the local folder and Vercel project are named differently. - - **If there is no git remote**, fall back to standard linking: - ```bash - vercel link --scope - ``` - This prompts the user to select or create a project. It creates `.vercel/project.json`. - -4. **Then deploy using the best available method:** - - If a git remote exists → commit and push (see git push method above) - - If no git remote → `vercel deploy [path] -y --no-wait --scope `, then `vercel inspect ` to check status - ---- - -### Not linked + CLI not authenticated → Install, auth, link, deploy - -The Vercel CLI isn't set up at all. - -1. **Install the CLI (if not already installed):** - ```bash - npm install -g vercel - ``` - -2. **Authenticate:** - ```bash - vercel login - ``` - The user completes auth in their browser. If running in a non-interactive environment where login is not possible, skip to the **no-auth fallback** below. - -3. **Ask which team to deploy to** — present team slugs from `vercel teams list --format json` as a bulleted list. If only one team / personal account, skip. Once selected, proceed immediately. - -4. **Link the project** with the selected team scope (use `--repo` if a git remote exists, plain `vercel link` otherwise): - ```bash - vercel link --repo --scope # if git remote exists - vercel link --scope # if no git remote - ``` - -5. **Deploy** using the best available method (git push if remote exists, otherwise `vercel deploy -y --no-wait --scope `, then `vercel inspect ` to check status). - ---- - -### No-Auth Fallback — Codex sandbox - -**When to use:** In the Codex sandbox where the CLI may not be authenticated. Codex runs in a sandboxed environment by default — try the CLI first, and fall back to the deploy script if auth fails. - -1. **Check whether the Vercel CLI is installed** (no escalation needed for this check): - ```bash - command -v vercel - ``` - -2. **If `vercel` is installed**, try deploying with the CLI: - ```bash - vercel deploy [path] -y --no-wait - ``` - -3. **If `vercel` is not installed, or the CLI fails with "No existing credentials found"**, use the fallback script: - ```bash - skill_dir="" - - # Deploy current directory - bash "$skill_dir/resources/deploy-codex.sh" - - # Deploy specific project - bash "$skill_dir/resources/deploy-codex.sh" /path/to/project - - # Deploy existing tarball - bash "$skill_dir/resources/deploy-codex.sh" /path/to/project.tgz - ``` - -The script handles framework detection, packaging, and deployment. It waits for the build to complete and returns JSON with `previewUrl` and `claimUrl`. - -**Tell the user:** "Your deployment is ready at [previewUrl]. Claim it at [claimUrl] to manage your deployment." - -**Escalated network access:** Only escalate the actual deploy command if sandboxing blocks the network call (`sandbox_permissions=require_escalated`). Do **not** escalate the `command -v vercel` check. - ---- - -## Agent-Specific Notes - -### Codex - -Codex runs in a sandbox. Check if the CLI is available first, then fall back to the deploy script. Go to the **no-auth fallback — Codex sandbox**. - ---- - -## Output - -Always show the user the deployment URL. - -- **Git push:** Use `vercel ls --format json` to find the preview URL. If the CLI isn't authenticated, tell the user to check the Vercel dashboard or commit status checks. -- **CLI deploy:** Show the URL returned by `vercel deploy --no-wait`. Use `vercel inspect ` to check build status and report it to the user. -- **No-auth fallback:** Show both the preview URL and the claim URL: - ``` - Deployment successful! - - Preview URL: https://my-app-abc123.vercel.app - Claim URL: https://vercel.com/claim-deployment?code=... - - View your site at the Preview URL. - To transfer this deployment to your Vercel account, visit the Claim URL. - ``` - -**Do not** curl or fetch the deployed URL to verify it works. Just return the link. - ---- - -## Troubleshooting - -### Escalated Network Access (Codex / sandboxed environments) - -If deployment fails due to network issues (timeouts, DNS errors, connection resets) in a sandboxed environment, rerun with escalated permissions (`sandbox_permissions=require_escalated`). Do not escalate the initial CLI availability check — only escalate the actual deploy command. - -Example guidance to the user: -``` -The deploy needs escalated network access to deploy to Vercel. I can rerun -the command with escalated permissions — want me to proceed? -``` - -### CLI Auth Failure - -If `vercel login` or `vercel deploy` fails with authentication errors, fall back to the Codex no-auth deploy script. diff --git a/plugins/build-web-apps/skills/deploy-to-vercel/agents/openai.yaml b/plugins/build-web-apps/skills/deploy-to-vercel/agents/openai.yaml deleted file mode 100644 index 33256ef2..00000000 --- a/plugins/build-web-apps/skills/deploy-to-vercel/agents/openai.yaml +++ /dev/null @@ -1,3 +0,0 @@ -interface: - display_name: "Deploy to Vercel" - short_description: "Deploy applications and websites to Vercel" diff --git a/plugins/build-web-apps/skills/deploy-to-vercel/resources/deploy-codex.sh b/plugins/build-web-apps/skills/deploy-to-vercel/resources/deploy-codex.sh deleted file mode 100644 index af07d0fd..00000000 --- a/plugins/build-web-apps/skills/deploy-to-vercel/resources/deploy-codex.sh +++ /dev/null @@ -1,301 +0,0 @@ -#!/bin/bash - -# Vercel Deployment Script for Codex (via claimable deploy endpoint) -# Usage: ./deploy-codex.sh [project-path] -# Returns: JSON with previewUrl, claimUrl, deploymentId, projectId - -set -euo pipefail - -DEPLOY_ENDPOINT="https://codex-deploy-skills.vercel.sh/api/deploy" - -# Detect framework from package.json -detect_framework() { - local pkg_json="$1" - - if [ ! -f "$pkg_json" ]; then - echo "null" - return - fi - - local content=$(cat "$pkg_json") - - # Helper to check if a package exists in dependencies or devDependencies. - # Use exact matching by default, with a separate prefix matcher for scoped - # package families like "@remix-run/". - has_dep_exact() { - echo "$content" | grep -q "\"$1\"" - } - - has_dep_prefix() { - echo "$content" | grep -q "\"$1" - } - - # Order matters - check more specific frameworks first - - # Blitz - if has_dep_exact "blitz"; then echo "blitzjs"; return; fi - - # Next.js - if has_dep_exact "next"; then echo "nextjs"; return; fi - - # Gatsby - if has_dep_exact "gatsby"; then echo "gatsby"; return; fi - - # Remix - if has_dep_prefix "@remix-run/"; then echo "remix"; return; fi - - # React Router (v7 framework mode) - if has_dep_prefix "@react-router/"; then echo "react-router"; return; fi - - # TanStack Start - if has_dep_exact "@tanstack/start"; then echo "tanstack-start"; return; fi - - # Astro - if has_dep_exact "astro"; then echo "astro"; return; fi - - # Hydrogen (Shopify) - if has_dep_exact "@shopify/hydrogen"; then echo "hydrogen"; return; fi - - # SvelteKit - if has_dep_exact "@sveltejs/kit"; then echo "sveltekit-1"; return; fi - - # Svelte (standalone) - if has_dep_exact "svelte"; then echo "svelte"; return; fi - - # Nuxt - if has_dep_exact "nuxt"; then echo "nuxtjs"; return; fi - - # Vue with Vitepress - if has_dep_exact "vitepress"; then echo "vitepress"; return; fi - - # Vue with Vuepress - if has_dep_exact "vuepress"; then echo "vuepress"; return; fi - - # Gridsome - if has_dep_exact "gridsome"; then echo "gridsome"; return; fi - - # SolidStart - if has_dep_exact "@solidjs/start"; then echo "solidstart-1"; return; fi - - # Docusaurus - if has_dep_exact "@docusaurus/core"; then echo "docusaurus-2"; return; fi - - # RedwoodJS - if has_dep_prefix "@redwoodjs/"; then echo "redwoodjs"; return; fi - - # Hexo - if has_dep_exact "hexo"; then echo "hexo"; return; fi - - # Eleventy - if has_dep_exact "@11ty/eleventy"; then echo "eleventy"; return; fi - - # Angular / Ionic Angular - if has_dep_exact "@ionic/angular"; then echo "ionic-angular"; return; fi - if has_dep_exact "@angular/core"; then echo "angular"; return; fi - - # Ionic React - if has_dep_exact "@ionic/react"; then echo "ionic-react"; return; fi - - # Create React App - if has_dep_exact "react-scripts"; then echo "create-react-app"; return; fi - - # Ember - if has_dep_exact "ember-cli" || has_dep_exact "ember-source"; then echo "ember"; return; fi - - # Dojo - if has_dep_exact "@dojo/framework"; then echo "dojo"; return; fi - - # Polymer - if has_dep_prefix "@polymer/"; then echo "polymer"; return; fi - - # Preact - if has_dep_exact "preact"; then echo "preact"; return; fi - - # Stencil - if has_dep_exact "@stencil/core"; then echo "stencil"; return; fi - - # UmiJS - if has_dep_exact "umi"; then echo "umijs"; return; fi - - # Sapper (legacy Svelte) - if has_dep_exact "sapper"; then echo "sapper"; return; fi - - # Saber - if has_dep_exact "saber"; then echo "saber"; return; fi - - # Sanity - if has_dep_exact "sanity"; then echo "sanity-v3"; return; fi - if has_dep_prefix "@sanity/"; then echo "sanity"; return; fi - - # Storybook - if has_dep_prefix "@storybook/"; then echo "storybook"; return; fi - - # NestJS - if has_dep_exact "@nestjs/core"; then echo "nestjs"; return; fi - - # Elysia - if has_dep_exact "elysia"; then echo "elysia"; return; fi - - # Hono - if has_dep_exact "hono"; then echo "hono"; return; fi - - # Fastify - if has_dep_exact "fastify"; then echo "fastify"; return; fi - - # h3 - if has_dep_exact "h3"; then echo "h3"; return; fi - - # Nitro - if has_dep_exact "nitropack"; then echo "nitro"; return; fi - - # Express - if has_dep_exact "express"; then echo "express"; return; fi - - # Vite (generic - check last among JS frameworks) - if has_dep_exact "vite"; then echo "vite"; return; fi - - # Parcel - if has_dep_exact "parcel"; then echo "parcel"; return; fi - - # No framework detected - echo "null" -} - -# Parse arguments -INPUT_PATH="${1:-.}" - -# Create temp directory for packaging -TEMP_DIR=$(mktemp -d) -TARBALL="$TEMP_DIR/project.tgz" -STAGING_DIR="$TEMP_DIR/staging" -CLEANUP_TEMP=true - -cleanup() { - if [ "$CLEANUP_TEMP" = true ]; then - rm -rf "$TEMP_DIR" - fi -} -trap cleanup EXIT - -echo "Preparing deployment..." >&2 - -# Check if input is a .tgz file or a directory -FRAMEWORK="null" - -if [ -f "$INPUT_PATH" ] && [[ "$INPUT_PATH" == *.tgz ]]; then - # Input is already a tarball, use it directly - echo "Using provided tarball..." >&2 - TARBALL="$INPUT_PATH" - CLEANUP_TEMP=false - # Can't detect framework from tarball, leave as null -elif [ -d "$INPUT_PATH" ]; then - # Input is a directory, need to tar it - PROJECT_PATH=$(cd "$INPUT_PATH" && pwd) - - # Detect framework from package.json - FRAMEWORK=$(detect_framework "$PROJECT_PATH/package.json") - - # Stage files into a temporary directory to avoid mutating the source tree. - mkdir -p "$STAGING_DIR" - echo "Staging project files..." >&2 - tar -C "$PROJECT_PATH" \ - --exclude='node_modules' \ - --exclude='.git' \ - --exclude='.env' \ - --exclude='.env.*' \ - -cf - . | tar -C "$STAGING_DIR" -xf - - - # Check if this is a static HTML project (no package.json) - if [ ! -f "$PROJECT_PATH/package.json" ]; then - # Find HTML files in root - HTML_FILES=$(find "$STAGING_DIR" -maxdepth 1 -name "*.html" -type f) - HTML_COUNT=$(printf '%s\n' "$HTML_FILES" | sed '/^$/d' | wc -l | tr -d '[:space:]') - - # If there's exactly one HTML file and it's not index.html, rename it - if [ "$HTML_COUNT" -eq 1 ]; then - HTML_FILE=$(echo "$HTML_FILES" | head -1) - BASENAME=$(basename "$HTML_FILE") - if [ "$BASENAME" != "index.html" ]; then - echo "Renaming $BASENAME to index.html..." >&2 - mv "$HTML_FILE" "$STAGING_DIR/index.html" - fi - fi - fi - - # Create tarball from the staging directory - echo "Creating deployment package..." >&2 - tar -czf "$TARBALL" -C "$STAGING_DIR" . -else - echo "Error: Input must be a directory or a .tgz file" >&2 - exit 1 -fi - -if [ "$FRAMEWORK" != "null" ]; then - echo "Detected framework: $FRAMEWORK" >&2 -fi - -# Deploy -echo "Deploying..." >&2 -RESPONSE=$(curl -s -X POST "$DEPLOY_ENDPOINT" -F "file=@$TARBALL" -F "framework=$FRAMEWORK") - -# Check for error in response -if echo "$RESPONSE" | grep -q '"error"'; then - ERROR_MSG=$(echo "$RESPONSE" | grep -o '"error":"[^"]*"' | cut -d'"' -f4) - echo "Error: $ERROR_MSG" >&2 - exit 1 -fi - -# Extract URLs from response -PREVIEW_URL=$(echo "$RESPONSE" | grep -o '"previewUrl":"[^"]*"' | cut -d'"' -f4) -CLAIM_URL=$(echo "$RESPONSE" | grep -o '"claimUrl":"[^"]*"' | cut -d'"' -f4) - -if [ -z "$PREVIEW_URL" ]; then - echo "Error: Could not extract preview URL from response" >&2 - echo "$RESPONSE" >&2 - exit 1 -fi - -echo "Deployment started. Waiting for build to complete..." >&2 -echo "Preview URL: $PREVIEW_URL" >&2 - -# Poll the preview URL until it returns a non-5xx response (5xx = still building) -MAX_ATTEMPTS=60 # 5 minutes max (60 * 5 seconds) -ATTEMPT=0 - -while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do - HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$PREVIEW_URL") - - if [ "$HTTP_STATUS" -eq 200 ]; then - echo "" >&2 - echo "Deployment ready!" >&2 - break - elif [ "$HTTP_STATUS" -ge 500 ]; then - # 5xx means still building/deploying - echo "Building... (attempt $((ATTEMPT + 1))/$MAX_ATTEMPTS)" >&2 - sleep 5 - ATTEMPT=$((ATTEMPT + 1)) - elif [ "$HTTP_STATUS" -ge 400 ] && [ "$HTTP_STATUS" -lt 500 ]; then - # 4xx might be an error or the app itself returns 4xx - it's responding - echo "" >&2 - echo "Deployment ready (returned $HTTP_STATUS)!" >&2 - break - else - # Any other status, assume it's ready - echo "" >&2 - echo "Deployment ready!" >&2 - break - fi -done - -if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then - echo "" >&2 - echo "Warning: Timed out waiting for deployment, but it may still be building." >&2 -fi - -echo "" >&2 -echo "Preview URL: $PREVIEW_URL" >&2 -echo "Claim URL: $CLAIM_URL" >&2 -echo "" >&2 - -# Output JSON for programmatic use -echo "$RESPONSE" diff --git a/plugins/build-web-apps/skills/frontend-app-builder/SKILL.md b/plugins/build-web-apps/skills/frontend-app-builder/SKILL.md new file mode 100644 index 00000000..bf4a100c --- /dev/null +++ b/plugins/build-web-apps/skills/frontend-app-builder/SKILL.md @@ -0,0 +1,41 @@ +--- +name: frontend-app-builder +description: Use when building frontend applications with generated visual assets and browser testing. +--- + +# Frontend App Builder + +Use this skill to turn a frontend application request into a working, visually checked app. Design or source the needed visual assets with built-in Codex image generation, implement those designs in code, then verify the result with the Browser plugin and built-in app browser unless the user asks not to. + +## Workflow + +1. Read the existing app structure, scripts, styling system, and asset locations before editing. +2. Define the minimum visual direction and asset list needed for the screen or flow. +3. Prefer built-in Codex image generation for missing raster assets such as hero imagery, product scenes, illustrations, textures, mockups, icons, thumbnails, and empty-state art. +4. Implement the design in the app using the repo's existing framework, routing, component, styling, and asset conventions. +5. Use the Browser plugin / built-in app browser for frontend testing unless the user explicitly asks not to use it. +6. Fix issues found during browser testing, then repeat the browser check for the changed surfaces. + +## Asset Design + +- Use existing brand or product assets when the user has provided them; otherwise use built-in Codex image generation instead of placeholder gradients, generic SVG decoration, or empty gray boxes. +- Prompt generated assets with concrete subject, style, composition, aspect ratio, background needs, and intended UI placement. +- Keep UI text, labels, numbers, and controls in code rather than baked into generated images. +- Store generated or edited assets in the project's normal public/static asset location and reference them through the app's existing asset pipeline. +- Prefer assets that reveal the actual product, use case, state, or atmosphere the interface needs to communicate. + +## Implementation + +- Build the real usable surface first, not a marketing wrapper around a future app. +- Match existing conventions for components, tokens, spacing, routing, state, loading, errors, and empty states. +- Keep layouts responsive with stable dimensions for images, toolbars, grids, cards, and controls so generated assets do not cause shifting or overlap. +- Make the generated assets serve the interface: crop, mask, size, and lazy-load them intentionally instead of dropping them in at arbitrary dimensions. +- Supplement implementation with type checks, linting, and unit tests when the repo already uses them. + +## Browser Testing + +- Use the Browser plugin and built-in app browser to open the local app, inspect screenshots, and interact with the main workflow unless the user asks not to use it. +- Check at least one desktop viewport and one mobile-sized viewport when the UI is user-facing. +- Confirm generated assets load, are framed correctly, and do not obscure text or controls. +- Verify primary actions, navigation, hover/focus states, responsive wrapping, and obvious loading or error states. +- If the built-in app browser is unavailable, state that clearly and use the closest available visual/browser fallback. diff --git a/plugins/build-web-apps/skills/frontend-app-builder/agents/openai.yaml b/plugins/build-web-apps/skills/frontend-app-builder/agents/openai.yaml new file mode 100644 index 00000000..82b13adc --- /dev/null +++ b/plugins/build-web-apps/skills/frontend-app-builder/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Frontend App Builder" + short_description: "Build frontend apps with generated assets and browser testing" + default_prompt: "Build a frontend app with generated visual assets and browser verification." diff --git a/plugins/build-web-apps/skills/frontend-skill/SKILL.md b/plugins/build-web-apps/skills/frontend-skill/SKILL.md deleted file mode 100644 index 0ced5cf4..00000000 --- a/plugins/build-web-apps/skills/frontend-skill/SKILL.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -name: frontend-skill -description: Use when the task asks for a visually strong landing page, website, app, prototype, demo, or game UI. This skill enforces restrained composition, image-led hierarchy, cohesive content structure, and tasteful motion while avoiding generic cards, weak branding, and UI clutter. ---- - -# Frontend Skill - -Use this skill when the quality of the work depends on art direction, hierarchy, restraint, imagery, and motion rather than component count. - -Goal: ship interfaces that feel deliberate, premium, and current. Default toward award-level composition: one big idea, strong imagery, sparse copy, rigorous spacing, and a small number of memorable motions. - -## Working Model - -Before building, write three things: - -- visual thesis: one sentence describing mood, material, and energy -- content plan: hero, support, detail, final CTA -- interaction thesis: 2-3 motion ideas that change the feel of the page - -Each section gets one job, one dominant visual idea, and one primary takeaway or action. - -## Beautiful Defaults - -- Start with composition, not components. -- Prefer a full-bleed hero or full-canvas visual anchor. -- Make the brand or product name the loudest text. -- Keep copy short enough to scan in seconds. -- Use whitespace, alignment, scale, cropping, and contrast before adding chrome. -- Limit the system: two typefaces max, one accent color by default. -- Default to cardless layouts. Use sections, columns, dividers, lists, and media blocks instead. -- Treat the first viewport as a poster, not a document. - -## Landing Pages - -Default sequence: - -1. Hero: brand or product, promise, CTA, and one dominant visual -2. Support: one concrete feature, offer, or proof point -3. Detail: atmosphere, workflow, product depth, or story -4. Final CTA: convert, start, visit, or contact - -Hero rules: - -- One composition only. -- Full-bleed image or dominant visual plane. -- Canonical full-bleed rule: on branded landing pages, the hero itself must run edge-to-edge with no inherited page gutters, framed container, or shared max-width; constrain only the inner text/action column. -- Brand first, headline second, body third, CTA fourth. -- No hero cards, stat strips, logo clouds, pill soup, or floating dashboards by default. -- Keep headlines to roughly 2-3 lines on desktop and readable in one glance on mobile. -- Keep the text column narrow and anchored to a calm area of the image. -- All text over imagery must maintain strong contrast and clear tap targets. - -If the first viewport still works after removing the image, the image is too weak. If the brand disappears after hiding the nav, the hierarchy is too weak. - -Viewport budget: - -- If the first screen includes a sticky/fixed header, that header counts against the hero. The combined header + hero content must fit within the initial viewport at common desktop and mobile sizes. -- When using `100vh`/`100svh` heroes, subtract persistent UI chrome (`calc(100svh - header-height)`) or overlay the header instead of stacking it in normal flow. - -## Apps - -Default to Linear-style restraint: - -- calm surface hierarchy -- strong typography and spacing -- few colors -- dense but readable information -- minimal chrome -- cards only when the card is the interaction - -For app UI, organize around: - -- primary workspace -- navigation -- secondary context or inspector -- one clear accent for action or state - -Avoid: - -- dashboard-card mosaics -- thick borders on every region -- decorative gradients behind routine product UI -- multiple competing accent colors -- ornamental icons that do not improve scanning - -If a panel can become plain layout without losing meaning, remove the card treatment. - -## Imagery - -Imagery must do narrative work. - -- Use at least one strong, real-looking image for brands, venues, editorial pages, and lifestyle products. -- Prefer in-situ photography over abstract gradients or fake 3D objects. -- Choose or crop images with a stable tonal area for text. -- Do not use images with embedded signage, logos, or typographic clutter fighting the UI. -- Do not generate images with built-in UI frames, splits, cards, or panels. -- If multiple moments are needed, use multiple images, not one collage. - -The first viewport needs a real visual anchor. Decorative texture is not enough. - -## Copy - -- Write in product language, not design commentary. -- Let the headline carry the meaning. -- Supporting copy should usually be one short sentence. -- Cut repetition between sections. -- Do not include prompt language or design commentary into the UI. -- Give every section one responsibility: explain, prove, deepen, or convert. - -If deleting 30 percent of the copy improves the page, keep deleting. - -## Utility Copy For Product UI - -When the work is a dashboard, app surface, admin tool, or operational workspace, default to utility copy over marketing copy. - -- Prioritize orientation, status, and action over promise, mood, or brand voice. -- Start with the working surface itself: KPIs, charts, filters, tables, status, or task context. Do not introduce a hero section unless the user explicitly asks for one. -- Section headings should say what the area is or what the user can do there. -- Good: "Selected KPIs", "Plan status", "Search metrics", "Top segments", "Last sync". -- Avoid aspirational hero lines, metaphors, campaign-style language, and executive-summary banners on product surfaces unless specifically requested. -- Supporting text should explain scope, behavior, freshness, or decision value in one sentence. -- If a sentence could appear in a homepage hero or ad, rewrite it until it sounds like product UI. -- If a section does not help someone operate, monitor, or decide, remove it. -- Litmus check: if an operator scans only headings, labels, and numbers, can they understand the page immediately? - -## Motion - -Use motion to create presence and hierarchy, not noise. - -Ship at least 2-3 intentional motions for visually led work: - -- one entrance sequence in the hero -- one scroll-linked, sticky, or depth effect -- one hover, reveal, or layout transition that sharpens affordance - -Prefer Framer Motion when available for: - -- section reveals -- shared layout transitions -- scroll-linked opacity, translate, or scale shifts -- sticky storytelling -- carousels that advance narrative, not just fill space -- menus, drawers, and modal presence effects - -Motion rules: - -- noticeable in a quick recording -- smooth on mobile -- fast and restrained -- consistent across the page -- removed if ornamental only - -## Hard Rules - -- No cards by default. -- No hero cards by default. -- No boxed or center-column hero when the brief calls for full bleed. -- No more than one dominant idea per section. -- No section should need many tiny UI devices to explain itself. -- No headline should overpower the brand on branded pages. -- No filler copy. -- No split-screen hero unless text sits on a calm, unified side. -- No more than two typefaces without a clear reason. -- No more than one accent color unless the product already has a strong system. - -## Reject These Failures - -- Generic SaaS card grid as the first impression -- Beautiful image with weak brand presence -- Strong headline with no clear action -- Busy imagery behind text -- Sections that repeat the same mood statement -- Carousel with no narrative purpose -- App UI made of stacked cards instead of layout - -## Litmus Checks - -- Is the brand or product unmistakable in the first screen? -- Is there one strong visual anchor? -- Can the page be understood by scanning headlines only? -- Does each section have one job? -- Are cards actually necessary? -- Does motion improve hierarchy or atmosphere? -- Would the design still feel premium if all decorative shadows were removed? diff --git a/plugins/build-web-apps/skills/frontend-skill/agents/openai.yaml b/plugins/build-web-apps/skills/frontend-skill/agents/openai.yaml deleted file mode 100644 index ddafd948..00000000 --- a/plugins/build-web-apps/skills/frontend-skill/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Frontend Skill" - short_description: "Design visually strong landing pages, websites, apps, and UI" - default_prompt: "Use $frontend-skill to establish a visual thesis, content plan, and interaction thesis before building a visually strong landing page, website, app, prototype, demo, or game UI." diff --git a/plugins/build-web-apps/skills/react-best-practices/AGENTS.md b/plugins/build-web-apps/skills/react-best-practices/AGENTS.md deleted file mode 100755 index a194a618..00000000 --- a/plugins/build-web-apps/skills/react-best-practices/AGENTS.md +++ /dev/null @@ -1,3373 +0,0 @@ -# React Best Practices - -**Version 1.0.0** -Vercel Engineering -January 2026 - -> **Note:** -> This document is mainly for agents and LLMs to follow when maintaining, -> generating, or refactoring React and Next.js codebases. Humans -> may also find it useful, but guidance here is optimized for automation -> and consistency by AI-assisted workflows. - ---- - -## 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. - ---- - -## Table of Contents - -1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL** - - 1.1 [Defer Await Until Needed](#11-defer-await-until-needed) - - 1.2 [Dependency-Based Parallelization](#12-dependency-based-parallelization) - - 1.3 [Prevent Waterfall Chains in API Routes](#13-prevent-waterfall-chains-in-api-routes) - - 1.4 [Promise.all() for Independent Operations](#14-promiseall-for-independent-operations) - - 1.5 [Strategic Suspense Boundaries](#15-strategic-suspense-boundaries) -2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL** - - 2.1 [Avoid Barrel File Imports](#21-avoid-barrel-file-imports) - - 2.2 [Conditional Module Loading](#22-conditional-module-loading) - - 2.3 [Defer Non-Critical Third-Party Libraries](#23-defer-non-critical-third-party-libraries) - - 2.4 [Dynamic Imports for Heavy Components](#24-dynamic-imports-for-heavy-components) - - 2.5 [Preload Based on User Intent](#25-preload-based-on-user-intent) -3. [Server-Side Performance](#3-server-side-performance) — **HIGH** - - 3.1 [Authenticate Server Actions Like API Routes](#31-authenticate-server-actions-like-api-routes) - - 3.2 [Avoid Duplicate Serialization in RSC Props](#32-avoid-duplicate-serialization-in-rsc-props) - - 3.3 [Cross-Request LRU Caching](#33-cross-request-lru-caching) - - 3.4 [Hoist Static I/O to Module Level](#34-hoist-static-io-to-module-level) - - 3.5 [Minimize Serialization at RSC Boundaries](#35-minimize-serialization-at-rsc-boundaries) - - 3.6 [Parallel Data Fetching with Component Composition](#36-parallel-data-fetching-with-component-composition) - - 3.7 [Per-Request Deduplication with React.cache()](#37-per-request-deduplication-with-reactcache) - - 3.8 [Use after() for Non-Blocking Operations](#38-use-after-for-non-blocking-operations) -4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH** - - 4.1 [Deduplicate Global Event Listeners](#41-deduplicate-global-event-listeners) - - 4.2 [Use Passive Event Listeners for Scrolling Performance](#42-use-passive-event-listeners-for-scrolling-performance) - - 4.3 [Use SWR for Automatic Deduplication](#43-use-swr-for-automatic-deduplication) - - 4.4 [Version and Minimize localStorage Data](#44-version-and-minimize-localstorage-data) -5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM** - - 5.1 [Calculate Derived State During Rendering](#51-calculate-derived-state-during-rendering) - - 5.2 [Defer State Reads to Usage Point](#52-defer-state-reads-to-usage-point) - - 5.3 [Do not wrap a simple expression with a primitive result type in useMemo](#53-do-not-wrap-a-simple-expression-with-a-primitive-result-type-in-usememo) - - 5.4 [Don't Define Components Inside Components](#54-dont-define-components-inside-components) - - 5.5 [Extract Default Non-primitive Parameter Value from Memoized Component to Constant](#55-extract-default-non-primitive-parameter-value-from-memoized-component-to-constant) - - 5.6 [Extract to Memoized Components](#56-extract-to-memoized-components) - - 5.7 [Narrow Effect Dependencies](#57-narrow-effect-dependencies) - - 5.8 [Put Interaction Logic in Event Handlers](#58-put-interaction-logic-in-event-handlers) - - 5.9 [Split Combined Hook Computations](#59-split-combined-hook-computations) - - 5.10 [Subscribe to Derived State](#510-subscribe-to-derived-state) - - 5.11 [Use Functional setState Updates](#511-use-functional-setstate-updates) - - 5.12 [Use Lazy State Initialization](#512-use-lazy-state-initialization) - - 5.13 [Use Transitions for Non-Urgent Updates](#513-use-transitions-for-non-urgent-updates) - - 5.14 [Use useDeferredValue for Expensive Derived Renders](#514-use-usedeferredvalue-for-expensive-derived-renders) - - 5.15 [Use useRef for Transient Values](#515-use-useref-for-transient-values) -6. [Rendering Performance](#6-rendering-performance) — **MEDIUM** - - 6.1 [Animate SVG Wrapper Instead of SVG Element](#61-animate-svg-wrapper-instead-of-svg-element) - - 6.2 [CSS content-visibility for Long Lists](#62-css-content-visibility-for-long-lists) - - 6.3 [Hoist Static JSX Elements](#63-hoist-static-jsx-elements) - - 6.4 [Optimize SVG Precision](#64-optimize-svg-precision) - - 6.5 [Prevent Hydration Mismatch Without Flickering](#65-prevent-hydration-mismatch-without-flickering) - - 6.6 [Suppress Expected Hydration Mismatches](#66-suppress-expected-hydration-mismatches) - - 6.7 [Use Activity Component for Show/Hide](#67-use-activity-component-for-showhide) - - 6.8 [Use defer or async on Script Tags](#68-use-defer-or-async-on-script-tags) - - 6.9 [Use Explicit Conditional Rendering](#69-use-explicit-conditional-rendering) - - 6.10 [Use React DOM Resource Hints](#610-use-react-dom-resource-hints) - - 6.11 [Use useTransition Over Manual Loading States](#611-use-usetransition-over-manual-loading-states) -7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM** - - 7.1 [Avoid Layout Thrashing](#71-avoid-layout-thrashing) - - 7.2 [Build Index Maps for Repeated Lookups](#72-build-index-maps-for-repeated-lookups) - - 7.3 [Cache Property Access in Loops](#73-cache-property-access-in-loops) - - 7.4 [Cache Repeated Function Calls](#74-cache-repeated-function-calls) - - 7.5 [Cache Storage API Calls](#75-cache-storage-api-calls) - - 7.6 [Combine Multiple Array Iterations](#76-combine-multiple-array-iterations) - - 7.7 [Early Length Check for Array Comparisons](#77-early-length-check-for-array-comparisons) - - 7.8 [Early Return from Functions](#78-early-return-from-functions) - - 7.9 [Hoist RegExp Creation](#79-hoist-regexp-creation) - - 7.10 [Use flatMap to Map and Filter in One Pass](#710-use-flatmap-to-map-and-filter-in-one-pass) - - 7.11 [Use Loop for Min/Max Instead of Sort](#711-use-loop-for-minmax-instead-of-sort) - - 7.12 [Use Set/Map for O(1) Lookups](#712-use-setmap-for-o1-lookups) - - 7.13 [Use toSorted() Instead of sort() for Immutability](#713-use-tosorted-instead-of-sort-for-immutability) -8. [Advanced Patterns](#8-advanced-patterns) — **LOW** - - 8.1 [Initialize App Once, Not Per Mount](#81-initialize-app-once-not-per-mount) - - 8.2 [Store Event Handlers in Refs](#82-store-event-handlers-in-refs) - - 8.3 [useEffectEvent for Stable Callback Refs](#83-useeffectevent-for-stable-callback-refs) - ---- - -## 1. Eliminating Waterfalls - -**Impact: CRITICAL** - -Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains. - -### 1.1 Defer Await Until Needed - -**Impact: HIGH (avoids blocking unused code paths)** - -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. - -### 1.2 Dependency-Based Parallelization - -**Impact: CRITICAL (2-10× improvement)** - -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:** - -```typescript -const userPromise = fetchUser() -const profilePromise = userPromise.then(user => fetchProfile(user.id)) - -const [user, config, profile] = await Promise.all([ - userPromise, - fetchConfig(), - profilePromise -]) -``` - -We can also create all the promises first, and do `Promise.all()` at the end. - -Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all) - -### 1.3 Prevent Waterfall Chains in API Routes - -**Impact: CRITICAL (2-10× improvement)** - -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). - -### 1.4 Promise.all() for Independent Operations - -**Impact: CRITICAL (2-10× improvement)** - -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() -]) -``` - -### 1.5 Strategic Suspense Boundaries - -**Impact: HIGH (faster initial paint)** - -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 ( -
-
Sidebar
-
Header
-
- -
-
Footer
-
- ) -} -``` - -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 ( -
-
Sidebar
-
Header
-
- }> - - -
-
Footer
-
- ) -} - -async function DataDisplay() { - const data = await fetchData() // Only blocks this component - return
{data.content}
-} -``` - -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 ( -
-
Sidebar
-
Header
- }> - - - -
Footer
-
- ) -} - -function DataDisplay({ dataPromise }: { dataPromise: Promise }) { - const data = use(dataPromise) // Unwraps the promise - return
{data.content}
-} - -function DataSummary({ dataPromise }: { dataPromise: Promise }) { - const data = use(dataPromise) // Reuses the same promise - return
{data.summary}
-} -``` - -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. - ---- - -## 2. Bundle Size Optimization - -**Impact: CRITICAL** - -Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint. - -### 2.1 Avoid Barrel File Imports - -**Impact: CRITICAL (200-800ms import cost, slow builds)** - -Import directly from source files instead of barrel files 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: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js) - -### 2.2 Conditional Module Loading - -**Impact: HIGH (loads large data only when needed)** - -Load large data or modules only when a feature is activated. - -**Example: lazy-load animation frames** - -```tsx -function AnimationPlayer({ enabled, setEnabled }: { enabled: boolean; setEnabled: React.Dispatch> }) { - const [frames, setFrames] = useState(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 - return -} -``` - -The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed. - -### 2.3 Defer Non-Critical Third-Party Libraries - -**Impact: MEDIUM (loads after hydration)** - -Analytics, logging, and error tracking don't block user interaction. Load them after hydration. - -**Incorrect: blocks initial bundle** - -```tsx -import { Analytics } from '@vercel/analytics/react' - -export default function RootLayout({ children }) { - return ( - - - {children} - - - - ) -} -``` - -**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 ( - - - {children} - - - - ) -} -``` - -### 2.4 Dynamic Imports for Heavy Components - -**Impact: CRITICAL (directly affects TTI and LCP)** - -Use `next/dynamic` to lazy-load large components not needed on initial render. - -**Incorrect: Monaco bundles with main chunk ~300KB** - -```tsx -import { MonacoEditor } from './monaco-editor' - -function CodePanel({ code }: { code: string }) { - return -} -``` - -**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 -} -``` - -### 2.5 Preload Based on User Intent - -**Impact: MEDIUM (reduces perceived latency)** - -Preload heavy bundles before they're needed to reduce perceived latency. - -**Example: preload on hover/focus** - -```tsx -function EditorButton({ onClick }: { onClick: () => void }) { - const preload = () => { - if (typeof window !== 'undefined') { - void import('./monaco-editor') - } - } - - return ( - - ) -} -``` - -**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 - {children} - -} -``` - -The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed. - ---- - -## 3. Server-Side Performance - -**Impact: HIGH** - -Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. - -### 3.1 Authenticate Server Actions Like API Routes - -**Impact: CRITICAL (prevents unauthorized access to server mutations)** - -Server Actions (functions with `"use server"`) are exposed as public endpoints, just like API routes. Always verify authentication and authorization **inside** each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly. - -Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation." - -**Incorrect: no authentication check** - -```typescript -'use server' - -export async function deleteUser(userId: string) { - // Anyone can call this! No auth check - await db.user.delete({ where: { id: userId } }) - return { success: true } -} -``` - -**Correct: authentication inside the action** - -```typescript -'use server' - -import { verifySession } from '@/lib/auth' -import { unauthorized } from '@/lib/errors' - -export async function deleteUser(userId: string) { - // Always check auth inside the action - const session = await verifySession() - - if (!session) { - throw unauthorized('Must be logged in') - } - - // Check authorization too - if (session.user.role !== 'admin' && session.user.id !== userId) { - throw unauthorized('Cannot delete other users') - } - - await db.user.delete({ where: { id: userId } }) - return { success: true } -} -``` - -**With input validation:** - -```typescript -'use server' - -import { verifySession } from '@/lib/auth' -import { z } from 'zod' - -const updateProfileSchema = z.object({ - userId: z.string().uuid(), - name: z.string().min(1).max(100), - email: z.string().email() -}) - -export async function updateProfile(data: unknown) { - // Validate input first - const validated = updateProfileSchema.parse(data) - - // Then authenticate - const session = await verifySession() - if (!session) { - throw new Error('Unauthorized') - } - - // Then authorize - if (session.user.id !== validated.userId) { - throw new Error('Can only update own profile') - } - - // Finally perform the mutation - await db.user.update({ - where: { id: validated.userId }, - data: { - name: validated.name, - email: validated.email - } - }) - - return { success: true } -} -``` - -Reference: [https://nextjs.org/docs/app/guides/authentication](https://nextjs.org/docs/app/guides/authentication) - -### 3.2 Avoid Duplicate Serialization in RSC Props - -**Impact: LOW (reduces network payload by avoiding duplicate serialization)** - -RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (`.toSorted()`, `.filter()`, `.map()`) in client, not server. - -**Incorrect: duplicates array** - -```tsx -// RSC: sends 6 strings (2 arrays × 3 items) - -``` - -**Correct: sends 3 strings** - -```tsx -// RSC: send once - - -// Client: transform there -'use client' -const sorted = useMemo(() => [...usernames].sort(), [usernames]) -``` - -**Nested deduplication behavior:** - -```tsx -// string[] - duplicates everything -usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings - -// object[] - duplicates array structure only -users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4) -``` - -Deduplication works recursively. Impact varies by data type: - -- `string[]`, `number[]`, `boolean[]`: **HIGH impact** - array + all primitives fully duplicated - -- `object[]`: **LOW impact** - array duplicated, but nested objects deduplicated by reference - -**Operations breaking deduplication: create new references** - -- Arrays: `.toSorted()`, `.filter()`, `.map()`, `.slice()`, `[...arr]` - -- Objects: `{...obj}`, `Object.assign()`, `structuredClone()`, `JSON.parse(JSON.stringify())` - -**More examples:** - -```tsx -// ❌ Bad - u.active)} /> - - -// ✅ Good - - -// Do filtering/destructuring in client -``` - -**Exception:** Pass derived data when transformation is expensive or client doesn't need original. - -### 3.3 Cross-Request LRU Caching - -**Impact: HIGH (caches across requests)** - -`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({ - 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) - -### 3.4 Hoist Static I/O to Module Level - -**Impact: HIGH (avoids repeated file/network I/O per request)** - -When loading static assets (fonts, logos, images, config files) in route handlers or server functions, hoist the I/O operation to module level. Module-level code runs once when the module is first imported, not on every request. This eliminates redundant file system reads or network fetches that would otherwise run on every invocation. - -**Incorrect: reads font file on every request** - -**Correct: loads once at module initialization** - -**Alternative: synchronous file reads with Node.js fs** - -**General Node.js example: loading config or templates** - -**When to use this pattern:** - -- Loading fonts for OG image generation - -- Loading static logos, icons, or watermarks - -- Reading configuration files that don't change at runtime - -- Loading email templates or other static templates - -- Any static asset that's the same across all requests - -**When NOT to use this pattern:** - -- Assets that vary per request or user - -- Files that may change during runtime (use caching with TTL instead) - -- Large files that would consume too much memory if kept loaded - -- Sensitive data that shouldn't persist in memory - -**With Vercel's [Fluid Compute](https://vercel.com/docs/fluid-compute):** Module-level caching is especially effective because multiple concurrent requests share the same function instance. The static assets stay loaded in memory across requests without cold start penalties. - -**In traditional serverless:** Each cold start re-executes module-level code, but subsequent warm invocations reuse the loaded assets until the instance is recycled. - -### 3.5 Minimize Serialization at RSC Boundaries - -**Impact: HIGH (reduces data transfer size)** - -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 -} - -'use client' -function Profile({ user }: { user: User }) { - return
{user.name}
// uses 1 field -} -``` - -**Correct: serializes only 1 field** - -```tsx -async function Page() { - const user = await fetchUser() - return -} - -'use client' -function Profile({ name }: { name: string }) { - return
{name}
-} -``` - -### 3.6 Parallel Data Fetching with Component Composition - -**Impact: CRITICAL (eliminates server-side waterfalls)** - -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 ( -
-
{header}
- -
- ) -} - -async function Sidebar() { - const items = await fetchSidebarItems() - return -} -``` - -**Correct: both fetch simultaneously** - -```tsx -async function Header() { - const data = await fetchHeader() - return
{data}
-} - -async function Sidebar() { - const items = await fetchSidebarItems() - return -} - -export default function Page() { - return ( -
-
- -
- ) -} -``` - -**Alternative with children prop:** - -```tsx -async function Header() { - const data = await fetchHeader() - return
{data}
-} - -async function Sidebar() { - const items = await fetchSidebarItems() - return -} - -function Layout({ children }: { children: ReactNode }) { - return ( -
-
- {children} -
- ) -} - -export default function Page() { - return ( - - - - ) -} -``` - -### 3.7 Per-Request Deduplication with React.cache() - -**Impact: MEDIUM (deduplicates within request)** - -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 params = { uid: 1 } -getUser(params) // Query runs -getUser(params) // Cache hit (same reference) -``` - -If you must pass objects, pass the 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: [https://react.dev/reference/react/cache](https://react.dev/reference/react/cache) - -### 3.8 Use after() for Non-Blocking Operations - -**Impact: MEDIUM (faster response times)** - -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) - ---- - -## 4. Client-Side Data Fetching - -**Impact: MEDIUM-HIGH** - -Automatic deduplication and efficient data fetching patterns reduce redundant network requests. - -### 4.1 Deduplicate Global Event Listeners - -**Impact: LOW (single listener for N components)** - -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 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', () => { /* ... */ }) - // ... -} -``` - -### 4.2 Use Passive Event Listeners for Scrolling Performance - -**Impact: MEDIUM (eliminates scroll delay caused by event listeners)** - -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()`. - -### 4.3 Use SWR for Automatic Deduplication - -**Impact: MEDIUM-HIGH (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 -} -``` - -Reference: [https://swr.vercel.app](https://swr.vercel.app) - -### 4.4 Version and Minimize localStorage Data - -**Impact: MEDIUM (prevents schema conflicts, reduces storage size)** - -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. - ---- - -## 5. Re-render Optimization - -**Impact: MEDIUM** - -Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness. - -### 5.1 Calculate Derived State During Rendering - -**Impact: MEDIUM (avoids redundant renders and state drift)** - -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

{fullName}

-} -``` - -**Correct: derive during render** - -```tsx -function Form() { - const [firstName, setFirstName] = useState('First') - const [lastName, setLastName] = useState('Last') - const fullName = firstName + ' ' + lastName - - return

{fullName}

-} -``` - -Reference: [https://react.dev/learn/you-might-not-need-an-effect](https://react.dev/learn/you-might-not-need-an-effect) - -### 5.2 Defer State Reads to Usage Point - -**Impact: MEDIUM (avoids unnecessary subscriptions)** - -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 -} -``` - -**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 -} -``` - -### 5.3 Do not wrap a simple expression with a primitive result type in useMemo - -**Impact: LOW-MEDIUM (wasted computation on every render)** - -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 - // return some markup -} -``` - -**Correct:** - -```tsx -function Header({ user, notifications }: Props) { - const isLoading = user.isLoading || notifications.isLoading - - if (isLoading) return - // return some markup -} -``` - -### 5.4 Don't Define Components Inside Components - -**Impact: HIGH (prevents remount on every render)** - -Defining a component inside another component creates a new component type on every render. React sees a different component each time and fully remounts it, destroying all state and DOM. - -A common reason developers do this is to access parent variables without passing props. Always pass props instead. - -**Incorrect: remounts on every render** - -```tsx -function UserProfile({ user, theme }) { - // Defined inside to access `theme` - BAD - const Avatar = () => ( - - ) - - // Defined inside to access `user` - BAD - const Stats = () => ( -
- {user.followers} followers - {user.posts} posts -
- ) - - return ( -
- - -
- ) -} -``` - -Every time `UserProfile` renders, `Avatar` and `Stats` are new component types. React unmounts the old instances and mounts new ones, losing any internal state, running effects again, and recreating DOM nodes. - -**Correct: pass props instead** - -```tsx -function Avatar({ src, theme }: { src: string; theme: string }) { - return ( - - ) -} - -function Stats({ followers, posts }: { followers: number; posts: number }) { - return ( -
- {followers} followers - {posts} posts -
- ) -} - -function UserProfile({ user, theme }) { - return ( -
- - -
- ) -} -``` - -**Symptoms of this bug:** - -- Input fields lose focus on every keystroke - -- Animations restart unexpectedly - -- `useEffect` cleanup/setup runs on every parent render - -- Scroll position resets inside the component - -### 5.5 Extract Default Non-primitive Parameter Value from Memoized Component to Constant - -**Impact: MEDIUM (restores memoization by using a constant for default value)** - -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 - -``` - -**Correct: stable default value** - -```tsx -const NOOP = () => {}; - -const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) { - // ... -}) - -// Used without optional onClick - -``` - -### 5.6 Extract to Memoized Components - -**Impact: MEDIUM (enables early returns)** - -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 - }, [user]) - - if (loading) return - return
{avatar}
-} -``` - -**Correct: skips computation when loading** - -```tsx -const UserAvatar = memo(function UserAvatar({ user }: { user: User }) { - const id = useMemo(() => computeAvatarId(user), [user]) - return -}) - -function Profile({ user, loading }: Props) { - if (loading) return - return ( -
- -
- ) -} -``` - -**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. - -### 5.7 Narrow Effect Dependencies - -**Impact: LOW (minimizes effect re-runs)** - -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]) -``` - -### 5.8 Put Interaction Logic in Event Handlers - -**Impact: MEDIUM (avoids effect re-runs and duplicate side effects)** - -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 -} -``` - -**Correct: do it in the handler** - -```tsx -function Form() { - const theme = useContext(ThemeContext) - - function handleSubmit() { - post('/api/register') - showToast('Registered', theme) - } - - return -} -``` - -Reference: [https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler](https://react.dev/learn/removing-effect-dependencies#should-this-code-move-to-an-event-handler) - -### 5.9 Split Combined Hook Computations - -**Impact: MEDIUM (avoids recomputing independent steps)** - -When a hook contains multiple independent tasks with different dependencies, split them into separate hooks. A combined hook reruns all tasks when any dependency changes, even if some tasks don't use the changed value. - -**Incorrect: changing `sortOrder` recomputes filtering** - -```tsx -const sortedProducts = useMemo(() => { - const filtered = products.filter((p) => p.category === category) - const sorted = filtered.toSorted((a, b) => - sortOrder === "asc" ? a.price - b.price : b.price - a.price - ) - return sorted -}, [products, category, sortOrder]) -``` - -**Correct: filtering only recomputes when products or category change** - -```tsx -const filteredProducts = useMemo( - () => products.filter((p) => p.category === category), - [products, category] -) - -const sortedProducts = useMemo( - () => - filteredProducts.toSorted((a, b) => - sortOrder === "asc" ? a.price - b.price : b.price - a.price - ), - [filteredProducts, sortOrder] -) -``` - -This pattern also applies to `useEffect` when combining unrelated side effects: - -**Incorrect: both effects run when either dependency changes** - -```tsx -useEffect(() => { - analytics.trackPageView(pathname) - document.title = `${pageTitle} | My App` -}, [pathname, pageTitle]) -``` - -**Correct: effects run independently** - -```tsx -useEffect(() => { - analytics.trackPageView(pathname) -}, [pathname]) - -useEffect(() => { - document.title = `${pageTitle} | My App` -}, [pageTitle]) -``` - -**Note:** If your project has [React Compiler](https://react.dev/learn/react-compiler) enabled, it automatically optimizes dependency tracking and may handle some of these cases for you. - -### 5.10 Subscribe to Derived State - -**Impact: MEDIUM (reduces re-render frequency)** - -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