Performance Optimization
Note
This guide covers best practices and techniques to optimize your Vitto site for maximum performance.
Overview
A fast website improves user experience, SEO rankings, and conversion rates. Vitto is built on Vite, which provides excellent performance out of the box, but there are additional optimizations you can apply.
Build Optimization
Enable Minification
Enable HTML minification in production:
// vite.config.ts
import { defineConfig } from 'vite';
import vitto from 'vitto';
export default defineConfig({
plugins: [
vitto({
minify: process.env.NODE_ENV === 'production',
minifyOptions: {
collapseWhitespaces: 'conservative',
removeComments: true,
minifyCss: { lib: 'lightningcss' },
minifyJs: true,
},
}),
],
}); Warning
Avoid premature optimization. Focus on measurable performance issues rather than micro-optimizations that add complexity without real impact.
Tree Shaking
Vite automatically removes unused code. Import only what you need:
// Good - imports only what's needed
import { debounce } from 'lodash-es';
// Avoid - imports entire library
import _ from 'lodash'; Code Splitting
Vite automatically splits code. For manual control:
// Lazy load heavy components
const HeavyComponent = () => import('./HeavyComponent.js'); Asset Optimization
Images
1. Use Appropriate Formats
- WebP: Modern format with great compression
- AVIF: Even better compression, growing browser support
- JPEG: Photos and complex images
- PNG: Images requiring transparency
- SVG: Icons and simple graphics
2. Optimize Images
Use tools like:
# Install sharp for image processing
npm install sharp // hooks/images.ts
import sharp from 'sharp';
export default defineHooks('optimizedImages', async () => {
const images = await getImages();
await Promise.all(
images.map(async (img) => {
await sharp(img.path)
.resize(1200, 800, { fit: 'inside' })
.webp({ quality: 80 })
.toFile(img.outputPath);
})
);
return images;
}); 3. Responsive Images
<picture>
<source
srcset="/images/hero-large.webp"
media="(min-width: 1024px)"
type="image/webp"
>
<source
srcset="/images/hero-medium.webp"
media="(min-width: 640px)"
type="image/webp"
>
<img
src="/images/hero-small.jpg"
alt="{{ title }}"
loading="lazy"
width="800"
height="600"
>
</picture> 4. Lazy Loading
<img
src="/images/photo.jpg"
alt="{{ alt }}"
loading="lazy"
width="800"
height="600"
> CSS
1. Remove Unused CSS
Use PurgeCSS with Tailwind CSS:
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{vto,html,js,ts}'],
// ...
}; 2. Critical CSS
Inline critical CSS in <head>:
<head>
<style>
/* Critical above-the-fold styles */
body { margin: 0; font-family: sans-serif; }
.header { background: #000; color: #fff; }
</style>
{{ renderAssets() |> safe }}
</head> 3. CSS Optimization
Vite automatically optimizes CSS. For more control:
// vite.config.ts
export default defineConfig({
css: {
devSourcemap: false,
preprocessorOptions: {
scss: {
additionalData: '@import "./src/styles/variables.scss";',
},
},
},
}); JavaScript
1. Minimize Third-Party Scripts
Only include essential scripts:
{{# Load analytics only in production #}}
{{ if !isDev }}
<script defer src="https://analytics.example.com/script.js"></script>
{{ /if }} 2. Defer Non-Critical Scripts
<script defer src="/scripts/analytics.js"></script>
<script async src="/scripts/ads.js"></script> 3. Use Modern JavaScript
Vite builds for modern browsers by default:
// vite.config.ts
export default defineConfig({
build: {
target: 'esnext',
modulePreload: {
polyfill: false,
},
},
}); Content Optimization
Reduce Page Weight
1. Optimize HTML
vitto({
minify: true,
minifyOptions: {
removeComments: true,
collapseWhitespaces: 'conservative',
},
}); 2. Limit Data in Templates
Only pass necessary data to templates:
export default defineHooks('posts', async () => {
const posts = await getAllPosts();
// Only return fields needed for display
return posts.map((post) => ({
slug: post.slug,
title: post.title,
excerpt: post.excerpt,
date: post.date,
// Don't include full content in list view
}));
}); 3. Paginate Long Lists
const POSTS_PER_PAGE = 10;
export default defineHooks('posts', async () => {
const allPosts = await getAllPosts();
const allItems = allPosts.map((post) => ({
slug: post.slug,
title: post.title,
excerpt: post.excerpt,
date: post.date,
}));
// Return all items; plugin slices per page via paginate()
return allItems;
}); Configure dynamicRoutes in vite.config.ts:
vitto({
hooks: { posts: postsHook },
dynamicRoutes: [
{
template: 'blog',
dataSource: 'posts',
pageSize: 10,
getParams: (pageNum) => ({ _page: pageNum }),
getPath: (pageNum) => (pageNum === 1 ? 'blog.html' : `blog/${pageNum}.html`),
},
],
}); Font Optimization
1. Self-Host Fonts
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
} 2. Use font-display
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* or 'optional' */
} 3. Subset Fonts
Only include characters you need:
# Using glyphhanger
npx glyphhanger --subset=font.ttf --formats=woff2 --css Caching Strategy
Static Assets
Configure cache headers in your hosting platform:
# Long cache for hashed assets
/assets/*
Cache-Control: max-age=31536000, immutable
# Short cache for HTML
/*.html
Cache-Control: max-age=0, must-revalidate, public Service Worker
Implement a service worker for offline support:
// public/sw.js
const CACHE_NAME = 'vitto-v1';
const urlsToCache = ['/', '/styles.css', '/main.js'];
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache)));
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => response || fetch(event.request))
);
}); Register in your template:
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
}
</script> Network Optimization
Preconnect to Required Origins
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="https://api.example.com">
</head> Prefetch Next Pages
{{# Prefetch important pages #}}
<link rel="prefetch" href="/about.html">
<link rel="prefetch" href="/blog.html"> Resource Hints
<head>
{{# Preload critical resources #}}
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
{{# Preconnect to external domains #}}
<link rel="preconnect" href="https://analytics.example.com">
{{# DNS prefetch for third-party resources #}}
<link rel="dns-prefetch" href="https://cdn.example.com">
</head> Search Index Optimization
Reduce Index Size
vitto({
pagefindOptions: {
rootSelector: 'main', // Index only main content
excludeSelectors: ['nav', 'footer', '.sidebar'],
},
}); Exclude Unnecessary Pages
{{# Don't index error pages #}}
<div data-pagefind-ignore>
<h1>404 Not Found</h1>
</div> Monitoring Performance
Core Web Vitals
Monitor key metrics:
- LCP (Largest Contentful Paint): < 2.5s
- FID (First Input Delay): < 100ms
- CLS (Cumulative Layout Shift): < 0.1
Tools
Use these tools to measure performance:
- Lighthouse (Chrome DevTools)
- PageSpeed Insights
- WebPageTest
- Chrome User Experience Report
Implement Performance Monitoring
<script>
// Web Vitals
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'
function sendToAnalytics(metric) {
console.log(metric)
// Send to your analytics
}
getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getFCP(sendToAnalytics)
getLCP(sendToAnalytics)
getTTFB(sendToAnalytics)
</script> Tip
Set up performance monitoring early and track Core Web Vitals in production to catch regressions before they impact users.
Build Performance
Faster Builds
Note
Build performance matters for CI/CD pipelines. Optimize hooks and caching to keep builds fast.
1. Use npm ci
# Instead of npm install
npm ci 2. Cache Dependencies
In GitHub Actions:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' 3. Parallel Processing
// Process data in parallel
export default defineHooks('posts', async () => {
const files = await getMarkdownFiles();
// Process in parallel
const posts = await Promise.all(
files.map(async (file) => {
return await processMarkdown(file);
})
);
return posts;
}); 4. Cache Hook Results
let cachedData = null;
export default defineHooks('data', async () => {
if (cachedData) return cachedData;
cachedData = await fetchExpensiveData();
return cachedData;
}); Best Practices Checklist
Performance Budget
Set performance budgets:
// vite.config.ts
export default defineConfig({
build: {
reportCompressedSize: true,
chunkSizeWarningLimit: 500, // KB
rolldownOptions: {
output: {
manualChunks: {
vendor: ['heavy-library'],
},
},
},
},
}); Next Steps
- Examples - Real-world performance examples
- Troubleshooting - Common performance issues
- API Reference - Complete API documentation