Files
seo-intel-docs/docs/02-score-calculation.md
help4bis 335d9a76e1 Initial SEO-INTEL documentation: architecture, scoring, code structure
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>
2026-05-14 05:56:49 +10:00

269 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 5360
```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 6396
```python
def _approx_score(lcp_ms, fcp_ms, cls_val, tbt_ms, ttfb_ms) -> int | None:
"""Compute a rough 0100 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