perf(posts): list/search 零 capacity Vec 改 collect 预分配 + helpers retain

list.rs 5 处、search.rs 1 处用 Vec::new()+循环 push,初始 capacity 为 0,
循环触发多次指数 realloc。rows.iter().map().collect() 用 slice 迭代器的
精确 size_hint 一次性预分配。首页/列表/搜索/标签分页路径。

helpers.rs row_to_post_list_item / row_to_post_full:tags 从
into_iter().filter().collect() 改 retain 原地过滤。unwrap_or_default()
已返回 owned Vec,旧写法重建第二个 Vec。每行文章记录省一次 Vec 分配。

顺带修 slugify 一个 dead store(prev_dash=false 后被 true 无条件覆盖)。
This commit is contained in:
xfy 2026-07-15 11:46:56 +08:00
parent a41a1d3ae1
commit b269fcf205
4 changed files with 13 additions and 39 deletions

View File

@ -24,13 +24,9 @@ pub(super) fn row_to_post_list_item(row: &tokio_postgres::Row) -> PostListItem {
let status_str: String = row.get("status");
let status = PostStatus::from_str(&status_str).unwrap_or(PostStatus::Draft);
// 聚合标签并过滤空字符串。
let tags: Vec<String> = row
.try_get::<_, Vec<String>>("tags")
.unwrap_or_default()
.into_iter()
.filter(|t| !t.is_empty())
.collect();
// 聚合标签并原地过滤空字符串retain 避免 into_iter+filter+collect 的二次 Vec 分配)。
let mut tags: Vec<String> = row.try_get::<_, Vec<String>>("tags").unwrap_or_default();
tags.retain(|t| !t.is_empty());
let word_count: i32 = row.get("word_count");
let reading_time: i32 = row.get("reading_time");
@ -66,13 +62,9 @@ pub(super) async fn row_to_post_full(
let role_str: String = row.get("status");
let status = PostStatus::from_str(&role_str).unwrap_or(PostStatus::Draft);
// 聚合标签并过滤空字符串。
let tags: Vec<String> = row
.try_get::<_, Vec<String>>("tags")
.unwrap_or_default()
.into_iter()
.filter(|t| !t.is_empty())
.collect();
// 聚合标签并原地过滤空字符串retain 避免 into_iter+filter+collect 的二次 Vec 分配)。
let mut tags: Vec<String> = row.try_get::<_, Vec<String>>("tags").unwrap_or_default();
tags.retain(|t| !t.is_empty());
// 解析上一篇文章导航。
let prev_post = if let Ok(prev_title) = row.try_get::<_, String>("prev_title") {

View File

@ -98,10 +98,7 @@ pub async fn list_published_posts(
.await
.map_err(AppError::query)?;
let mut posts = Vec::new();
for row in &rows {
posts.push(row_to_post_list_item(row));
}
let posts: Vec<_> = rows.iter().map(row_to_post_list_item).collect();
crate::cache::set_post_list(&cache_key, posts.clone(), total).await;
Ok(PostListResponse { posts, total })
@ -156,10 +153,7 @@ pub async fn list_posts(page: i32, per_page: i32) -> Result<PostListResponse, Se
.await
.map_err(AppError::query)?;
let mut posts = Vec::new();
for row in &rows {
posts.push(row_to_post_list_item(row));
}
let posts: Vec<_> = rows.iter().map(row_to_post_list_item).collect();
Ok(PostListResponse { posts, total })
}
@ -219,10 +213,7 @@ pub async fn list_deleted_posts(
.await
.map_err(AppError::query)?;
let mut posts = Vec::new();
for row in &rows {
posts.push(row_to_post_list_item(row));
}
let posts: Vec<_> = rows.iter().map(row_to_post_list_item).collect();
Ok(PostListResponse { posts, total })
}
@ -312,10 +303,7 @@ pub async fn get_posts_by_tag(
.await
.map_err(AppError::query)?;
let mut posts = Vec::new();
for row in &rows {
posts.push(row_to_post_list_item(row));
}
let posts: Vec<_> = rows.iter().map(row_to_post_list_item).collect();
crate::cache::set_posts_by_tag_paged(&cache_key, posts.clone(), total).await;
Ok(PostListResponse { posts, total })
@ -364,10 +352,7 @@ pub async fn get_posts_by_tag(
.await
.map_err(AppError::query)?;
let mut posts = Vec::new();
for row in &rows {
posts.push(row_to_post_list_item(row));
}
let posts: Vec<_> = rows.iter().map(row_to_post_list_item).collect();
// total 为真实 COUNT(*),不再用 posts.len()。
crate::cache::set_posts_by_tag(&tag_name, posts.clone(), total).await;

View File

@ -85,10 +85,7 @@ pub async fn search_posts(query: String) -> Result<PostListResponse, ServerFnErr
.await
.map_err(AppError::query)?;
let mut posts = Vec::new();
for row in &rows {
posts.push(row_to_post_list_item(row));
}
let posts: Vec<_> = rows.iter().map(row_to_post_list_item).collect();
let total = posts.len() as i64;
cache::set_search_results(&cache_key, posts.clone(), total).await;

View File

@ -50,8 +50,8 @@ pub fn slugify(title: &str) -> String {
break;
}
}
prev_dash = false;
// 汉字成词,后接 `-`(合并连续、去除尾部由末尾清理负责)。
// 拼音后必然接 `-`,所以 prev_dash 在此无条件置 true中间无读。
if !push_char(&mut out, &mut len, '-') {
break;
}