你好呀,我是小邹。
今天给博客的留言板进行了优化,极大地提升了响应速度。欢迎大家留言~
留言板地址:https://www.hqxiaozou.top/about
示例图
![]()
背景与问题分析
在我开发的博客系统中,留言板功能是一个重要组成部分。在旧版本中,我采用一次性全量递归查询的方式加载所有评论及其回复。虽然这种方式在数据量小的情况下表现良好,但随着评论数量的增长和回复层级的加深,页面加载速度显著下降。
旧方案的问题:
- 数据库压力大:每次页面加载都需要执行复杂的递归查询
- 页面响应慢:大量数据传输和渲染导致用户等待时间长
- 资源浪费:用户可能只看部分评论,但系统加载了所有数据
解决方案:按需加载优化
新方案的核心思想是延迟加载和按需加载:
- 首次加载只查询顶级评论(parent_id=-1)
- 为每个评论添加"展开回复"按钮
- 用户点击时通过AJAX获取该评论下的完整回复树
- 平铺展示回复内容,保持界面简洁
系统架构变化
旧架构:
页面加载 → 查询所有评论(递归) → 返回嵌套结构 → 前端渲染
新架构:
页面加载 → 查询顶级评论 → 前端渲染
↓
用户点击"展开回复" → AJAX请求 → 递归查询该评论的回复树 → 返回平铺结构 → 动态渲染
关键技术实现
后端改造
1. 实体类增加回复计数字段
// Comments.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("article_comments")
public class Comments implements Serializable {
// ...其他字段
@TableField(exist = false)
private Long replyCount; // 新增回复计数字段
}
2. 评论服务层优化
// CommentServiceImpl.java
@Override
@Cacheable(value = ARTICLES_CACHE_NAME, key = "#blogId")
public List<Comments> listCommentByBlogId(int blogId) {
// 只查询顶级评论
QueryWrapper<Comments> wrapper = new QueryWrapper<Comments>()
.eq("blog_id", blogId)
.eq("is_visible", CommentStatus.VISIBLE.getStatus())
.eq("parent_id", -1)
.orderByAsc("sort")
.orderByDesc("create_time");
wrapper.select("id", "nickname", "content", "create_time", "avatar",
"sort", "parent_id", "province", "parent_name");
List<Comments> comments = commentsMapper.selectList(wrapper);
handleAddress(comments);
// 为每个评论添加回复数量
for (Comments comment : comments) {
long replyCount = commentsMapper.selectCount(new QueryWrapper<Comments>()
.eq("parent_id", comment.getId())
.eq("is_visible", CommentStatus.VISIBLE.getStatus()));
comment.setReplyCount(replyCount);
}
return comments;
}
@Override
public List<Comments> getReplyTreeByParentId(int parentId) {
// 查询直接子回复
QueryWrapper<Comments> wrapper = new QueryWrapper<Comments>()
.eq("parent_id", parentId)
.eq("is_visible", CommentStatus.VISIBLE.getStatus())
.orderByAsc("sort")
.orderByDesc("create_time");
wrapper.select("id", "nickname", "content", "create_time", "avatar",
"sort", "parent_id", "province", "parent_name");
List<Comments> directReplies = commentsMapper.selectList(wrapper);
// 递归查询每个回复的子回复
for (Comments reply : directReplies) {
List<Comments> childReplies = getReplyTreeByParentId(reply.getId());
reply.setReplyComments(childReplies);
}
return directReplies;
}
3. 新增API接口
// IndexController.java
@GetMapping("/comments/replies/tree")
@ResponseBody
public List<Comments> getReplyTree(@RequestParam int parentId) {
return commentService.getReplyTreeByParentId(parentId);
}
前端改造
1. HTML结构优化
<div class="comment" th:each="comment : ${comments}">
<!-- 评论内容 -->
<div class="actions">
<a class="reply" ...>回复</a>
<!-- 新增展开回复按钮 -->
<a th:if="${comment.replyCount > 0}"
class="show-replies"
th:attr="data-commentid=${comment.id}, data-replycount=${comment.replyCount}"
onclick="loadReplies(this)">
<button class="mini ui orange basic circular button">
展开回复(<span th:text="${comment.replyCount}"></span>)
</button>
</a>
</div>
<!-- 回复容器 -->
<div class="replies-container" th:attr="id='replies-' + ${comment.id}"></div>
</div>
2. JavaScript异步加载实现
function loadReplies(element) {
const $element = $(element);
const commentId = $element.data('commentid');
const container = $(`#replies-${commentId}`);
const button = $element.find('button');
// 切换展开/收起状态
if (container.children().length > 0) {
container.toggle();
if (container.is(':visible')) {
button.text('收起回复');
} else {
button.text(`展开回复(${$element.data('replycount')})`);
}
return;
}
// 显示加载中状态
const originalText = button.text();
button.text('加载中...').prop('disabled', true);
// 加载整个回复树
$.ajax({
url: '/comments/replies/tree',
type: 'GET',
data: {parentId: commentId},
success: function (replies) {
renderReplyTree(container, replies);
button.text('收起回复').prop('disabled', false);
},
error: function () {
alert('加载回复失败,请稍后再试');
button.text(originalText).prop('disabled', false);
}
});
}
// 平铺所有回复
function renderReplyTree(container, replies) {
if (!replies || replies.length === 0) return;
let html = '';
// 平铺所有回复(包括嵌套的子回复)
flattenReplies(replies).forEach(reply => {
html += renderReplyItem(reply);
});
container.html(html);
}
// 递归获取平铺的回复列表
function flattenReplies(replies) {
let flatList = [];
if (!replies) return flatList;
replies.forEach(reply => {
// 添加当前回复
flatList.push(reply);
// 递归添加子回复
if (reply.replyComments && reply.replyComments.length > 0) {
flatList = flatList.concat(flattenReplies(reply.replyComments));
}
});
return flatList;
}
function renderReplyItem(reply) {
return `
<div class="comment" style="margin-top: 15px; margin-bottom: 15px;">
<a class="avatar"><img src="${reply.avatar}"></a>
<div class="content">
<a class="author">
<span>${reply.nickname}</span>
</a>
<span> @ </span>
<a class="author" style="color: teal">
<span>${reply.parentName}</span>
</a>
<div class="metadata">
<span class="date">${formatDate(reply.createTime)}</span>
</div>
<span style="color: darkgray">${reply.province}</span>
<div class="text">
<p style="...">${reply.content.trim()}</p>
</div>
<div class="actions">
<a class="reply" ...>回复</a>
</div>
</div>
</div>`;
}
// 日期格式化函数
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())} ${padZero(date.getHours())}:${padZero(date.getMinutes())}`;
}
function padZero(num) {
return num.toString().padStart(2, '0');
}
3. CSS样式优化
/* 展开/收起按钮样式 */
.show-replies .ui.button {
background: #1e88e5;
color: white !important;
border: none !important;
padding: 6px 12px;
font-size: 12px;
border-radius: 15px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(30, 136, 229, 0.3);
}
/* 子评论间距 */
.replies-container .comment {
margin: 15px 0;
padding: 0;
background: transparent;
border-radius: 0;
border-left: none;
}
/* 确保子评论内容与父评论一致 */
.replies-container .comment .text p {
background: #fafafa repeating-linear-gradient(...) !important;
box-shadow: 0 2px 5px rgba(0,0,0,.15) !important;
margin: 20px 0 !important;
padding: 15px !important;
border-radius: 5px !important;
font-size: 14px !important;
color: #555 !important;
text-align: left !important;
white-space: normal !important;
}
性能对比
通过JMeter进行压力测试,模拟100用户同时访问留言板页面:
| 指标 | 旧方案 (全量加载) | 新方案 (按需加载) | 提升幅度 |
|---|---|---|---|
| 平均响应时间 | 1850ms | 320ms | 82.7%↓ |
| 数据库查询时间 | 1200ms | 35ms | 97.1%↓ |
| 数据传输量 | 1.8MB | 120KB | 93.3%↓ |
| 内存占用峰值 | 450MB | 80MB | 82.2%↓ |
总结与最佳实践
本次优化通过懒加载策略和分层查询,有效解决了全量递归查询带来的性能问题。核心经验总结如下:
- 性能显著提升:页面加载速度提高5倍以上
- 资源利用率优化:数据库负载降低97%,内存占用减少82%
- 用户体验改善:用户可按需查看回复,界面更清爽

当前留言板已经实现了分页~