Skip to content

Performance Optimization

This guide covers performance optimization techniques for EreoJS applications.

Bundle Size

Code Splitting

EreoJS automatically code-splits routes:

dist/
├── client/
│   ├── routes/
│   │   ├── index.js      # Only loads on /
│   │   ├── posts.js      # Only loads on /posts
│   │   └── about.js      # Only loads on /about
│   └── shared/
│       └── vendor.js     # Shared dependencies

Dynamic Imports

Lazy load heavy components:

tsx
import { lazy, Suspense } from 'react'

const HeavyChart = lazy(() => import('../components/HeavyChart'))

export default function Dashboard({ loaderData }) {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart data={loaderData.chartData} />
      </Suspense>
    </div>
  )
}

Tree Shaking

Import only what you need:

tsx
// Good - tree shakeable
import { format } from 'date-fns/format'

// Avoid - imports entire library
import { format } from 'date-fns'

Analyze Bundle

bash
bun ereo build --analyze

This opens an interactive visualization of your bundle.

Islands Optimization

Minimize Island Size

tsx
// Keep islands small and focused
// islands/LikeButton.tsx
export default function LikeButton({ postId, initialLikes }) {
  const [likes, setLikes] = useState(initialLikes)
  const like = () => fetch(`/api/like/${postId}`, { method: 'POST' })
    .then(r => r.json())
    .then(d => setLikes(d.likes))

  return <button onClick={like}>❤️ {likes}</button>
}

Defer Non-Critical Islands

tsx
// Hydrate when visible
<Comments
  data-island="Comments"
  data-hydrate="visible"
  postId={post.id}
/>

// Hydrate when idle
<RelatedPosts
  data-island="RelatedPosts"
  data-hydrate="idle"
  posts={related}
/>

Share State Efficiently

tsx
// lib/store.ts - shared between islands
import { signal } from '@ereo/state'

export const cartItems = signal([])
export const cartTotal = computed(
  () => cartItems.get().reduce((sum, i) => sum + i.price, 0),
  [cartItems]
)

// Multiple islands can use the same signals

Data Loading

Parallel Loading

tsx
export const loader = createLoader(async ({ params }) => {
  // Parallel - faster
  const [post, comments, author] = await Promise.all([
    getPost(params.id),
    getCommentsByPost(params.id),
    getUser(params.authorId)
  ])

  return { post, comments, author }
})

Avoid Waterfalls

tsx
// Bad - waterfall
export const loader = createLoader(async ({ params }) => {
  const post = await getPost(params.id)
  const author = await getUser(post.authorId)      // Waits for post
  const comments = await getCommentsByPost(post.id) // Waits for author

  return { post, author, comments }
})

// Good - parallel where possible
export const loader = createLoader(async ({ params }) => {
  const post = await getPost(params.id)

  const [author, comments] = await Promise.all([
    getUser(post.authorId),
    getCommentsByPost(post.id)
  ])

  return { post, author, comments }
})

Use Streaming for Slow Data

tsx
export const config = { render: 'streaming' }

export const loader = createLoader(async ({ params }) => {
  const post = await getPost(params.id)

  return {
    post,
    comments: defer(getCommentsByPost(post.id)),
    recommendations: defer(getRecommendations(post.id))
  }
})

Database Optimization

Use Indexes

sql
-- Index frequently queried columns
CREATE INDEX idx_posts_slug ON posts(slug);
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_created ON posts(created_at DESC);

Efficient Queries

tsx
import { eq } from 'drizzle-orm'

// Bad - N+1 problem
const allPosts = await db.select().from(posts)
for (const post of allPosts) {
  post.author = await db.select().from(users).where(eq(users.id, post.authorId))
}

// Good - single query with join
const postsWithAuthors = await db
  .select()
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))
  .orderBy(desc(posts.createdAt))

Pagination

tsx
import { desc, count } from 'drizzle-orm'

export const loader = createLoader(async ({ request }) => {
  const url = new URL(request.url)
  const page = parseInt(url.searchParams.get('page') || '1')
  const limit = 20

  const [allPosts, [{ total }]] = await Promise.all([
    db.select().from(posts)
      .orderBy(desc(posts.createdAt))
      .limit(limit)
      .offset((page - 1) * limit),
    db.select({ total: count() }).from(posts)
  ])

  return {
    posts: allPosts,
    pagination: {
      page,
      totalPages: Math.ceil(total / limit),
      hasNext: page * limit < total
    }
  }
})

Caching

Cache Expensive Operations

tsx
import { cached } from '@ereo/data'

export const loader = createLoader(async () => {
  const stats = await cached(
    'dashboard-stats',
    async () => {
      // Expensive aggregation
      return await db.query(`
        SELECT
          COUNT(*) as total_posts,
          SUM(views) as total_views,
          AVG(rating) as avg_rating
        FROM posts
      `)
    },
    { maxAge: 300, tags: ['stats'] }
  )

  return { stats }
})

Use Stale-While-Revalidate

tsx
export const config = {
  cache: {
    maxAge: 60,
    staleWhileRevalidate: 3600
  }
}

Image Optimization

Use Next-Gen Formats

tsx
<picture>
  <source srcSet="/image.avif" type="image/avif" />
  <source srcSet="/image.webp" type="image/webp" />
  <img src="/image.jpg" alt="Description" />
</picture>

Lazy Load Images

tsx
<img
  src="/image.jpg"
  alt="Description"
  loading="lazy"
  decoding="async"
/>

Responsive Images

tsx
<img
  src="/image.jpg"
  srcSet="/image-320.jpg 320w, /image-640.jpg 640w, /image-1280.jpg 1280w"
  sizes="(max-width: 320px) 280px, (max-width: 640px) 600px, 1200px"
  alt="Description"
/>

Monitoring

Add Performance Metrics

tsx
// middleware/metrics.ts
export const metricsMiddleware: MiddlewareHandler = async (request, context, next) => {
  const start = performance.now()
  const response = await next()
  const duration = performance.now() - start

  // Log slow requests
  if (duration > 1000) {
    console.warn(`Slow request: ${request.url} took ${duration}ms`)
  }

  // Add Server-Timing header
  response.headers.set('Server-Timing', `total;dur=${duration}`)

  return response
}

Web Vitals

tsx
// Track Core Web Vitals
import { onCLS, onFID, onLCP } from 'web-vitals'

onCLS(console.log)
onFID(console.log)
onLCP(console.log)

Checklist

  • [ ] Run bundle analyzer
  • [ ] Enable code splitting
  • [ ] Use islands for interactivity
  • [ ] Implement data caching
  • [ ] Add database indexes
  • [ ] Use streaming for slow data
  • [ ] Optimize images
  • [ ] Set up monitoring
  • [ ] Test on slow networks
  • [ ] Profile and measure

Released under the MIT License.