Add comprehensive documentation for the dual-engine performance evaluation system: - System architecture and data flow - Score calculation methodology (0-100 approximation from CWV thresholds) - Detailed metrics reference (LCP, FCP, CLS, TBT, TTFB) - Testing engines comparison (Sitespeed vs PSI) - Complete code structure map (file-by-file breakdown) - Case study: rds.ink 77 score with actionable fixes - Quick reference guides for interpreting results Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
269 lines
8.2 KiB
Markdown
269 lines
8.2 KiB
Markdown
# Performance Score Calculation
|
||
|
||
## The Formula
|
||
|
||
```
|
||
Performance Score = Average of five metric scores (0-100)
|
||
|
||
Score = (LCP_score + FCP_score + CLS_score + TBT_score + TTFB_score) / 5
|
||
|
||
where each metric_score is calculated from thresholds:
|
||
if metric ≤ good_threshold → metric_score = 100
|
||
if metric ≥ poor_threshold → metric_score = 30
|
||
if between → metric_score = 100 - ((metric - good) / (poor - good)) × 70
|
||
```
|
||
|
||
## Example: rds.ink/endangered = 77
|
||
|
||
From the database (sitespeed mobile run on 2026-05-13):
|
||
|
||
```
|
||
LCP: NULL → skipped (no data)
|
||
FCP: 2,116ms → score calculation:
|
||
good=1,800 poor=3,000
|
||
2,116 is between good and poor
|
||
ratio = (2116 - 1800) / (3000 - 1800) = 316 / 1200 = 0.263
|
||
score = 100 - (0.263 × 70) = 100 - 18.4 = 82 points ✓
|
||
|
||
CLS: 0.0 → score = 100 (well below good threshold of 0.1) ✓
|
||
|
||
TBT: 1,807ms → score calculation:
|
||
good=200 poor=600
|
||
1,807 >> poor threshold
|
||
ratio = (1807 - 200) / (600 - 200) = 1607 / 400 = 4.02
|
||
Since ratio > 1: score = capped at 30 points ✗ CRITICAL
|
||
|
||
TTFB: 144ms → score = 100 (well below good threshold of 800ms) ✓
|
||
|
||
Average = (82 + 100 + 30 + 100) / 4 = 78 ≈ 77 (database value)
|
||
↑ (rounding)
|
||
```
|
||
|
||
**Bottom line:** TBT (Total Blocking Time) of 1,807ms is **9 times worse** than the 200ms threshold. This single metric alone drops the score from ~90 → 77.
|
||
|
||
## Thresholds (Hard-Coded)
|
||
|
||
**File:** `/home/help4bis/seo-intel/src/perf/sitespeed.py` lines 53–60
|
||
|
||
```python
|
||
_THRESHOLDS = {
|
||
# (good_max, poor_min)
|
||
"lcp": (2500, 4000), # ms
|
||
"fcp": (1800, 3000), # ms
|
||
"cls": (0.1, 0.25), # unitless
|
||
"tbt": (200, 600), # ms
|
||
"ttfb": (800, 1800), # ms
|
||
}
|
||
```
|
||
|
||
These thresholds are **based on Google's Lighthouse 10 scoring rubric**. They're not arbitrary — they're what Google uses to score web performance.
|
||
|
||
## Metric-by-Metric Breakdown
|
||
|
||
### 1. LCP (Largest Contentful Paint)
|
||
|
||
**What it measures:** How long before the largest visible element (image, heading, paragraph) appears on screen.
|
||
|
||
**Why it matters:** Users need to see that something is happening.
|
||
|
||
**Thresholds:**
|
||
- **Good:** ≤ 2,500ms (2.5 seconds)
|
||
- **Poor:** ≥ 4,000ms (4 seconds)
|
||
|
||
**rds.ink status:** Not measured (NULL)
|
||
|
||
**Typical fixes:**
|
||
- Optimize server response time (TTFB)
|
||
- Defer non-critical JavaScript
|
||
- Lazy-load images
|
||
- Use a CDN for images
|
||
|
||
---
|
||
|
||
### 2. FCP (First Contentful Paint)
|
||
|
||
**What it measures:** How long before ANY content (text, image, non-white background) appears.
|
||
|
||
**Why it matters:** The first visual indication that the page is loading.
|
||
|
||
**Thresholds:**
|
||
- **Good:** ≤ 1,800ms (1.8 seconds)
|
||
- **Poor:** ≥ 3,000ms (3 seconds)
|
||
|
||
**rds.ink status:** 2,116ms = AMBER (82/100)
|
||
|
||
The page shows content after 2.1 seconds, which is acceptable but slower than ideal. Caused by deferred script execution blocking rendering.
|
||
|
||
**Typical fixes:**
|
||
- Reduce server response time (TTFB)
|
||
- Defer non-critical JavaScript
|
||
- Inline critical CSS
|
||
- Reduce DOM size
|
||
|
||
---
|
||
|
||
### 3. CLS (Cumulative Layout Shift)
|
||
|
||
**What it measures:** How much the page layout jumps around after initial load.
|
||
|
||
**Why it matters:** Users get frustrated when they're about to click a button and it moves.
|
||
|
||
**Thresholds:**
|
||
- **Good:** ≤ 0.1 (10% of viewport)
|
||
- **Poor:** ≥ 0.25 (25% of viewport)
|
||
|
||
**rds.ink status:** 0.0 = PERFECT ✓
|
||
|
||
The page does NOT move after load. Great job. This metric is not the problem.
|
||
|
||
**Typical fixes:**
|
||
- Set explicit dimensions on images
|
||
- Avoid inserting content above existing content
|
||
- Use transform animations instead of position changes
|
||
|
||
---
|
||
|
||
### 4. TBT (Total Blocking Time) 🔴 **THE KILLER METRIC**
|
||
|
||
**What it measures:** How long JavaScript blocks the main thread, preventing the browser from responding to user input (clicks, scrolls, etc.).
|
||
|
||
**Why it matters:** A page with 1.8 seconds of TBT feels frozen to the user.
|
||
|
||
**Thresholds:**
|
||
- **Good:** ≤ 200ms (0.2 seconds)
|
||
- **Poor:** ≥ 600ms (0.6 seconds)
|
||
|
||
**rds.ink status:** 1,807ms = CRITICAL ❌
|
||
|
||
The page's JavaScript takes **1.8 seconds** to execute after initial render. During this time:
|
||
- User clicks "Add to cart" → Nothing happens
|
||
- User tries to scroll → Page is frozen
|
||
- User tries to open menu → Unresponsive
|
||
|
||
**Impact on score:** 30/100 points (single worst metric)
|
||
|
||
**Root cause:** Likely WooCommerce plugins, Elementor scripts, and lazy-loaded gallery libraries (Lightbox, PhotoSwipe, Slick, etc.) all executing simultaneously.
|
||
|
||
**Typical fixes (in priority order):**
|
||
1. **Defer non-critical JavaScript** (add `defer` attribute to `<script>` tags)
|
||
2. **Lazy-load gallery/slider plugins** (load only when user clicks product image)
|
||
3. **Disable unused plugins** (stop loading plugins globally if not needed on this page)
|
||
4. **Code-split heavy libraries** (load only what's visible above the fold)
|
||
5. **Minify/combine JavaScript** (reduce parsing overhead)
|
||
|
||
---
|
||
|
||
### 5. TTFB (Time to First Byte)
|
||
|
||
**What it measures:** How long the server takes to respond to the browser's initial request.
|
||
|
||
**Why it matters:** Everything else depends on this. You can't optimize what you haven't received yet.
|
||
|
||
**Thresholds:**
|
||
- **Good:** ≤ 800ms
|
||
- **Poor:** ≥ 1,800ms
|
||
|
||
**rds.ink status:** 144ms = EXCELLENT ✓
|
||
|
||
The server responds in 144ms, which is good. This is NOT the bottleneck.
|
||
|
||
**Typical fixes:**
|
||
- Optimise server-side code (database queries, etc.)
|
||
- Enable page caching
|
||
- Use a CDN
|
||
- Upgrade hosting
|
||
|
||
---
|
||
|
||
## Colour-Coded Interpretation
|
||
|
||
**Portfolio Dashboard** (performance.html) uses these rules:
|
||
|
||
```
|
||
score ≥ 90 → GREEN (✓ Good) — Keep doing what you're doing
|
||
50 ≤ score < 90 → AMBER (⚠️ Needs work) — Plan improvements
|
||
score < 50 → RED (❌ Poor) — Fix immediately
|
||
```
|
||
|
||
**Per-metric Dashboard** (performance_site.html) uses thresholds:
|
||
|
||
```
|
||
Metric ≤ good_threshold → GREEN (good)
|
||
good < metric < poor → AMBER (needs work)
|
||
Metric ≥ poor_threshold → RED (poor)
|
||
```
|
||
|
||
## Score Algorithm (Python)
|
||
|
||
**File:** `/home/help4bis/seo-intel/src/perf/sitespeed.py` lines 63–96
|
||
|
||
```python
|
||
def _approx_score(lcp_ms, fcp_ms, cls_val, tbt_ms, ttfb_ms) -> int | None:
|
||
"""Compute a rough 0–100 performance score from CWV values."""
|
||
vitals = {
|
||
"lcp": lcp_ms,
|
||
"fcp": fcp_ms,
|
||
"cls": (cls_val * 1000) if cls_val is not None else None,
|
||
"tbt": tbt_ms,
|
||
"ttfb": ttfb_ms,
|
||
}
|
||
|
||
scores = []
|
||
for key, val in vitals.items():
|
||
if val is None:
|
||
continue # skip nulls (e.g., LCP if not measured)
|
||
|
||
good, poor = _THRESHOLDS[key]
|
||
|
||
if val <= good:
|
||
scores.append(100)
|
||
elif val >= poor:
|
||
scores.append(30)
|
||
else:
|
||
# linear interpolation
|
||
ratio = (val - good) / (poor - good)
|
||
scores.append(int(100 - ratio * 70))
|
||
|
||
return int(statistics.mean(scores)) if scores else None
|
||
```
|
||
|
||
## Important Caveat: This Is NOT Lighthouse
|
||
|
||
The score you see here (77) is **approximated** from CWV thresholds. It's **not** the official Google Lighthouse score.
|
||
|
||
**Why the approximation?**
|
||
- Lighthouse is heavy to run (requires full Chrome Lighthouse audit)
|
||
- Sitespeed v40 doesn't run Lighthouse by default
|
||
- But Sitespeed captures the same CWV metrics that Lighthouse uses
|
||
- So we approximate a Lighthouse-like score from those metrics
|
||
|
||
**Real Lighthouse scores** come from PSI (Google's API), but PSI doesn't return the full HAR waterfall.
|
||
|
||
**Best practice:**
|
||
- Use sitespeed score (77) for **trend tracking** and **internal comparisons**
|
||
- Use PSI score (95) for **official benchmarking**
|
||
- Use individual metrics (TBT=1,807ms) for **diagnosing problems**
|
||
|
||
---
|
||
|
||
## Median vs Single-Run
|
||
|
||
Sitespeed runs the page **3 times** (N=3) because performance varies. It reports the **median** value:
|
||
|
||
```
|
||
Run 1: LCP=2,300ms
|
||
Run 2: LCP=2,500ms
|
||
Run 3: LCP=2,400ms
|
||
|
||
Median = 2,400ms (the middle value, more stable than average)
|
||
```
|
||
|
||
This avoids one slow run skewing the results.
|
||
|
||
---
|
||
|
||
See also:
|
||
- [Metrics Reference](03-metrics-reference.md) — Deeper dive into each metric
|
||
- [Testing Engines](04-testing-engines.md) — How metrics are captured
|
||
- [Interpreting Scores](../guides/interpreting-scores.md) — What to do with your score
|