use serde::{Deserialize, Serialize};
use super::vitals::WebVitals;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PerformanceGrade {
Platinum,
Gold,
Silver,
Bronze,
NeedsImprovement,
}
impl PerformanceGrade {
pub fn from_score(score: u32) -> Self {
match score {
90..=100 => PerformanceGrade::Platinum,
75..=89 => PerformanceGrade::Gold,
60..=74 => PerformanceGrade::Silver,
50..=59 => PerformanceGrade::Bronze,
_ => PerformanceGrade::NeedsImprovement,
}
}
pub fn emoji(&self) -> &'static str {
match self {
PerformanceGrade::Platinum => "🏆",
PerformanceGrade::Gold => "🥇",
PerformanceGrade::Silver => "🥈",
PerformanceGrade::Bronze => "🥉",
PerformanceGrade::NeedsImprovement => "⚠️",
}
}
pub fn label(&self) -> &'static str {
match self {
PerformanceGrade::Platinum => "SEHR GUT",
PerformanceGrade::Gold => "GUT",
PerformanceGrade::Silver => "SOLIDE",
PerformanceGrade::Bronze => "AUSBAUFÄHIG",
PerformanceGrade::NeedsImprovement => "UNGENÜGEND",
}
}
}
impl std::fmt::Display for PerformanceGrade {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.emoji(), self.label())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceScore {
pub overall: u32,
pub grade: PerformanceGrade,
pub lcp_score: Option<u32>,
pub fcp_score: Option<u32>,
pub cls_score: Option<u32>,
pub interactivity_score: Option<u32>,
pub metrics_available: u32,
}
pub fn calculate_performance_score(vitals: &WebVitals) -> PerformanceScore {
let lcp_score = vitals.lcp.as_ref().map(|v| score_lcp(v.value));
let fcp_score = vitals.fcp.as_ref().map(|v| score_fcp(v.value));
let cls_score = vitals.cls.as_ref().map(|v| score_cls(v.value));
let interactivity_score = vitals.tbt.as_ref().map(|v| score_interactivity(v.value));
let mut total = 0u32;
let mut max_possible = 0u32;
for s in [lcp_score, fcp_score, cls_score, interactivity_score]
.into_iter()
.flatten()
{
total += s;
max_possible += 25;
}
let overall = (total * 100)
.checked_div(max_possible)
.map(|n| n.min(100))
.unwrap_or(0);
let metrics_available = max_possible / 25;
let grade = PerformanceGrade::from_score(overall);
PerformanceScore {
overall,
grade,
lcp_score,
fcp_score,
cls_score,
interactivity_score,
metrics_available,
}
}
fn score_lcp(ms: f64) -> u32 {
if ms <= 1200.0 {
25
} else if ms <= 2500.0 {
25 - ((ms - 1200.0) / 260.0) as u32
} else if ms <= 4000.0 {
20 - ((ms - 2500.0) / 150.0) as u32
} else if ms <= 6000.0 {
10 - ((ms - 4000.0) / 200.0) as u32
} else {
0
}
}
fn score_fcp(ms: f64) -> u32 {
if ms <= 1000.0 {
25
} else if ms <= 1800.0 {
25 - ((ms - 1000.0) / 160.0) as u32
} else if ms <= 3000.0 {
20 - ((ms - 1800.0) / 120.0) as u32
} else if ms <= 5000.0 {
10 - ((ms - 3000.0) / 200.0) as u32
} else {
0
}
}
fn score_cls(value: f64) -> u32 {
if value <= 0.05 {
25
} else if value <= 0.1 {
25 - ((value - 0.05) * 100.0) as u32
} else if value <= 0.25 {
20 - ((value - 0.1) * 66.0) as u32
} else if value <= 0.5 {
10 - ((value - 0.25) * 40.0) as u32
} else {
0
}
}
fn score_interactivity(ms: f64) -> u32 {
if ms <= 100.0 {
25
} else if ms <= 200.0 {
25 - ((ms - 100.0) / 20.0) as u32
} else if ms <= 500.0 {
20 - ((ms - 200.0) / 30.0) as u32
} else if ms <= 1000.0 {
10 - ((ms - 500.0) / 50.0) as u32
} else {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::performance::vitals::VitalMetric;
#[test]
fn test_performance_grade_from_score() {
assert_eq!(PerformanceGrade::from_score(95), PerformanceGrade::Platinum);
assert_eq!(PerformanceGrade::from_score(80), PerformanceGrade::Gold);
assert_eq!(PerformanceGrade::from_score(65), PerformanceGrade::Silver);
assert_eq!(PerformanceGrade::from_score(55), PerformanceGrade::Bronze);
assert_eq!(
PerformanceGrade::from_score(30),
PerformanceGrade::NeedsImprovement
);
}
#[test]
fn test_score_lcp_good() {
assert_eq!(score_lcp(1000.0), 25);
assert!(score_lcp(2000.0) >= 20);
}
#[test]
fn test_score_lcp_poor() {
assert!(score_lcp(5000.0) < 10);
assert_eq!(score_lcp(10000.0), 0);
}
#[test]
fn test_calculate_performance_score() {
let vitals = WebVitals {
lcp: Some(VitalMetric::new(1500.0, 2500.0, 4000.0)),
fcp: Some(VitalMetric::new(1200.0, 1800.0, 3000.0)),
cls: Some(VitalMetric::new(0.05, 0.1, 0.25)),
tbt: Some(VitalMetric::new(150.0, 200.0, 600.0)),
..Default::default()
};
let score = calculate_performance_score(&vitals);
assert!(score.overall >= 80);
assert!(matches!(
score.grade,
PerformanceGrade::Gold | PerformanceGrade::Platinum
));
}
}