From 36554af5f5d08e0dd3d050933c17743ddb1af859 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 18 Jun 2026 10:03:35 +0800 Subject: [PATCH] perf(image): store cached image data as Bytes to avoid Vec cloning --- Cargo.lock | 1 + Cargo.toml | 1 + src/api/image.rs | 41 +++++++++++++++++++++++------------------ 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05e4cc3..532ca5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5393,6 +5393,7 @@ version = "0.2.0" dependencies = [ "argon2", "axum", + "bytes", "chrono", "deadpool-postgres", "dioxus", diff --git a/Cargo.toml b/Cargo.toml index db8bda4..ae5c55c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ moka = { version = "0.12", features = ["future"], optional = true } governor = { version = "0.8", optional = true } md-5 = { version = "0.10", optional = true } futures = { version = "0.3", optional = true } +bytes = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3", features = ["Document", "Window", "Storage", "Element", "HtmlElement", "DomTokenList", "MediaQueryList", "HtmlImageElement", "MouseEvent", "KeyboardEvent", "Node", "EventTarget", "Navigator"] } diff --git a/src/api/image.rs b/src/api/image.rs index 6f85661..d94332d 100644 --- a/src/api/image.rs +++ b/src/api/image.rs @@ -19,6 +19,8 @@ use moka::future::Cache; use serde::Deserialize; #[cfg(feature = "server")] use std::sync::LazyLock; +#[cfg(feature = "server")] +use bytes::Bytes; #[cfg(feature = "server")] fn etag_for(data: &[u8]) -> String { @@ -51,7 +53,7 @@ pub const MAX_IMAGE_PIXELS: u32 = 25_000_000; // ~5k x 5k #[derive(Debug, Clone)] /// 缓存条目,保存处理后的图片字节与 Content-Type。 struct CachedImage { - data: Vec, + data: Bytes, content_type: HeaderValue, } @@ -186,7 +188,7 @@ fn content_type(format: image::ImageFormat) -> HeaderValue { #[cfg(feature = "server")] fn image_response( - data: Vec, + data: Bytes, content_type: HeaderValue, cache_control: &'static str, headers: &HeaderMap, @@ -399,7 +401,10 @@ async fn read_disk_cache(cache_key: &str) -> Option { .ok() .unwrap_or_else(|| "application/octet-stream".to_string()); let content_type = HeaderValue::from_str(&ct_str).ok()?; - Some(CachedImage { data, content_type }) + Some(CachedImage { + data: Bytes::from(data), + content_type, + }) } #[cfg(feature = "server")] @@ -454,7 +459,7 @@ pub async fn serve_image( return match tokio::fs::read(&file_path).await { Ok(data) => { let ct = content_type(detect_format(&path)); - image_response(data, ct, "public, max-age=31536000, immutable", &headers) + image_response(Bytes::from(data), ct, "public, max-age=31536000, immutable", &headers) } Err(_) => StatusCode::NOT_FOUND.into_response(), }; @@ -462,20 +467,19 @@ pub async fn serve_image( let cache_key = params.cache_key(&path); if let Some(cached) = IMAGE_CACHE.get(&cache_key).await { - return image_response(cached.data, cached.content_type, "public, max-age=86400", &headers); + return image_response( + cached.data.clone(), + cached.content_type, + "public, max-age=86400", + &headers, + ); } if let Some(cached) = read_disk_cache(&cache_key).await { - let _ = IMAGE_CACHE - .insert( - cache_key.clone(), - CachedImage { - data: cached.data.clone(), - content_type: cached.content_type.clone(), - }, - ) - .await; - return image_response(cached.data, cached.content_type, "public, max-age=86400", &headers); + let data = cached.data.clone(); + let content_type = cached.content_type.clone(); + let _ = IMAGE_CACHE.insert(cache_key.clone(), cached).await; + return image_response(data, content_type, "public, max-age=86400", &headers); } let data = match tokio::fs::read(&file_path).await { @@ -502,6 +506,7 @@ pub async fn serve_image( } }; + let processed = Bytes::from(processed); let cached = CachedImage { data: processed.clone(), content_type: content_type.clone(), @@ -758,7 +763,7 @@ mod tests { #[test] fn image_response_includes_cache_headers() { let resp = image_response( - vec![1, 2, 3], + Bytes::from(vec![1, 2, 3]), HeaderValue::from_static("image/webp"), "public, max-age=86400", &HeaderMap::new(), @@ -778,7 +783,7 @@ mod tests { #[test] fn image_response_returns_304_when_etag_matches() { - let data = vec![1, 2, 3]; + let data = Bytes::from(vec![1, 2, 3]); let etag = etag_for(&data); let mut req_headers = HeaderMap::new(); req_headers.insert( @@ -826,7 +831,7 @@ mod tests { #[test] fn image_response_raw_file_is_immutable() { let resp = image_response( - vec![1, 2, 3], + Bytes::from(vec![1, 2, 3]), HeaderValue::from_static("image/jpeg"), "public, max-age=31536000, immutable", &HeaderMap::new(),