Echo Template Caching
Introduction
Template caching is an essential performance optimization technique in web applications. When your application renders templates frequently, especially those with complex layouts or calculations, caching the rendered output can significantly reduce server load and improve response times.
In this guide, we'll explore how to implement and manage template caching in Echo templates. We'll cover different caching strategies, when to use them, and how to maintain cache consistency when your data changes.
What is Template Caching?
Template caching stores the rendered HTML output of a template in memory or a persistent store (like Redis), allowing your application to serve the cached version instead of re-rendering the template for each request. This approach can dramatically improve performance, especially for templates that:
- Have complex layouts
- Include many partials/components
- Require heavy data processing before rendering
- Are requested frequently with the same parameters
Basic Template Caching
Let's start with a simple example of implementing template caching in an Echo application:
package main
import (
"github.com/labstack/echo/v4"
"time"
"sync"
)
// Simple in-memory cache
type TemplateCache struct {
cache map[string]CacheEntry
mu sync.RWMutex
}
type CacheEntry struct {
content string
expiration time.Time
}
func NewTemplateCache() *TemplateCache {
return &TemplateCache{
cache: make(map[string]CacheEntry),
}
}
func (tc *TemplateCache) Set(key string, content string, duration time.Duration) {
tc.mu.Lock()
defer tc.mu.Unlock()
tc.cache[key] = CacheEntry{
content: content,
expiration: time.Now().Add(duration),
}
}
func (tc *TemplateCache) Get(key string) (string, bool) {
tc.mu.RLock()
defer tc.mu.RUnlock()
entry, found := tc.cache[key]
if !found {
return "", false
}
if time.Now().After(entry.expiration) {
delete(tc.cache, key)
return "", false
}
return entry.content, true
}
func main() {
e := echo.New()
templateCache := NewTemplateCache()
e.GET("/product/:id", func(c echo.Context) error {
productID := c.Param("id")
cacheKey := "product_" + productID
// Try to get from cache first
if content, found := templateCache.Get(cacheKey); found {
return c.HTML(200, content)
}
// Cache miss - generate content
product := fetchProductData(productID) // Imagine this is your data access function
// Render template
renderedHTML := renderProductTemplate(product) // Your template rendering function
// Store in cache for 5 minutes
templateCache.Set(cacheKey, renderedHTML, 5*time.Minute)
return c.HTML(200, renderedHTML)
})
e.Start(":8080")
}
// Mock functions for example
func fetchProductData(id string) map[string]interface{} {
// In a real app, this would fetch from a database
return map[string]interface{}{
"id": id,
"name": "Sample Product",
"price": 19.99,
}
}
func renderProductTemplate(data map[string]interface{}) string {
// In a real app, this would use your template engine
return "<h1>" + data["name"].(string) + "</h1><p>Price: $" +
fmt.Sprintf("%.2f", data["price"].(float64)) + "</p>"
}
Cache Key Strategies
Effective caching depends on proper cache key design. Here are some strategies for creating cache keys:
Route-Based Keys
cacheKey := "route_" + c.Request().URL.Path
This approach works well for pages that don't vary based on user state or query parameters.
Parameter-Based Keys
cacheKey := fmt.Sprintf("product_%s_page_%s_sort_%s",
c.Param("id"),
c.QueryParam("page"),
c.QueryParam("sort")
)
This is useful when the content changes based on route parameters or query strings.
User-Based Keys
cacheKey := fmt.Sprintf("user_%s_dashboard", userID)
Use this approach when content varies by user but the template structure remains the same.
Controlling Cache Duration
Different types of content require different caching strategies:
Static Content
Content that rarely changes (e.g., "About Us" pages) can be cached for longer periods:
templateCache.Set(cacheKey, renderedHTML, 24*time.Hour)
Dynamic Content
For frequently updated content, use shorter cache durations:
templateCache.Set(cacheKey, renderedHTML, 5*time.Minute)
Real-time Content
Some content should not be cached at all or cached very briefly:
templateCache.Set(cacheKey, renderedHTML, 30*time.Second)
Advanced Caching Techniques
Partial Template Caching
Instead of caching entire pages, you can cache just the expensive parts:
func renderPageWithPartialCache(c echo.Context) error {
// Get header from cache
headerHTML, headerFound := templateCache.Get("common_header")
if !headerFound {
headerHTML = renderHeader()
templateCache.Set("common_header", headerHTML, 1*time.Hour)
}
// Get footer from cache
footerHTML, footerFound := templateCache.Get("common_footer")
if !footerFound {
footerHTML = renderFooter()
templateCache.Set("common_footer", footerHTML, 1*time.Hour)
}
// Generate the dynamic content that shouldn't be cached
contentHTML := renderPageContent(c.Param("id"))
// Combine all parts
fullPage := headerHTML + contentHTML + footerHTML
return c.HTML(200, fullPage)
}
Cache Invalidation
Cache invalidation is the process of removing or updating cached content when the source data changes:
func updateProduct(c echo.Context) error {
productID := c.Param("id")
// Update product in database
err := saveProductChanges(productID, c.FormParams())
if err != nil {
return c.JSON(400, map[string]string{"error": err.Error()})
}
// Invalidate cache
cacheKey := "product_" + productID
templateCache.mu.Lock()
delete(templateCache.cache, cacheKey)
templateCache.mu.Unlock()
return c.JSON(200, map[string]string{"status": "success"})
}
Using External Cache Stores
For production applications, consider using distributed caches like Redis:
package main
import (
"github.com/labstack/echo/v4"
"github.com/go-redis/redis/v8"
"context"
"time"
)
func main() {
e := echo.New()
// Connect to Redis
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
ctx := context.Background()
e.GET("/product/:id", func(c echo.Context) error {
productID := c.Param("id")
cacheKey := "product_template:" + productID
// Try to get from cache
cachedHTML, err := rdb.Get(ctx, cacheKey).Result()
if err == nil {
// Cache hit
return c.HTML(200, cachedHTML)
}
// Cache miss - generate content
product := fetchProductData(productID)
renderedHTML := renderProductTemplate(product)
// Store in cache for 5 minutes
rdb.Set(ctx, cacheKey, renderedHTML, 5*time.Minute)
return c.HTML(200, renderedHTML)
})
e.Start(":8080")
}
Cache Monitoring and Management
To maintain a healthy caching system, implement monitoring:
func getCacheStats(c echo.Context) error {
tc.mu.RLock()
defer tc.mu.RUnlock()
activeEntries := 0
expiredEntries := 0
now := time.Now()
for _, entry := range tc.cache {
if now.Before(entry.expiration) {
activeEntries++
} else {
expiredEntries++
}
}
stats := map[string]interface{}{
"total_entries": len(tc.cache),
"active_entries": activeEntries,
"expired_entries": expiredEntries,
}
return c.JSON(200, stats)
}
Real-World Application: Blog System
Let's implement a more complete example with a blog system:
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"time"
"sync"
"fmt"
)
// Simplified blog post model
type BlogPost struct {
ID string
Title string
Content string
Author string
Date time.Time
}
// Cache implementation
type TemplateCache struct {
cache map[string]CacheEntry
mu sync.RWMutex
}
type CacheEntry struct {
content string
expiration time.Time
}
func NewTemplateCache() *TemplateCache {
return &TemplateCache{
cache: make(map[string]CacheEntry),
}
}
// Cache methods (same as before)
// ...
func main() {
e := echo.New()
e.Use(middleware.Logger())
cache := NewTemplateCache()
// Mock database
posts := map[string]BlogPost{
"1": {ID: "1", Title: "Getting Started with Echo", Content: "Echo is a web framework...", Author: "John", Date: time.Now().AddDate(0, 0, -3)},
"2": {ID: "2", Title: "Template Caching", Content: "Improve performance with...", Author: "Jane", Date: time.Now().AddDate(0, 0, -1)},
}
// Home page - list of blog posts
e.GET("/", func(c echo.Context) error {
cacheKey := "home_page"
if content, found := cache.Get(cacheKey); found {
return c.HTML(200, content)
}
// Generate HTML for all posts
html := "<h1>Blog Posts</h1><ul>"
for _, post := range posts {
html += fmt.Sprintf("<li><a href='/post/%s'>%s</a> by %s</li>",
post.ID, post.Title, post.Author)
}
html += "</ul>"
// Cache for 1 hour
cache.Set(cacheKey, html, 1*time.Hour)
return c.HTML(200, html)
})
// Individual post page
e.GET("/post/:id", func(c echo.Context) error {
id := c.Param("id")
cacheKey := "post_" + id
if content, found := cache.Get(cacheKey); found {
return c.HTML(200, content)
}
post, exists := posts[id]
if !exists {
return c.HTML(404, "<h1>Post not found</h1>")
}
html := fmt.Sprintf(`
<h1>%s</h1>
<p>By %s on %s</p>
<div>%s</div>
<p><a href="/">Back to home</a></p>
`, post.Title, post.Author, post.Date.Format("Jan 2, 2006"), post.Content)
// Cache for 12 hours
cache.Set(cacheKey, html, 12*time.Hour)
return c.HTML(200, html)
})
// Create/update post - with cache invalidation
e.POST("/admin/post", func(c echo.Context) error {
// Authentication would be here in a real app
id := c.FormValue("id")
title := c.FormValue("title")
content := c.FormValue("content")
author := c.FormValue("author")
// Update the post
posts[id] = BlogPost{
ID: id,
Title: title,
Content: content,
Author: author,
Date: time.Now(),
}
// Invalidate related caches
cache.mu.Lock()
delete(cache.cache, "home_page") // Home page lists all posts
delete(cache.cache, "post_"+id) // The specific post page
cache.mu.Unlock()
return c.Redirect(302, "/post/"+id)
})
e.Start(":8080")
}
Summary
Template caching is a powerful technique that can significantly improve the performance of your Echo applications. Key points to remember:
- Cache rendered templates, not just data, to avoid repeated template parsing and rendering
- Choose appropriate cache keys based on what makes content unique
- Set cache durations according to how frequently content changes
- Implement cache invalidation to ensure users see updated content
- Consider using distributed caches like Redis for production applications
- Monitor cache performance to optimize usage
By implementing template caching properly, you can reduce server load, improve response times, and enhance the overall user experience of your applications.
Additional Resources
Exercises
- Extend the blog example to include a cache for category pages that list posts by category
- Implement a "cache warmup" function that pre-caches frequently accessed pages on application startup
- Create a middleware that automatically caches GET requests based on URL patterns
- Implement a system that uses ETag headers with the template cache to support conditional requests
- Build a dashboard that shows cache hit rates and allows manual invalidation of specific cache keys
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)