Claude
Skills
Sign in
Back

page-efficiency-score

Included with Lifetime
$97 forever

Compute a composite 0-100 efficiency score using JS/CSS coverage, render-blocking resources, transfer size, and navigation timing via CDP.

Image & Video

What this skill does


# Page Efficiency Score

Calculate a weighted composite score (0-100) that measures how efficiently a page uses its resources. Combines JavaScript coverage, CSS coverage, render-blocking resource count, total transfer size, TTFB, and DOMContentLoaded timing into a single actionable metric.

## When to Use

- Quick health check of page load efficiency during development or QA
- Comparing efficiency across pages, routes, or deployments
- Identifying the primary bottleneck category (JS bloat, CSS bloat, render-blocking, network)
- Tracking efficiency improvements over time with a single comparable number
- Prioritizing optimization efforts based on weighted category scores

## Prerequisites

- Playwright MCP server connected with a **Chromium** browser session (CDP required for JS/CSS coverage)
- Target page must be accessible in the browser session
- Page should be tested in a clean state (clear cache for accurate transfer size measurement, or test with cache to measure real-world performance)
- Network conditions should be consistent between comparisons

## Scoring System

The composite score is the weighted sum of six category scores. Each category is scored 0, 5, or 10 based on thresholds.

### Scoring Table

| Category | Weight | Good (10 pts) | OK (5 pts) | Bad (0 pts) |
|---|---|---|---|---|
| JS Unused % | 25% | < 30% unused | 30-60% unused | > 60% unused |
| CSS Unused % | 15% | < 40% unused | 40-70% unused | > 70% unused |
| Render-blocking Resources | 20% | 0-2 resources | 3-5 resources | > 5 resources |
| Total Transfer Size | 20% | < 500 KB | 500 KB - 2 MB | > 2 MB |
| TTFB | 10% | < 200 ms | 200-600 ms | > 600 ms |
| DOMContentLoaded | 10% | < 1000 ms | 1000-3000 ms | > 3000 ms |

### Score Interpretation

| Score Range | Rating | Meaning |
|---|---|---|
| 85 - 100 | Excellent | Page is well-optimized; minor improvements possible |
| 70 - 84 | Good | Solid performance with some optimization opportunities |
| 50 - 69 | Needs Work | Significant inefficiencies; prioritize top weight categories |
| 30 - 49 | Poor | Major resource waste; likely impacts user experience |
| 0 - 29 | Critical | Severe inefficiency across multiple categories |

## Workflow

### Step 1: Start Coverage Tracking via CDP

Use `browser_run_code` to initialize both JavaScript and CSS coverage tracking. This must run **before** navigation.

```javascript
browser_run_code({
  code: `async (page) => {
    const client = await page.context().newCDPSession(page);
    page.__cdpClient = client;

    // Enable profiler for JS coverage
    await client.send('Profiler.enable');
    await client.send('Profiler.startPreciseCoverage', {
      callCount: false,
      detailed: true
    });

    // Enable CSS coverage
    await client.send('CSS.enable');
    await client.send('CSS.startRuleUsageTracking');

    return 'JS and CSS coverage tracking started';
  }`
})
```

### Step 2: Navigate to the Target Page

```
browser_navigate({ url: "https://example.com/page" })
```

Wait for the page to be fully loaded. Use `networkidle` state for the most accurate measurement:

```javascript
browser_run_code({
  code: `async (page) => {
    await page.waitForLoadState('networkidle');
    return 'Page loaded (networkidle)';
  }`
})
```

### Step 3: Collect JS and CSS Coverage Data

Use `browser_run_code` to stop coverage tracking and retrieve the raw data.

```javascript
browser_run_code({
  code: `async (page) => {
    const client = page.__cdpClient;

    // Collect JS coverage
    const jsCoverage = await client.send('Profiler.takePreciseCoverage');
    await client.send('Profiler.stopPreciseCoverage');
    await client.send('Profiler.disable');

    // Collect CSS coverage
    const cssCoverage = await client.send('CSS.stopRuleUsageTracking');

    // Calculate JS usage
    let totalJSBytes = 0;
    let usedJSBytes = 0;

    jsCoverage.result.forEach(script => {
      const scriptLength = script.end || 0;
      totalJSBytes += scriptLength;
      script.functions.forEach(fn => {
        fn.ranges.forEach(range => {
          if (range.count > 0) {
            usedJSBytes += (range.endOffset - range.startOffset);
          }
        });
      });
    });

    const jsUnusedPercent = totalJSBytes > 0
      ? Math.round(((totalJSBytes - usedJSBytes) / totalJSBytes) * 1000) / 10
      : 0;

    // Calculate CSS usage
    const totalCSSRules = cssCoverage.ruleUsage.length;
    const usedCSSRules = cssCoverage.ruleUsage.filter(r => r.used).length;
    const cssUnusedPercent = totalCSSRules > 0
      ? Math.round(((totalCSSRules - usedCSSRules) / totalCSSRules) * 1000) / 10
      : 0;

    // Store for later use
    await page.evaluate((data) => {
      window.__coverageData = data;
    }, {
      jsUnusedPercent,
      cssUnusedPercent,
      totalJSBytes,
      usedJSBytes,
      totalCSSRules,
      usedCSSRules
    });

    return {
      jsUnusedPercent,
      cssUnusedPercent,
      totalJSKB: Math.round(totalJSBytes / 1024),
      usedJSKB: Math.round(usedJSBytes / 1024),
      totalCSSRules,
      usedCSSRules
    };
  }`
})
```

### Step 4: Collect Timing and Resource Data

Use `browser_evaluate` to extract Resource Timing, Navigation Timing, and render-blocking resource information.

```javascript
browser_evaluate({
  function: `() => {
    // Navigation Timing
    const navEntry = performance.getEntriesByType('navigation')[0] || {};
    const ttfb = Math.round(navEntry.responseStart - navEntry.requestStart) || 0;
    const domContentLoaded = Math.round(navEntry.domContentLoadedEventEnd - navEntry.startTime) || 0;
    const loadEventEnd = Math.round(navEntry.loadEventEnd - navEntry.startTime) || 0;
    const domInteractive = Math.round(navEntry.domInteractive - navEntry.startTime) || 0;

    // Resource Timing
    const resources = performance.getEntriesByType('resource');
    let totalTransferBytes = 0;
    let renderBlockingCount = 0;
    const renderBlockingResources = [];

    resources.forEach(r => {
      totalTransferBytes += (r.transferSize || 0);

      // Check renderBlockingStatus (Chromium 105+)
      if (r.renderBlockingStatus === 'blocking') {
        renderBlockingCount++;
        renderBlockingResources.push({
          url: r.name.split('/').pop().split('?')[0].substring(0, 60),
          type: r.initiatorType,
          transferSizeKB: Math.round((r.transferSize || 0) / 1024 * 10) / 10,
          durationMs: Math.round(r.duration)
        });
      }
    });

    const totalTransferKB = Math.round(totalTransferBytes / 1024 * 10) / 10;

    return {
      timing: {
        ttfb: ttfb,
        domContentLoaded: domContentLoaded,
        domInteractive: domInteractive,
        loadEventEnd: loadEventEnd
      },
      transfer: {
        totalTransferKB: totalTransferKB,
        totalTransferMB: Math.round(totalTransferKB / 1024 * 100) / 100,
        resourceCount: resources.length
      },
      renderBlocking: {
        count: renderBlockingCount,
        resources: renderBlockingResources
      }
    };
  }`
})
```

### Step 5: Collect Network Request Count

Use `browser_network_requests` to get the total number of HTTP requests.

```
browser_network_requests({ includeStatic: true })
```

### Step 6: Compute the Composite Score

Use `browser_evaluate` to calculate the weighted composite score using all collected data. Pass the coverage data and resource data as needed.

```javascript
browser_evaluate({
  function: `() => {
    const coverage = window.__coverageData || {};

    // Navigation Timing
    const navEntry = performance.getEntriesByType('navigation')[0] || {};
    const ttfb = Math.round(navEntry.responseStart - navEntry.requestStart) || 0;
    const domContentLoaded = Math.round(navEntry.domContentLoadedEventEnd - navEntry.startTime) || 0;

    // Resources
    const resources = performance.getEntriesByType('resource');
    let totalTransferBytes = 0;
    let renderBlockingCount = 0;
    resources.forEach(r => {
      totalTransferBytes += (r.transferSize |

Related in Image & Video