Vercel React Best Practices — StarMapper
Performance optimization guide for React 19 + Next.js 16.2 on Vercel free tier.
When to Apply
- Writing new React components or Next.js pages
- Implementing data fetching (chunk loop, cache reads)
- Reviewing components for performance issues
- Optimizing bundle size (MapLibre is heavy at ~900KB)
- Refactoring existing code
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 | Re-render Optimization | MEDIUM | rerender- |
| 5 | Rendering Performance | MEDIUM | rendering- |
| 6 | JavaScript Performance | LOW-MEDIUM | js- |
Quick Reference
1. Eliminating Waterfalls (CRITICAL)
async-parallel— UsePromise.all()for independent operations// ✅ parallel: fetch repo info and check cache simultaneously const [repoInfo, cached] = await Promise.all([ fetchRepoInfo(owner, repo), checkStargazerCache(owner, repo), ]);async-api-routes— Start promises early, await late// ✅ start DB lookup early const cachePromise = db.stargazerCache.findUnique({ where: { owner_repo: { owner, repo } } }); // ... other sync work ... const cached = await cachePromise;async-defer-await— Only await in the branch that needs it
2. Bundle Size Optimization (CRITICAL for MapLibre)
bundle-dynamic-imports— MapLibre GL is ~900KB, always dynamic:// ✅ correct — StarMapper uses this pattern import dynamic from "next/dynamic"; const StargazerMapDynamic = dynamic(() => import("./stargazer-map"), { ssr: false });bundle-barrel-imports— Import directly, avoid index.ts barrel files:// ❌ barrel (pulls in everything) import { geocode, geocodeBatch } from "@/lib"; // ✅ direct import import { geocode } from "@/lib/geocoder";bundle-defer-third-party— Load analytics/non-critical scripts after hydrationbundle-conditional— Only load MapLibre when user navigates to map page (Next.js App Router handles this naturally via route segments)
3. Server-Side Performance (HIGH)
server-cache-react— UseReact.cache()for per-request deduplication in Server Components:import { cache } from "react"; const getCachedRepoInfo = cache(async (owner: string, repo: string) => { return fetchRepoInfo(owner, repo); });server-serialization— Minimize data passed to client components:// ❌ pass entire object with 50 fields <StargazerMap repoData={fullRepoData} /> // ✅ only what's needed <StargazerMap owner={owner} repo={repo} stars={stars} />server-parallel-fetching— Parallel Server Component data fetching for map page
4. Re-render Optimization (MEDIUM)
rerender-memo—memo()for StargazerMap (re-renders on every points update):export const StargazerMap = memo(({ points, ... }: Props) => { // ... });rerender-dependencies— Use primitive dependencies in effects:// ❌ object reference changes every render useEffect(() => { ... }, [repoData]); // ✅ primitive values useEffect(() => { ... }, [owner, repo]);rerender-memo— Extract GeoJSON feature array into useMemo:const geojsonData = useMemo(() => ({ type: "FeatureCollection" as const, features: points.map(pointToFeature), }), [points]);rerender-functional-setstate— Use functional setState for chunk loop accumulation:setPoints(prev => [...prev, ...newPoints]);
5. Rendering Performance (MEDIUM)
rendering-hydration-no-flicker— Use inline script for dark/light theme (StarMapper already does this inlayout.tsx):// ✅ inline script prevents FOUC on theme loadrendering-conditional-render— Use ternary, not&&for conditional rendering:// ❌ risky with 0/falsy values {count && <Badge count={count} />} // ✅ explicit {count > 0 ? <Badge count={count} /> : null}
6. JavaScript Performance (LOW-MEDIUM)
js-index-maps— Build Map for repeated lookups (e.g., country name normalization):// Build once, lookup many times const countryMap = new Map(countries.map(c => [c.code, c.name]));js-set-map-lookups— Use Set forhas()checks overincludes():const unmappedSet = new Set(unmapped.map(u => u.login)); const isUnmapped = unmappedSet.has(login); // O(1) vs O(n)js-early-exit— Return early from geocoding functions
StarMapper-Specific Notes
- MapLibre GL bundle (~900KB) is the single biggest bundle concern. Dynamic import with
ssr:falseis mandatory and already in place viastargazer-map-dynamic.tsx. - Chunk loop state accumulates progressively —
setPoints(prev => [...prev, ...newPoints])is correct functional setState pattern. - GeoJSON construction is O(n) with n=stars —
useMemoonpointsarray is important for large repos. - Vercel free = no Edge Runtime needed — standard Node.js serverless functions work fine.