Solving Next.js Metadata Duplication: Transition from revalidatePath to Optimistic Updates
Summary
This article covers the limitations of server state synchronization in Next.js and how to resolve generateMetadata duplicate execution issues. It explains metadata duplication problems caused by revalidatePath usage and the necessity of prioritizing Optimistic Updates.
Solving Next.js Metadata Duplication: Transition from revalidatePath to Optimistic Updates
Problem Situation
In Next.js applications, whenever user interactions (likes, comments, view count increases) occurred, revalidatePath
was used to invalidate page cache. However, this approach caused metadata duplication issues.
Duplicate Metadata Example
<head>
<title>
SQL Information Processing Engineer Practical Exam Collection | 정처기 감자
</title>
<title>
SQL Information Processing Engineer Practical Exam Collection | 정처기 감자
</title>
<meta name="description" content="..." />
<meta name="description" content="..." />
<!-- Same metadata generated twice -->
</head>
Root Cause Analysis
Previous Flow
- User accesses the page and
generateMetadata
is executed (title, meta description tags are generated) - User performs like/comment/view count action
- Server Action calls
revalidatePath
- Page cache invalidation
- Next request triggers
generateMetadata
re-execution - New metadata duplicates with existing metadata
Core Problem
- Cache invalidation by
revalidatePath
re-executes metadata generation logic - Metadata synchronization issues between server and client
- Metadata processing conflicts between SSR and CSR
Limitations of Server State Synchronization in Next.js
Problems with Server State Synchronization Approach
When using Server State Synchronization in Next.js, you inevitably need to use revalidatePath
. However, this causes the following serious problems:
1. generateMetadata Duplicate Execution Issue
// Server state synchronization approach
export async function toggleLike(slug: string) {
const result = await togglePostLike(slug, userSession);
if (result.success) {
revalidatePath(`/exam-registration/${slug}`); // Cache invalidation
// → generateMetadata re-executes, causing metadata duplication
return { success: true, liked: result.liked };
}
}
2. Performance Overhead
- Unnecessary page re-rendering: Entire page is regenerated
- Metadata recalculation:
generateMetadata
function executes every time - Cache efficiency degradation: Page cache is continuously invalidated
Recommendations for Next.js
In Next.js, you should always prioritize Optimistic Updates over server state synchronization approaches.
Reasons:
- Prevent metadata duplication: Avoid using
revalidatePath
- Performance optimization: Prevent unnecessary page re-rendering
- Enhanced user experience: Immediate UI response
- Cache efficiency: Maintain page cache while synchronizing only data
When server state synchronization is needed:
- When data consistency is extremely important (financial, payment systems)
- When real-time synchronization is essential (collaboration tools, chat)
- When complex business logic exists on the server
However, for general web applications, Optimistic Updates is the better choice.
Solution: Implementing Optimistic Updates
1. Complete Removal of revalidatePath
Before:
// actions.ts
export async function toggleLike(slug: string) {
const result = await togglePostLike(slug, userSession);
if (result.success) {
revalidatePath(`/exam-registration/${slug}`); // Cache invalidation
return { success: true, liked: result.liked };
}
}
After:
// actions.ts
export async function toggleLike(slug: string) {
const result = await togglePostLike(slug, userSession);
if (result.success) {
// Remove revalidatePath - return data only
return { success: true, liked: result.liked };
}
}
2. Client-Side Optimistic Updates Implementation
Like Button
// LikeButtonSupabase.tsx
const handleClick = async () => {
// 1. Optimistic update: immediate UI reflection
const newLikedState = !isLiked;
const newCount = newLikedState ? likeCount + 1 : likeCount - 1;
setIsLiked(newLikedState);
setLikeCount(Math.max(0, newCount));
// 2. Server update
const result = await toggleLike(slug);
if (result.success) {
// 3. Synchronize with server result
setIsLiked(result.liked ?? false);
setLikeCount(result.likeCount ?? 0);
} else {
// 4. Rollback on failure
setIsLiked(!newLikedState);
setLikeCount(likeCount);
}
};
Comment System
// CommentSection.tsx
const handleAddComment = async (data: CommentFormData) => {
// 1. Create temporary comment
const tempId = `temp_${Date.now()}`;
const tempComment: Comment = {
id: tempId,
content: data.content,
author: data.author,
// ...
};
// 2. Optimistic update: immediate UI reflection
setComments((prev) => [...prev, tempComment]);
// 3. Server update
const result = await createComment(slug, data);
if (result.success && result.comment) {
// 4. Replace temporary comment with actual comment on success
setComments((prev) =>
prev.map((comment) =>
comment.id === tempId ? { ...result.comment!, replies: [] } : comment
)
);
} else {
// 5. Remove temporary comment on failure
setComments((prev) => prev.filter((comment) => comment.id !== tempId));
}
};
Soft Delete Handling
// Server: Soft delete
const { error } = await supabase
.from("comments")
.update({ author: null, content: null })
.eq("id", commentId);
// Client: Optimistic soft delete
setComments((prev) =>
prev.map((comment) =>
comment.id === commentId
? { ...comment, author: null, content: null, isDeleted: true }
: comment
)
);
Results and Improvements
✅ Problem Resolution
- Complete elimination of metadata duplication: Removed unnecessary cache invalidation by eliminating
revalidatePath
- Immediate UI response: Instant screen updates without waiting for server response
- Consistent user experience: Fast interactions regardless of network latency
📈 Performance Improvements
- Network request optimization: Data synchronization without page revalidation
- Cache efficiency: Performance improvement by removing unnecessary cache invalidation
- SEO stability: Resolution of SEO issues caused by metadata duplication
🛡️ Stability Assurance
- Rollback mechanism: UI state recovery on server request failure
- Type safety: TypeScript type checking for all state changes
- Error handling: Response to error situations at each step
Key Insights
- Metadata duplication is a cache management issue: Inappropriate use of
revalidatePath
was the cause - Power of Optimistic Updates: Simultaneous improvement of user experience and performance
- Separation of server and client roles: Server handles data processing, client manages UI state
- Consistency of soft delete: Both server and client use the same deletion strategy
Conclusion
The transition from revalidatePath
to Optimistic Updates was not just a technical change, but a fundamental improvement in user experience. We were able to resolve metadata duplication issues while implementing a faster and more responsive interface.
Through this experience, we were able to reconfirm the importance of cache strategy and the value of client-side state management.
Tech Stack: Next.js 15, TypeScript, Supabase, Tailwind CSS
Scope: Like, comment, view count, download count systems
Results: 0% metadata duplication, 100% UI responsiveness improvement