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

8.2 KiB
Raw Blame History

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

_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

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: