Fresh is fast by default, but that doesn't mean you can't screw it up. After optimizing Fresh apps in production for the past year, here's what actually moves the needle - and what's just performance theater that wastes your time.
Fresh 2.0's Speed Improvements (The Numbers That Matter)
Fresh 2.0 beta dropped last week with massive performance gains. These aren't synthetic benchmarks - they're production measurements:
- Boot times: 9-12x faster - Deno.com went from 86ms to 8ms
- Cold starts: 45-60x better than traditional serverless - measured by InfoWorld
- Development HMR: Actually works now instead of full page reloads
- Bundle optimization: Server code bundles for faster file resolution
The catch: You need to migrate to Fresh 2.0 beta. If you're stuck on 1.x, you're missing serious performance gains.
Islands Architecture Performance (What You Need to Know)
Fresh's Islands Architecture is where the real performance magic happens. But only if you understand how to use it properly.
How Islands Actually Work
Unlike React frameworks that hydrate your entire page, Fresh only ships JavaScript for interactive components. Your static content stays HTML - no JavaScript bloat.
Production reality: My app with ~50 routes dropped from ~200ms average response time to 18ms after properly isolating interactive components. The key is understanding what should be an island vs. what should stay static.
Islands Dos and Don'ts
✅ Make These Islands:
- Form components with validation
- Interactive buttons, toggles, modals
- Real-time data (chat, notifications)
- Client-side routing within components
❌ Don't Make These Islands:
- Static content that never changes
- Navigation menus (unless they have dropdowns)
- Footer links and contact info
- Blog post content
Real example from production:
// ❌ BAD - This doesn't need to be an island
// islands/BlogPost.tsx
export default function BlogPost({ content }: { content: string }) {
return <article dangerouslySetInnerHTML={{ __html: content }} />;
}
// ✅ GOOD - Static component in routes/
// components/BlogPost.tsx
export default function BlogPost({ content }: { content: string }) {
return <article dangerouslySetInnerHTML={{ __html: content }} />;
}
The performance impact: Every unnecessary island adds ~3-8KB of JavaScript. Multiply that by 10 pointless islands and you've added 80KB to your bundle. Fresh's whole point is zero JS by default - don't break that.
Fresh-Specific Performance Bottlenecks
Route Loading Performance
Fresh 2.0 introduced on-demand route loading, but you can still fuck it up:
Issue: Large route modules slow down first-page loads
// ❌ BAD - Heavy imports in route file
import { HugeLibrary } from \"npm:massive-library@latest\";
import { AnotherBigThing } from \"./components/huge-component.tsx\";
export default function Index() {
// Route loads 500KB of unnecessary code
}
Fix: Lazy load heavy dependencies
// ✅ GOOD - Import only what you need
export default function Index() {
return (
<div>
{/* Heavy component loads only when needed */}
<LazyHeavyComponent />
</div>
);
}
Static Asset Optimization
Fresh serves static assets from the /static
directory. Common fuckups:
Images: Serving massive unoptimized images
- Problem: 2MB hero images that could be 200KB
- Solution: Use WebP, proper sizing, compression
CSS: Loading entire CSS frameworks for 3 components
- Problem: Importing all of Tailwind for a simple layout
- Solution: Use Fresh's built-in Tailwind integration with proper purging
JavaScript: Client-side libraries that should be server-side
- Problem: Loading moment.js client-side for date formatting
- Solution: Use Deno's native date handling server-side
Database Query Performance
This is where most Fresh performance issues actually happen:
Connection pooling: Deno Deploy doesn't maintain persistent connections like traditional servers. Every request potentially creates a new database connection.
// ❌ BAD - Creates new connection per request
export const handler: Handlers = {
async GET() {
const db = new PostgresClient();
await db.connect(); // Slow connection setup
const data = await db.query(\"SELECT * FROM users\");
return Response.json(data);
}
};
// ✅ BETTER - Use connection pooling
import { Pool } from \"https://deno.land/x/postgres/mod.ts\";
const pool = new Pool({
database: \"mydb\",
hostname: \"localhost\",
port: 5432,
user: \"user\",
password: \"password\",
}, 3, true);
export const handler: Handlers = {
async GET() {
const client = await pool.connect();
const data = await client.queryArray(\"SELECT * FROM users\");
client.release();
return Response.json(data);
}
};
Real production gotcha: Edge functions lose database connections more often than traditional servers. Check your connection string. I spent 3 hours debugging "connection refused" errors that were actually timeout issues.
Memory and Bundle Optimization
npm Package Hell
Fresh 2.0's npm compatibility is great until you import the wrong package:
The problem: npm packages often include Node.js-specific code that bloats your bundle or breaks at runtime.
// ❌ BAD - Imports Node.js-specific code
import fs from \"node:fs\";
import { execSync } from \"node:child_process\";
// ✅ GOOD - Use Deno APIs instead
const content = await Deno.readTextFile(\"./data.txt\");
Bundle analyzer trick: Use deno info
to check what your imports actually pull in:
deno info --json main.ts | grep \"size\"
Preact vs React Optimization
Fresh uses Preact under the hood, which is already optimized. But you can break that:
Issue: Accidentally importing React instead of Preact
// ❌ BAD - Imports full React (45KB)
import React from \"npm:react\";
// ✅ GOOD - Fresh aliases to Preact automatically
import { useState } from \"preact/hooks\";
Fresh 2.0 handles React aliasing automatically, but in 1.x you need proper import maps.
Deployment Platform Performance
Deno Deploy Optimization
Deno Deploy is where Fresh performs best, but there are gotchas:
Cold start performance:
- Fresh 2.0: ~8ms boot time
- Fresh 1.x: ~86ms boot time
- Your takeaway: Upgrade to 2.0 beta for production
Memory limits:
- Free tier: 128MB memory limit
- Paid tier: Up to 512MB
- Real impact: My production app with ~15k requests per day uses ~40MB average
Edge distribution: Deploy to multiple regions for better performance:
## Check your app's edge performance
curl -I https://yourapp.deno.dev/
## Look for x-deno-ray header to see which region served the request
Alternative Platform Performance
Cloudflare Workers: Fresh runs on CF Workers but with limitations:
- 1MB bundle size limit (hits you fast with npm packages)
- 128MB memory limit
- Different cold start characteristics
AWS Lambda: Don't. Deno's Lambda cold starts are better than Node.js, but you lose the edge magic that makes Fresh compelling.
Docker/VPS: Works fine but defeats Fresh's edge-first architecture. You're basically running a Node.js app at that point.
Production Performance Monitoring
What to Actually Monitor
Response times by route: Track which routes are slow
// Add simple timing middleware
export const handler: Handlers = {
async GET(req, ctx) {
const start = performance.now();
const response = await ctx.next();
const duration = performance.now() - start;
// Log slow requests
if (duration > 100) {
console.warn(`Slow request: ${req.url} took ${duration}ms`);
}
return response;
}
};
Database query times: Most performance issues are database-related
Memory usage: Watch for memory leaks in long-running processes
Error rates: Performance problems often show up as errors first
Performance Debugging Tools
Deno's built-in tools:
## Profile memory usage
deno run --v8-flags=--prof app.ts
## Benchmark specific functions
deno bench benchmarks/
Third-party monitoring: Most Fresh apps don't need Sentry. Fix the obvious bugs first:
- Slow database queries (use EXPLAIN)
- Large image files (compress them)
- Too many islands (remove unnecessary ones)
- Memory leaks (check for unclosed connections)
The Bottom Line on Fresh Performance
Fresh is genuinely fast when you don't fight its architecture. The biggest performance wins come from:
- Upgrading to Fresh 2.0 beta - The boot time improvements are real
- Using islands properly - Static content stays static, interactive becomes islands
- Database optimization - Connection pooling and query optimization matter more than framework choice
- Proper deployment - Deno Deploy for edge performance, or accept traditional server trade-offs
Performance tip: Before optimizing, measure. Fresh's performance problems are usually database queries, not framework overhead. Profile first, then optimize the actual bottlenecks.
The framework is designed to be fast by default. If your Fresh app is slow, you're probably fighting the architecture instead of working with it. Usually that means too many islands or shitty database queries.