perf(image): store cached image data as Bytes to avoid Vec cloning
This commit is contained in:
parent
411e565465
commit
36554af5f5
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -5393,6 +5393,7 @@ version = "0.2.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"axum",
|
"axum",
|
||||||
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"deadpool-postgres",
|
"deadpool-postgres",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
|
|||||||
@ -37,6 +37,7 @@ moka = { version = "0.12", features = ["future"], optional = true }
|
|||||||
governor = { version = "0.8", optional = true }
|
governor = { version = "0.8", optional = true }
|
||||||
md-5 = { version = "0.10", optional = true }
|
md-5 = { version = "0.10", optional = true }
|
||||||
futures = { version = "0.3", optional = true }
|
futures = { version = "0.3", optional = true }
|
||||||
|
bytes = "1"
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[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"] }
|
web-sys = { version = "0.3", features = ["Document", "Window", "Storage", "Element", "HtmlElement", "DomTokenList", "MediaQueryList", "HtmlImageElement", "MouseEvent", "KeyboardEvent", "Node", "EventTarget", "Navigator"] }
|
||||||
|
|||||||
@ -19,6 +19,8 @@ use moka::future::Cache;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
fn etag_for(data: &[u8]) -> String {
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
/// 缓存条目,保存处理后的图片字节与 Content-Type。
|
/// 缓存条目,保存处理后的图片字节与 Content-Type。
|
||||||
struct CachedImage {
|
struct CachedImage {
|
||||||
data: Vec<u8>,
|
data: Bytes,
|
||||||
content_type: HeaderValue,
|
content_type: HeaderValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +188,7 @@ fn content_type(format: image::ImageFormat) -> HeaderValue {
|
|||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
fn image_response(
|
fn image_response(
|
||||||
data: Vec<u8>,
|
data: Bytes,
|
||||||
content_type: HeaderValue,
|
content_type: HeaderValue,
|
||||||
cache_control: &'static str,
|
cache_control: &'static str,
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
@ -399,7 +401,10 @@ async fn read_disk_cache(cache_key: &str) -> Option<CachedImage> {
|
|||||||
.ok()
|
.ok()
|
||||||
.unwrap_or_else(|| "application/octet-stream".to_string());
|
.unwrap_or_else(|| "application/octet-stream".to_string());
|
||||||
let content_type = HeaderValue::from_str(&ct_str).ok()?;
|
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")]
|
#[cfg(feature = "server")]
|
||||||
@ -454,7 +459,7 @@ pub async fn serve_image(
|
|||||||
return match tokio::fs::read(&file_path).await {
|
return match tokio::fs::read(&file_path).await {
|
||||||
Ok(data) => {
|
Ok(data) => {
|
||||||
let ct = content_type(detect_format(&path));
|
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(),
|
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||||
};
|
};
|
||||||
@ -462,20 +467,19 @@ pub async fn serve_image(
|
|||||||
|
|
||||||
let cache_key = params.cache_key(&path);
|
let cache_key = params.cache_key(&path);
|
||||||
if let Some(cached) = IMAGE_CACHE.get(&cache_key).await {
|
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 {
|
if let Some(cached) = read_disk_cache(&cache_key).await {
|
||||||
let _ = IMAGE_CACHE
|
let data = cached.data.clone();
|
||||||
.insert(
|
let content_type = cached.content_type.clone();
|
||||||
cache_key.clone(),
|
let _ = IMAGE_CACHE.insert(cache_key.clone(), cached).await;
|
||||||
CachedImage {
|
return image_response(data, content_type, "public, max-age=86400", &headers);
|
||||||
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 = match tokio::fs::read(&file_path).await {
|
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 {
|
let cached = CachedImage {
|
||||||
data: processed.clone(),
|
data: processed.clone(),
|
||||||
content_type: content_type.clone(),
|
content_type: content_type.clone(),
|
||||||
@ -758,7 +763,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn image_response_includes_cache_headers() {
|
fn image_response_includes_cache_headers() {
|
||||||
let resp = image_response(
|
let resp = image_response(
|
||||||
vec![1, 2, 3],
|
Bytes::from(vec![1, 2, 3]),
|
||||||
HeaderValue::from_static("image/webp"),
|
HeaderValue::from_static("image/webp"),
|
||||||
"public, max-age=86400",
|
"public, max-age=86400",
|
||||||
&HeaderMap::new(),
|
&HeaderMap::new(),
|
||||||
@ -778,7 +783,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn image_response_returns_304_when_etag_matches() {
|
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 etag = etag_for(&data);
|
||||||
let mut req_headers = HeaderMap::new();
|
let mut req_headers = HeaderMap::new();
|
||||||
req_headers.insert(
|
req_headers.insert(
|
||||||
@ -826,7 +831,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn image_response_raw_file_is_immutable() {
|
fn image_response_raw_file_is_immutable() {
|
||||||
let resp = image_response(
|
let resp = image_response(
|
||||||
vec![1, 2, 3],
|
Bytes::from(vec![1, 2, 3]),
|
||||||
HeaderValue::from_static("image/jpeg"),
|
HeaderValue::from_static("image/jpeg"),
|
||||||
"public, max-age=31536000, immutable",
|
"public, max-age=31536000, immutable",
|
||||||
&HeaderMap::new(),
|
&HeaderMap::new(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user