use super::super::{AuditGate, AuditOptions, Finding, Severity, Site};
use super::hreflang_attr;
const NAME: &str = "performance";
#[derive(Debug, Clone, Copy)]
pub struct PerformanceGate;
impl AuditGate for PerformanceGate {
fn name(&self) -> &'static str {
NAME
}
fn explain(&self) -> &'static str {
"Enforces per-page weight + JS budgets. Default page weight \
budget is 100 KiB (HTML + inline CSS); default JS budget is \
50 KiB (inline + first-party referenced bundles). Tune via \
the [audit.budgets] table in ssg.toml."
}
fn run(&self, site: &Site, opts: &AuditOptions) -> Vec<Finding> {
let mut findings = Vec::new();
for path in &site.html_files {
let Ok(html) = site.read(path) else { continue };
let rel = site.rel(path);
let html_weight = html.len();
if html_weight > opts.page_weight_budget {
findings.push(
Finding::new(
NAME,
Severity::Warn,
format!(
"page weight {html_weight} bytes exceeds budget {} bytes",
opts.page_weight_budget
),
)
.with_code("PERF-PAGE-OVER")
.with_path(rel.clone()),
);
}
let js_weight = inline_js_bytes(&html)
+ referenced_js_bytes(&site.root, path, &html);
if js_weight > opts.js_budget {
findings.push(
Finding::new(
NAME,
Severity::Warn,
format!(
"JS weight {js_weight} bytes exceeds budget {} bytes",
opts.js_budget
),
)
.with_code("PERF-JS-OVER")
.with_path(rel.clone()),
);
}
}
findings
}
}
fn inline_js_bytes(html: &str) -> usize {
let lower = html.to_lowercase();
let mut total = 0usize;
let mut cursor = 0;
while let Some(rel) = lower[cursor..].find("<script") {
let abs = cursor + rel;
let open_end =
lower[abs..].find('>').map_or(lower.len(), |e| abs + e + 1);
let tag = &html[abs..open_end];
if hreflang_attr(tag, "src").is_some() {
cursor = open_end;
continue;
}
cursor = open_end;
let Some(close) = lower[cursor..].find("</script>") else {
break;
};
total += close;
cursor += close + "</script>".len();
}
total
}
fn referenced_js_bytes(
root: &std::path::Path,
page: &std::path::Path,
html: &str,
) -> usize {
let lower = html.to_lowercase();
let mut total = 0usize;
let mut cursor = 0;
while let Some(rel) = lower[cursor..].find("<script") {
let abs = cursor + rel;
let end = lower[abs..].find('>').map_or(lower.len(), |e| abs + e + 1);
let tag = &html[abs..end];
cursor = end;
let Some(src) = hreflang_attr(tag, "src") else {
continue;
};
if src.starts_with("http://")
|| src.starts_with("https://")
|| src.starts_with("//")
{
continue;
}
let candidate = if let Some(s) = src.strip_prefix('/') {
root.join(s)
} else if let Some(parent) = page.parent() {
parent.join(&src)
} else {
root.join(&src)
};
if let Ok(meta) = std::fs::metadata(&candidate) {
total += meta.len() as usize;
}
}
total
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn site(html: &str) -> Site {
let tmp = tempfile::tempdir().unwrap();
let p = tmp.path().join("page.html");
std::fs::write(&p, html).unwrap();
let root = tmp.path().to_path_buf();
std::mem::forget(tmp);
Site {
root,
html_files: vec![p],
}
}
#[test]
fn small_page_passes() {
let html = "<html><body>tiny</body></html>";
let f = PerformanceGate.run(&site(html), &AuditOptions::default());
assert!(f.is_empty(), "got {f:?}");
}
#[test]
fn over_budget_page_flagged() {
let html = "<html>".to_string() + &"x".repeat(2000) + "</html>";
let opts = AuditOptions {
page_weight_budget: 100,
..AuditOptions::default()
};
let f = PerformanceGate.run(&site(&html), &opts);
assert!(f
.iter()
.any(|x| x.code.as_deref() == Some("PERF-PAGE-OVER")));
}
#[test]
fn over_budget_inline_js_flagged() {
let html = format!(
"<html><body><script>{}</script></body></html>",
"x".repeat(2000)
);
let opts = AuditOptions {
js_budget: 100,
page_weight_budget: 1_000_000,
..AuditOptions::default()
};
let f = PerformanceGate.run(&site(&html), &opts);
assert!(f.iter().any(|x| x.code.as_deref() == Some("PERF-JS-OVER")));
}
#[test]
fn external_script_src_does_not_count_toward_js_budget() {
let html = r#"<html><body><script src="https://cdn.example/big.js"></script></body></html>"#;
let opts = AuditOptions {
js_budget: 50,
page_weight_budget: 1_000_000,
..AuditOptions::default()
};
let f = PerformanceGate.run(&site(html), &opts);
assert!(
f.iter().all(|x| x.code.as_deref() != Some("PERF-JS-OVER")),
"external scripts must not count; got {f:?}"
);
}
#[test]
fn protocol_relative_script_src_is_external() {
let html = r#"<html><body><script src="//cdn.example/big.js"></script></body></html>"#;
let opts = AuditOptions {
js_budget: 1,
page_weight_budget: 1_000_000,
..AuditOptions::default()
};
let f = PerformanceGate.run(&site(html), &opts);
assert!(f.iter().all(|x| x.code.as_deref() != Some("PERF-JS-OVER")));
}
#[test]
fn referenced_root_relative_js_is_summed() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().to_path_buf();
std::fs::write(root.join("big.js"), vec![0u8; 5_000]).unwrap();
let html_path = root.join("page.html");
std::fs::write(
&html_path,
r#"<html><body><script src="/big.js"></script></body></html>"#,
)
.unwrap();
std::mem::forget(tmp);
let s = Site {
root,
html_files: vec![html_path],
};
let opts = AuditOptions {
js_budget: 100,
page_weight_budget: 1_000_000,
..AuditOptions::default()
};
let f = PerformanceGate.run(&s, &opts);
assert!(
f.iter().any(|x| x.code.as_deref() == Some("PERF-JS-OVER")),
"expected JS-OVER from referenced file; got {f:?}"
);
}
#[test]
fn referenced_relative_js_uses_page_parent() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().to_path_buf();
let sub = root.join("sub");
std::fs::create_dir_all(&sub).unwrap();
std::fs::write(sub.join("local.js"), vec![0u8; 5_000]).unwrap();
let html_path = sub.join("page.html");
std::fs::write(
&html_path,
r#"<html><body><script src="local.js"></script></body></html>"#,
)
.unwrap();
std::mem::forget(tmp);
let s = Site {
root,
html_files: vec![html_path],
};
let opts = AuditOptions {
js_budget: 100,
page_weight_budget: 1_000_000,
..AuditOptions::default()
};
let f = PerformanceGate.run(&s, &opts);
assert!(
f.iter().any(|x| x.code.as_deref() == Some("PERF-JS-OVER")),
"expected JS-OVER from page-relative ref; got {f:?}"
);
}
#[test]
fn unreadable_html_file_is_skipped() {
let tmp = tempfile::tempdir().unwrap();
let bogus = tmp.path().join("ghost.html");
let s = Site {
root: tmp.path().to_path_buf(),
html_files: vec![bogus],
};
std::mem::forget(tmp);
let f = PerformanceGate.run(&s, &AuditOptions::default());
assert!(f.is_empty());
}
#[test]
fn inline_js_is_counted_when_no_src_attribute() {
let html = format!(
"<html><body><script>{}</script><script>nested</script></body></html>",
"y".repeat(200)
);
let opts = AuditOptions {
js_budget: 100,
page_weight_budget: 1_000_000,
..AuditOptions::default()
};
let f = PerformanceGate.run(&site(&html), &opts);
assert!(f.iter().any(|x| x.code.as_deref() == Some("PERF-JS-OVER")));
}
#[test]
fn metadata_methods_exposed() {
let g = PerformanceGate;
assert_eq!(g.name(), "performance");
assert!(g.explain().contains("budget"));
let _copy: PerformanceGate = g;
let _clone = g;
assert!(format!("{g:?}").contains("PerformanceGate"));
}
}