// Analyze and optimize application performance including bundle size, render performance, and API response times. Use when investigating slow pages or optimizing production builds.
| name | performance-opt |
| description | Analyze and optimize application performance including bundle size, render performance, and API response times. Use when investigating slow pages or optimizing production builds. |
| allowed-tools | Read, Grep, Glob, Bash |
This skill helps you identify and fix performance bottlenecks across the platform.
Core Web Vitals:
Other Metrics:
# Run Lighthouse
npx lighthouse https://sgcarstrends.com --view
# Or use Chrome DevTools:
# 1. Open DevTools
# 2. Lighthouse tab
# 3. Generate report
# Next.js bundle analyzer
cd apps/web
# Add to next.config.js temporarily
ANALYZE=true pnpm build
# Or use bundle-analyzer package
pnpm add -D @next/bundle-analyzer
# View analysis
open .next/analyze/client.html
1. Dynamic Imports
// โ Static import (loads immediately)
import { HeavyComponent } from "./heavy-component";
export default function Page() {
return <HeavyComponent />;
}
// โ
Dynamic import (lazy load)
import dynamic from "next/dynamic";
const HeavyComponent = dynamic(() => import("./heavy-component"), {
loading: () => <div>Loading...</div>,
ssr: false, // Disable SSR if not needed
});
export default function Page() {
return <HeavyComponent />;
}
2. Tree Shaking
// โ Imports entire library
import _ from "lodash";
const result = _.uniq(array);
// โ
Import only what you need
import uniq from "lodash/uniq";
const result = uniq(array);
// โ
Or use modern alternative
const result = [...new Set(array)];
3. Optimize Dependencies
# Find large dependencies
npx npkill
# Or analyze with bundlephobia
# Visit: https://bundlephobia.com
# Replace large libs with smaller alternatives:
# moment.js (70kb) โ date-fns (13kb) or dayjs (2kb)
# lodash (71kb) โ lodash-es + tree shaking
4. Code Splitting
// Split by route (automatic in Next.js)
// Each page is a separate chunk
// Split by component
const AdminPanel = dynamic(() => import("./admin-panel"));
// Split by condition
const Chart = dynamic(() =>
import(userPreference === "advanced" ? "./advanced-chart" : "./simple-chart")
);
1. useMemo
// โ Recalculates on every render
function Component({ data }) {
const processed = expensiveOperation(data);
return <div>{processed}</div>;
}
// โ
Memoized calculation
function Component({ data }) {
const processed = useMemo(
() => expensiveOperation(data),
[data]
);
return <div>{processed}</div>;
}
2. useCallback
// โ New function on every render
function Parent() {
return <Child onClick={() => console.log("clicked")} />;
}
// โ
Memoized function
function Parent() {
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
return <Child onClick={handleClick} />;
}
3. React.memo
// โ Re-renders even when props unchanged
function ChildComponent({ name }) {
return <div>{name}</div>;
}
// โ
Only re-renders when props change
const ChildComponent = React.memo(function ChildComponent({ name }) {
return <div>{name}</div>;
});
# Install virtualization library
pnpm add -D react-window
import { FixedSizeList } from "react-window";
// โ Renders all items (slow for 1000+ items)
function CarList({ cars }) {
return (
<div>
{cars.map(car => <CarCard key={car.id} car={car} />)}
</div>
);
}
// โ
Only renders visible items
function CarList({ cars }) {
const Row = ({ index, style }) => (
<div style={style}>
<CarCard car={cars[index]} />
</div>
);
return (
<FixedSizeList
height={600}
itemCount={cars.length}
itemSize={100}
width="100%"
>
{Row}
</FixedSizeList>
);
}
import { useDeferredValue, useState } from "react";
function SearchComponent() {
const [input, setInput] = useState("");
const deferredInput = useDeferredValue(input); // Defers update
return (
<>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<SearchResults query={deferredInput} />
</>
);
}
// Add query timing
const start = Date.now();
const result = await db.query.cars.findMany();
const duration = Date.now() - start;
if (duration > 100) {
console.warn(`Slow query: ${duration}ms`);
}
1. Add Indexes
// packages/database/src/db/schema/cars.ts
import { pgTable, text, index } from "drizzle-orm/pg-core";
export const cars = pgTable("cars", {
id: text("id").primaryKey(),
make: text("make").notNull(),
month: text("month").notNull(),
}, (table) => ({
// Add indexes for frequently queried columns
makeIdx: index("cars_make_idx").on(table.make),
monthIdx: index("cars_month_idx").on(table.month),
}));
2. Avoid N+1 Queries
// โ N+1 queries (slow)
const posts = await db.query.posts.findMany();
for (const post of posts) {
post.author = await db.query.users.findFirst({
where: eq(users.id, post.authorId),
});
}
// โ
Single query with join (fast)
const posts = await db.query.posts.findMany({
with: {
author: true,
},
});
3. Select Only Needed Columns
// โ Selects all columns
const users = await db.query.users.findMany();
// โ
Select only what's needed
const users = await db
.select({
id: users.id,
name: users.name,
email: users.email,
})
.from(users);
4. Use Pagination
// โ Loads all records
const cars = await db.query.cars.findMany();
// โ
Paginated query
const cars = await db.query.cars.findMany({
limit: 20,
offset: (page - 1) * 20,
});
5. Batch Queries
// โ Multiple separate queries
const user1 = await db.query.users.findFirst({ where: eq(users.id, "1") });
const user2 = await db.query.users.findFirst({ where: eq(users.id, "2") });
const user3 = await db.query.users.findFirst({ where: eq(users.id, "3") });
// โ
Single batched query
const userIds = ["1", "2", "3"];
const users = await db.query.users.findMany({
where: inArray(users.id, userIds),
});
import { redis } from "@sgcarstrends/utils";
export async function getCarsWithCache(make: string) {
const cacheKey = `cars:${make}`;
// Check cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached as string);
}
// Fetch from database
const cars = await db.query.cars.findMany({
where: eq(cars.make, make),
});
// Cache for 1 hour
await redis.set(cacheKey, JSON.stringify(cars), { ex: 3600 });
return cars;
}
// Cache with revalidation
export const revalidate = 3600; // Revalidate every hour
export async function getData() {
const res = await fetch("https://api.example.com/data", {
next: { revalidate: 3600 },
});
return res.json();
}
// Cache indefinitely, revalidate on demand
export const dynamic = "force-static";
export async function getStaticData() {
// This data is cached until manually revalidated
const data = await db.query.cars.findMany();
return data;
}
import Image from "next/image";
// โ Regular img tag
<img src="/logo.png" alt="Logo" />
// โ
Optimized image
<Image
src="/logo.png"
alt="Logo"
width={200}
height={200}
priority // For above-the-fold images
/>
// โ
Responsive image
<Image
src="/hero.jpg"
alt="Hero"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority
/>
// Use modern formats
<Image
src="/image.webp" // WebP for smaller size
alt="Image"
width={800}
height={600}
/>
// Automatic format optimization with Next.js Image
// Next.js automatically serves WebP/AVIF when supported
// Add response time logging
app.use(async (c, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
c.header("X-Response-Time", `${duration}ms`);
if (duration > 500) {
console.warn(`Slow endpoint: ${c.req.path} (${duration}ms)`);
}
});
// Enable compression in Hono
import { compress } from "hono/compress";
const app = new Hono();
app.use("*", compress());
// Implement cursor-based pagination
export async function getCars(cursor?: string, limit = 20) {
const query = db.query.cars.findMany({
limit: limit + 1, // Fetch one extra to determine if there's more
orderBy: desc(cars.createdAt),
});
if (cursor) {
query.where(lt(cars.id, cursor));
}
const results = await query;
const hasMore = results.length > limit;
const items = hasMore ? results.slice(0, -1) : results;
return {
items,
nextCursor: hasMore ? items[items.length - 1].id : null,
};
}
// infra/api.ts
export function API({ stack, app }: StackContext) {
const api = new Function(stack, "api", {
handler: "apps/api/src/index.handler",
runtime: "nodejs20.x",
architecture: "arm64", // Graviton2 for better performance
memory: 1024, // More memory = faster CPU
nodejs: {
esbuild: {
minify: true, // Smaller bundle = faster cold starts
bundle: true,
},
},
});
}
For production with consistent traffic:
const api = new Function(stack, "api", {
handler: "apps/api/src/index.handler",
reservedConcurrentExecutions: 10, // Reserve instances
});
// app/layout.tsx
import { SpeedInsights } from "@vercel/speed-insights/next";
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<SpeedInsights />
</body>
</html>
);
}
export async function performanceTracked Operation() {
performance.mark("operation-start");
await doSomething();
performance.mark("operation-end");
performance.measure("operation", "operation-start", "operation-end");
const measure = performance.getEntriesByName("operation")[0];
console.log(`Operation took ${measure.duration}ms`);
}
# Install Apache Bench
# or use k6, Artillery, etc.
# Test API endpoint
ab -n 1000 -c 10 https://api.sgcarstrends.com/health
# With k6
k6 run loadtest.js
// __tests__/performance/database.test.ts
import { performance } from "perf_hooks";
describe("Database Performance", () => {
it("queries cars in < 100ms", async () => {
const start = performance.now();
await db.query.cars.findMany({ limit: 100 });
const duration = performance.now() - start;
expect(duration).toBeLessThan(100);
});
});
apps/web/next.config.js - Next.js configuration