32 KiB
Next.js Optimization Guide for United Tattoo
Improving Developer Experience & Site Performance
Last Updated: 2025-11-27 Target Framework: Next.js 14 (App Router) Deployment: Cloudflare Workers via OpenNext
Table of Contents
- Quick Wins (This Week)
- Developer Experience Improvements
- Performance Optimizations
- Code Quality & Maintainability
- Implementation Priority Matrix
- Measuring Success
Quick Wins (This Week)
1.1 Add MDX Support for Content Pages
Problem: Editing content pages requires React boilerplate and components. Solution: Add MDX to write pages in markdown with optional React components.
Implementation:
npm install @next/mdx @mdx-js/loader @mdx-js/react
Create mdx-components.tsx in root:
import type { MDXComponents } from 'mdx/types'
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
h1: ({ children }) => <h1 className="text-4xl font-bold mb-4">{children}</h1>,
h2: ({ children }) => <h2 className="text-3xl font-semibold mb-3">{children}</h2>,
p: ({ children }) => <p className="mb-4 leading-relaxed">{children}</p>,
a: ({ href, children }) => <a href={href} className="text-blue-600 hover:underline">{children}</a>,
...components,
}
}
Update next.config.mjs:
import createMDX from '@next/mdx'
const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [],
rehypePlugins: [],
},
})
export default withMDX({
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
// ... rest of config
})
Convert pages to MDX:
# Example: app/aftercare/page.tsx → app/aftercare/page.mdx
Benefits:
- ✅ Write content in markdown (much faster)
- ✅ Drop in React components when needed
- ✅ No boilerplate for simple pages
- ⏱️ Time saved: 5-10 minutes per content edit
1.2 Create Content Component Library
Problem: Repeating the same UI patterns across pages. Solution: Pre-built, reusable content components.
Create components/content/ directory:
// components/content/Section.tsx
export function Section({
title,
children,
className = ""
}: {
title?: string
children: React.ReactNode
className?: string
}) {
return (
<section className={`py-12 ${className}`}>
{title && <h2 className="text-3xl font-bold mb-6">{title}</h2>}
<div className="prose prose-lg max-w-none">
{children}
</div>
</section>
)
}
// components/content/Hero.tsx
export function Hero({
title,
subtitle,
backgroundImage,
cta
}: HeroProps) {
return (
<div
className="relative h-[600px] flex items-center justify-center"
style={{ backgroundImage: `url(${backgroundImage})` }}
>
<div className="text-center text-white z-10">
<h1 className="text-6xl font-bold mb-4">{title}</h1>
{subtitle && <p className="text-xl mb-8">{subtitle}</p>}
{cta && (
<Button href={cta.href} size="lg">
{cta.text}
</Button>
)}
</div>
<div className="absolute inset-0 bg-black/40" />
</div>
)
}
// components/content/Card.tsx
export function Card({
title,
description,
image,
href
}: CardProps) {
return (
<Link href={href} className="block group">
<div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition-shadow">
{image && (
<img
src={image}
alt={title}
className="w-full h-48 object-cover"
/>
)}
<div className="p-6">
<h3 className="text-xl font-semibold mb-2 group-hover:text-blue-600">
{title}
</h3>
{description && (
<p className="text-gray-600">{description}</p>
)}
</div>
</div>
</Link>
)
}
// Export barrel
// components/content/index.ts
export { Section } from './Section'
export { Hero } from './Hero'
export { Card } from './Card'
export { Grid } from './Grid'
export { CallToAction } from './CallToAction'
Usage in pages:
import { Hero, Section, Grid, Card } from '@/components/content'
export default function ServicesPage() {
return (
<>
<Hero
title="Our Services"
subtitle="Professional tattoo artistry"
backgroundImage="/images/hero-services.jpg"
/>
<Section title="What We Offer">
<Grid columns={3}>
<Card
title="Custom Tattoos"
description="Work with our artists to create your dream piece"
image="/images/custom.jpg"
href="/book"
/>
{/* More cards... */}
</Grid>
</Section>
</>
)
}
Benefits:
- ✅ Consistent UI across site
- ✅ Less code duplication
- ✅ Easier to maintain styles
- ⏱️ Time saved: 15-20 minutes per page creation
1.3 Convert Pages to Server Components
Problem: Unnecessary client-side JavaScript on static pages.
Solution: Remove "use client" from pages that don't need interactivity.
Audit candidates:
# Find all pages with "use client"
grep -r "use client" app/ --include="page.tsx"
Likely candidates for conversion:
app/page.tsx(homepage) - Most sections can be server-renderedapp/artists/page.tsx(artist listing) - Just displays dataapp/aftercare/page.tsx(static content)app/privacy/page.tsx(static content)app/terms/page.tsx(static content)
Before:
"use client"
export default function ArtistsPage() {
const [artists, setArtists] = useState([])
useEffect(() => {
fetch('/api/artists')
.then(r => r.json())
.then(setArtists)
}, [])
return <ArtistGrid artists={artists} />
}
After:
// No "use client" directive - this is a server component
async function getArtists() {
const db = getDB()
return await db.artists.findMany({
where: { isActive: true },
include: { portfolioImages: { take: 6 } }
})
}
export default async function ArtistsPage() {
const artists = await getArtists()
return <ArtistGrid artists={artists} />
}
For interactive parts, create client islands:
// app/artists/page.tsx (server component)
export default async function ArtistsPage() {
const artists = await getArtists()
return (
<div>
<h1>Our Artists</h1>
{/* Client component for filtering only */}
<ArtistFilter />
<ArtistGrid artists={artists} />
</div>
)
}
// components/ArtistFilter.tsx (client component)
"use client"
export function ArtistFilter() {
const [filter, setFilter] = useState('')
// Only the filter is interactive, rest is server-rendered
}
Benefits:
- ✅ Faster initial page load (less JS to download)
- ✅ Better SEO (fully rendered HTML)
- ✅ Reduced hydration time
- 📉 Performance: 30-50% less JavaScript per page
1.4 Add Loading & Error States
Problem: No feedback during data fetching, poor error UX.
Solution: Use Next.js 14's loading.tsx and error.tsx conventions.
Create loading states:
// app/artists/loading.tsx
export default function Loading() {
return (
<div className="container py-12">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="animate-pulse">
<div className="bg-gray-200 h-64 rounded-lg mb-4" />
<div className="bg-gray-200 h-6 w-3/4 rounded mb-2" />
<div className="bg-gray-200 h-4 w-1/2 rounded" />
</div>
))}
</div>
</div>
)
}
// app/artists/[id]/loading.tsx
export default function Loading() {
return <PortfolioSkeleton />
}
Create error boundaries:
// app/artists/error.tsx
"use client"
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="container py-12 text-center">
<h2 className="text-2xl font-bold mb-4">Something went wrong!</h2>
<p className="text-gray-600 mb-6">{error.message}</p>
<button
onClick={reset}
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700"
>
Try again
</button>
</div>
)
}
Benefits:
- ✅ Better user experience during loading
- ✅ Graceful error handling
- ✅ No need to manage loading state manually
- ⏱️ Time saved: 5-10 minutes per page (no manual loading state)
Developer Experience Improvements
2.1 Improve Type Safety
Current issue: Some areas lack proper TypeScript types.
Add Zod schemas for API responses:
// lib/schemas/artist.ts
import { z } from 'zod'
export const artistSchema = z.object({
id: z.string(),
name: z.string(),
slug: z.string(),
bio: z.string().nullable(),
specialties: z.array(z.string()),
instagramHandle: z.string().nullable(),
portfolioImages: z.array(z.object({
id: z.string(),
url: z.string(),
alt: z.string().nullable(),
tags: z.array(z.string()),
})),
})
export type Artist = z.infer<typeof artistSchema>
// Use in API routes for validation
export async function GET() {
const data = await db.artists.findMany()
const validated = z.array(artistSchema).parse(data) // Runtime validation
return Response.json(validated)
}
Generate types from database schema:
npm install drizzle-kit
// scripts/generate-types.ts
import { generateTypes } from 'drizzle-kit'
generateTypes({
schema: './lib/db.ts',
out: './types/database.d.ts'
})
Benefits:
- ✅ Catch errors at compile time
- ✅ Better autocomplete in IDE
- ✅ Runtime validation of API data
- 🐛 Fewer bugs in production
2.2 Add Development Scripts
Create helper scripts for common tasks:
// package.json
{
"scripts": {
"dev:all": "concurrently \"npm run dev\" \"npm run db:studio:local\"",
"dev:debug": "NODE_OPTIONS='--inspect' next dev",
"db:reset:local": "wrangler d1 execute united-tattoo --local --file=./sql/schema.sql && node scripts/seed-local.js",
"db:seed:local": "node scripts/seed-local.js",
"analyze": "ANALYZE=true npm run build",
"type-check:watch": "tsc --noEmit --watch",
"clean": "rm -rf .next .open-next node_modules/.cache",
"fresh": "npm run clean && npm install && npm run dev"
}
}
Create seed script for local development:
// scripts/seed-local.ts
import { getDB } from '@/lib/db'
async function seed() {
const db = getDB()
// Create test user
await db.users.create({
data: {
email: 'test@example.com',
name: 'Test Artist',
role: 'ARTIST',
}
})
// Create test artist
await db.artists.create({
data: {
name: 'Test Artist',
slug: 'test-artist',
bio: 'Test bio',
specialties: ['Realism', 'Color'],
userId: '...',
}
})
console.log('✅ Database seeded!')
}
seed().catch(console.error)
Benefits:
- ✅ Faster development setup
- ✅ Easy database reset/seeding
- ✅ Better debugging capabilities
- ⏱️ Time saved: 10-15 minutes daily on setup tasks
2.3 Improve Error Messages
Add better error handling in database layer:
// lib/db.ts
export const db = {
artists: {
async findById(id: string) {
try {
const db = getDB()
const artist = await db.prepare(
'SELECT * FROM artists WHERE id = ?'
).bind(id).first()
if (!artist) {
throw new Error(`Artist not found: ${id}`)
}
return artist
} catch (error) {
// Add context to errors
throw new Error(
`Failed to fetch artist ${id}: ${error.message}`,
{ cause: error }
)
}
}
}
}
Add request logging:
// middleware.ts
export function middleware(request: NextRequest) {
const start = Date.now()
console.log(`→ ${request.method} ${request.nextUrl.pathname}`)
const response = NextResponse.next()
response.headers.set('X-Response-Time', `${Date.now() - start}ms`)
console.log(
`← ${request.method} ${request.nextUrl.pathname} ` +
`(${Date.now() - start}ms)`
)
return response
}
Benefits:
- ✅ Easier debugging
- ✅ Faster error resolution
- ✅ Better production monitoring
- 🐛 Faster bug fixes
2.4 Create Component Templates
Add templates for common patterns:
# scripts/create-page.sh
#!/bin/bash
PAGE_NAME=$1
ROUTE_PATH=$2
mkdir -p "app/$ROUTE_PATH"
cat > "app/$ROUTE_PATH/page.tsx" <<EOF
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '$PAGE_NAME | United Tattoo',
description: 'Description here',
}
export default async function ${PAGE_NAME}Page() {
return (
<div className="container py-12">
<h1 className="text-4xl font-bold mb-6">$PAGE_NAME</h1>
{/* Content here */}
</div>
)
}
EOF
echo "✅ Created page: app/$ROUTE_PATH/page.tsx"
Usage:
npm run create:page "Services" "services"
VSCode snippets:
// .vscode/snippets.code-snippets
{
"Next.js Page": {
"prefix": "npage",
"body": [
"import { Metadata } from 'next'",
"",
"export const metadata: Metadata = {",
" title: '${1:Page Title} | United Tattoo',",
" description: '${2:Description}',",
"}",
"",
"export default async function ${1}Page() {",
" return (",
" <div className=\"container py-12\">",
" <h1 className=\"text-4xl font-bold mb-6\">${1}</h1>",
" $0",
" </div>",
" )",
"}"
]
},
"API Route": {
"prefix": "napi",
"body": [
"import { NextRequest } from 'next/server'",
"import { getServerSession } from 'next-auth'",
"import { authOptions } from '@/lib/auth'",
"",
"export async function GET(request: NextRequest) {",
" const session = await getServerSession(authOptions)",
" ",
" if (!session) {",
" return Response.json({ error: 'Unauthorized' }, { status: 401 })",
" }",
" ",
" // Logic here",
" $0",
" ",
" return Response.json({ data: null })",
"}"
]
}
}
Benefits:
- ✅ Consistent code structure
- ✅ Faster file creation
- ✅ Less boilerplate typing
- ⏱️ Time saved: 2-5 minutes per file
Performance Optimizations
3.1 Optimize Images
Current issue: Some images not using Next.js Image component.
Replace <img> with <Image>:
// Before
<img src="/images/hero.jpg" alt="Hero" />
// After
import Image from 'next/image'
<Image
src="/images/hero.jpg"
alt="Hero"
width={1920}
height={1080}
priority // For above-the-fold images
placeholder="blur"
blurDataURL="data:image/..." // Optional blur-up effect
/>
For portfolio images from R2:
// components/PortfolioImage.tsx
import Image from 'next/image'
export function PortfolioImage({ image }: { image: PortfolioImage }) {
return (
<Image
src={image.url}
alt={image.alt || image.title}
width={800}
height={600}
className="rounded-lg shadow-md"
loading="lazy" // Lazy load below fold
quality={85} // Reduce quality slightly for smaller files
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
)
}
Generate blur placeholders:
npm install plaiceholder
// lib/blur-image.ts
import { getPlaiceholder } from 'plaiceholder'
export async function getBlurDataURL(src: string) {
try {
const buffer = await fetch(src).then(r => r.arrayBuffer())
const { base64 } = await getPlaiceholder(Buffer.from(buffer))
return base64
} catch {
return undefined
}
}
Benefits:
- 📉 30-50% smaller image sizes
- ✅ Automatic WebP/AVIF conversion
- ✅ Responsive images
- ✅ Better CLS (Cumulative Layout Shift)
3.2 Add Route-Level Caching
Add caching headers to static content:
// app/artists/page.tsx
export const revalidate = 3600 // Revalidate every hour
export default async function ArtistsPage() {
const artists = await getArtists()
return <ArtistGrid artists={artists} />
}
// app/artists/[id]/page.tsx
export const revalidate = 1800 // 30 minutes
export async function generateStaticParams() {
const artists = await getArtists()
return artists.map(a => ({ id: a.slug }))
}
Add API route caching:
// app/api/artists/route.ts
export async function GET() {
const artists = await db.artists.findMany()
return Response.json(artists, {
headers: {
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
}
})
}
Add Cloudflare KV caching for expensive operations:
// lib/cache.ts
export async function cached<T>(
key: string,
fn: () => Promise<T>,
ttl: number = 3600
): Promise<T> {
const kv = getKVNamespace()
// Try cache first
const cached = await kv.get(key, 'json')
if (cached) return cached as T
// Execute and cache
const result = await fn()
await kv.put(key, JSON.stringify(result), { expirationTtl: ttl })
return result
}
// Usage
const artists = await cached('artists:all', () => db.artists.findMany(), 3600)
Benefits:
- 📉 50-90% faster repeat visits
- ✅ Reduced database load
- ✅ Better scalability
- 💰 Lower hosting costs
3.3 Optimize Fonts
Use next/font with local fonts:
// app/layout.tsx
import { Inter, Playfair_Display } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap', // Prevent FOIT (Flash of Invisible Text)
})
const playfair = Playfair_Display({
subsets: ['latin'],
variable: '--font-playfair',
display: 'swap',
})
export default function RootLayout({ children }) {
return (
<html className={`${inter.variable} ${playfair.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}
Update Tailwind config:
// tailwind.config.ts
export default {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)', 'sans-serif'],
serif: ['var(--font-playfair)', 'serif'],
},
},
},
}
Benefits:
- 📉 No layout shift from font loading
- ✅ Automatic font subsetting
- ✅ Preloading and optimization
- ⚡ Faster perceived load time
3.4 Code Splitting & Lazy Loading
Lazy load heavy components:
// Before - BookingForm loaded immediately
import { BookingForm } from '@/components/BookingForm'
export default function BookPage() {
return <BookingForm />
}
// After - BookingForm loaded on demand
import dynamic from 'next/dynamic'
const BookingForm = dynamic(
() => import('@/components/BookingForm'),
{
loading: () => <LoadingSpinner />,
ssr: false // Client-only if needed
}
)
export default function BookPage() {
return <BookingForm />
}
Lazy load admin dashboard components:
// app/admin/page.tsx
import dynamic from 'next/dynamic'
const AnalyticsDashboard = dynamic(() => import('@/components/admin/AnalyticsDashboard'))
const PortfolioManager = dynamic(() => import('@/components/admin/PortfolioManager'))
const CalendarManager = dynamic(() => import('@/components/admin/CalendarManager'))
export default function AdminPage() {
return (
<div>
<AnalyticsDashboard />
<PortfolioManager />
<CalendarManager />
</div>
)
}
Benefits:
- 📉 40-60% smaller initial bundle
- ✅ Faster time to interactive
- ✅ Better mobile performance
- ⚡ Lighthouse score: +10-20 points
3.5 Optimize Third-Party Scripts
Use next/script for external scripts:
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
{/* Analytics - load after page interactive */}
<Script
src="https://analytics.example.com/script.js"
strategy="afterInteractive"
/>
{/* Non-critical - load lazily */}
<Script
src="https://widget.example.com/chat.js"
strategy="lazyOnload"
/>
</body>
</html>
)
}
Defer non-essential features:
// components/ChatWidget.tsx
"use client"
import { useEffect, useState } from 'react'
export function ChatWidget() {
const [loaded, setLoaded] = useState(false)
useEffect(() => {
// Load chat widget after 5 seconds or user interaction
const timer = setTimeout(() => setLoaded(true), 5000)
const loadOnInteraction = () => {
setLoaded(true)
clearTimeout(timer)
}
window.addEventListener('scroll', loadOnInteraction, { once: true })
window.addEventListener('click', loadOnInteraction, { once: true })
return () => clearTimeout(timer)
}, [])
if (!loaded) return null
return <div id="chat-widget">{/* Widget here */}</div>
}
Benefits:
- 📉 Faster initial page load
- ✅ Better Core Web Vitals
- ✅ Non-blocking script loading
- ⚡ Improved FCP and TTI
3.6 Database Query Optimization
Add indexes to frequently queried columns:
-- sql/migrations/YYYYMMDD_0001_add_indexes.sql
-- Speed up artist lookups by slug
CREATE INDEX IF NOT EXISTS idx_artists_slug ON artists(slug);
-- Speed up portfolio queries
CREATE INDEX IF NOT EXISTS idx_portfolio_artist_id ON portfolio_images(artist_id);
CREATE INDEX IF NOT EXISTS idx_portfolio_sort ON portfolio_images(artist_id, sort_order);
-- Speed up appointment queries
CREATE INDEX IF NOT EXISTS idx_appointments_artist_date ON appointments(artist_id, appointment_date);
CREATE INDEX IF NOT EXISTS idx_appointments_status ON appointments(status);
-- Speed up user lookups
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
Optimize queries with JOINs:
// Before - Multiple queries (N+1 problem)
const artists = await db.artists.findMany()
for (const artist of artists) {
artist.images = await db.portfolioImages.findMany({
where: { artistId: artist.id }
})
}
// After - Single query with JOIN
const artists = await db.prepare(`
SELECT
a.*,
json_group_array(
json_object(
'id', pi.id,
'url', pi.url,
'alt', pi.alt
)
) as images
FROM artists a
LEFT JOIN portfolio_images pi ON pi.artist_id = a.id
GROUP BY a.id
`).all()
Add prepared statement caching:
// lib/db.ts
const statementCache = new Map()
export function getCachedStatement(sql: string) {
if (!statementCache.has(sql)) {
const db = getDB()
statementCache.set(sql, db.prepare(sql))
}
return statementCache.get(sql)
}
// Usage
const stmt = getCachedStatement('SELECT * FROM artists WHERE slug = ?')
const artist = await stmt.bind(slug).first()
Benefits:
- 📉 50-80% faster queries
- ✅ Reduced database load
- ✅ Better scalability
- ⚡ Faster API responses
3.7 Optimize CSS
Use Tailwind's JIT mode (already enabled but optimize):
// tailwind.config.ts
export default {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
// Don't include node_modules unless needed
],
theme: {
extend: {
// Only include custom values you actually use
},
},
plugins: [],
}
Extract critical CSS for above-the-fold:
npm install critical
// next.config.mjs
export default {
experimental: {
optimizeCss: true, // Enable CSS optimization
},
}
Remove unused ShadCN components:
# Audit which components are actually used
grep -r "from '@/components/ui'" app/ components/ --no-filename | \
sed 's/.*from//' | sort | uniq
Benefits:
- 📉 20-40% smaller CSS bundle
- ✅ Faster first paint
- ✅ Better mobile performance
- ⚡ Improved FCP
Code Quality & Maintainability
4.1 Consistent File Structure
Establish clear conventions:
app/
├── (marketing)/ # Public pages
│ ├── page.tsx # Homepage
│ ├── artists/ # Artist pages
│ ├── book/ # Booking
│ └── [slug]/ # Dynamic content pages
├── (dashboard)/ # Protected pages
│ ├── admin/ # Admin dashboard
│ └── artist-dashboard/ # Artist dashboard
└── api/ # API routes
├── artists/
├── appointments/
└── admin/
components/
├── ui/ # ShadCN components
├── content/ # Content components (NEW)
├── admin/ # Admin-specific
└── shared/ # Reusable components
lib/
├── db.ts # Database layer
├── auth.ts # Authentication
├── utils/ # Utilities
│ ├── date.ts
│ ├── format.ts
│ └── validation.ts
└── hooks/ # Custom React hooks
Benefits:
- ✅ Easier to find files
- ✅ Clear separation of concerns
- ✅ Onboarding new developers faster
4.2 Add Documentation
Create component documentation:
// components/content/Hero.tsx
/**
* Hero section component for top of pages
*
* @example
* ```tsx
* <Hero
* title="Welcome"
* subtitle="Your journey starts here"
* backgroundImage="/images/hero.jpg"
* cta={{ text: "Get Started", href: "/book" }}
* />
* ```
*/
export function Hero({
title,
subtitle,
backgroundImage,
cta
}: HeroProps) {
// Implementation
}
Add README files:
<!-- components/content/README.md -->
# Content Components
Pre-built components for quickly creating marketing pages.
## Available Components
### Hero
Full-width hero section with background image and CTA.
**Props:**
- `title` (string) - Main heading
- `subtitle` (string, optional) - Subheading
- `backgroundImage` (string) - Image URL
- `cta` (object, optional) - Call to action button
**Example:**
...
Benefits:
- ✅ Faster onboarding
- ✅ Less time answering questions
- ✅ Consistent component usage
4.3 Add Storybook (Optional)
For visual component development:
npx storybook@latest init
// components/content/Hero.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Hero } from './Hero'
const meta: Meta<typeof Hero> = {
title: 'Content/Hero',
component: Hero,
}
export default meta
type Story = StoryObj<typeof Hero>
export const Default: Story = {
args: {
title: 'Welcome to United Tattoo',
subtitle: 'Professional artistry in Fountain, CO',
backgroundImage: '/images/hero.jpg',
cta: {
text: 'Book Now',
href: '/book',
},
},
}
export const WithoutSubtitle: Story = {
args: {
title: 'Our Services',
backgroundImage: '/images/services.jpg',
},
}
Benefits:
- ✅ Visual component library
- ✅ Test components in isolation
- ✅ Faster iteration on UI
- 🎨 Better design system
Implementation Priority Matrix
Priority 1: Quick Wins (Week 1)
High Impact, Low Effort
- ✅ Add MDX support (1-2 hours)
- ✅ Create content component library (3-4 hours)
- ✅ Convert pages to server components (2-3 hours)
- ✅ Add loading/error states (1-2 hours)
- ✅ Optimize images (2-3 hours)
Total: ~12-16 hours Impact: Major DX improvement + 20-30% performance boost
Priority 2: Performance (Week 2-3)
High Impact, Medium Effort
- ✅ Add route-level caching (3-4 hours)
- ✅ Database indexes (1-2 hours)
- ✅ Code splitting & lazy loading (4-5 hours)
- ✅ Optimize fonts (1-2 hours)
- ✅ Optimize third-party scripts (2-3 hours)
Total: ~12-16 hours Impact: 40-60% performance improvement
Priority 3: Code Quality (Week 3-4)
Medium Impact, Low-Medium Effort
- ✅ Improve type safety (4-5 hours)
- ✅ Add development scripts (2-3 hours)
- ✅ Better error messages (3-4 hours)
- ✅ Component templates (2-3 hours)
- ✅ File structure cleanup (3-4 hours)
Total: ~14-19 hours Impact: Better maintainability, fewer bugs
Priority 4: Documentation (Ongoing)
Lower Impact, Low Effort
- ✅ Component documentation (1-2 hours/week)
- ✅ Add README files (1-2 hours)
- ✅ Storybook setup (4-6 hours, optional)
Total: ~6-10 hours Impact: Easier onboarding, consistent usage
Measuring Success
Developer Experience Metrics
Before:
- ⏱️ Time to create new page: 30-45 minutes
- ⏱️ Time to add content section: 15-20 minutes
- 🐛 Time to debug error: 20-30 minutes
- 📚 Documentation: Minimal
After (Target):
- ⏱️ Time to create new page: 10-15 minutes (50-60% faster)
- ⏱️ Time to add content section: 5-10 minutes (50% faster)
- 🐛 Time to debug error: 10-15 minutes (40% faster)
- 📚 Documentation: Comprehensive
Performance Metrics
Measure with Lighthouse:
# Install Lighthouse CLI
npm install -g lighthouse
# Run audit
lighthouse https://united-tattoos.com --view
Target improvements:
| Metric | Current | Target | Improvement |
|---|---|---|---|
| Performance Score | ~75 | 90+ | +15-20 |
| FCP (First Contentful Paint) | ~2.5s | <1.5s | 40% faster |
| LCP (Largest Contentful Paint) | ~4.0s | <2.5s | 38% faster |
| TTI (Time to Interactive) | ~5.0s | <3.0s | 40% faster |
| CLS (Cumulative Layout Shift) | ~0.15 | <0.1 | 33% better |
| Bundle Size (JS) | ~800KB | <500KB | 38% smaller |
User Experience Metrics
Track with analytics:
- ⏱️ Bounce rate: Target <40% (currently ~50%)
- 📱 Mobile conversion rate: Target +20%
- ⏱️ Average session duration: Target +30%
- 🔄 Repeat visitor rate: Target +25%
Next Steps
Week 1: Quick Wins
- Monday: Add MDX support + convert 3 static pages
- Tuesday: Create content component library
- Wednesday: Convert 5 pages to server components
- Thursday: Add loading/error states across app
- Friday: Optimize all images
Week 2: Performance
- Monday: Add route-level caching
- Tuesday: Database indexes + query optimization
- Wednesday: Code splitting for admin dashboard
- Thursday: Lazy load heavy components
- Friday: Optimize fonts + third-party scripts
Week 3: Code Quality
- Monday: Improve type safety with Zod
- Tuesday: Add development scripts
- Wednesday: Better error messages + logging
- Thursday: Create component templates
- Friday: File structure cleanup
Week 4: Documentation & Polish
- Monday-Wednesday: Add documentation
- Thursday: Storybook setup (optional)
- Friday: Measure results, create performance report
Conclusion
By implementing these optimizations, you'll achieve:
Developer Experience:
- ✅ 50-60% faster page creation
- ✅ Much easier content editing with MDX
- ✅ Less boilerplate and complexity
- ✅ Better debugging and error messages
Performance:
- 📉 40-60% smaller JavaScript bundles
- ⚡ 30-40% faster page loads
- ✅ Better Core Web Vitals
- 💰 Lower hosting costs
Maintainability:
- ✅ Better code organization
- ✅ Comprehensive documentation
- ✅ Fewer bugs in production
- ✅ Easier onboarding
Total Investment: ~40-60 hours over 4 weeks Long-term Savings: ~5-10 hours/week in development time
This positions you well for the future - whether you eventually migrate to Astro or stay with Next.js, you'll have a cleaner, faster, more maintainable codebase.