Build Performance Metrics

Stati includes a built-in performance metrics system that helps you understand and optimize your site’s build performance. This guide explains how to use it.

Quick Start

Enable metrics collection with the --metrics flag:

stati build --metrics

This will:

  1. Collect timing data for all build phases
  2. Track cache hit/miss rates
  3. Save a detailed JSON file to .stati/metrics/

CLI Options

--metrics

Enable metrics collection. Can also be enabled via the STATI_METRICS=1 environment variable.

# Via flag
stati build --metrics

# Via environment variable
STATI_METRICS=1 stati build

--metrics-file <path>

Specify a custom output path for the metrics JSON file. The path is relative to .stati/metrics/:

# Writes to .stati/metrics/my-metrics.json
stati build --metrics --metrics-file my-metrics.json

# Writes to .stati/metrics/reports/build.json
stati build --metrics --metrics-file reports/build.json

--metrics-detailed

Include per-page timing information in the output:

stati build --metrics --metrics-detailed

--metrics-html

Generate an interactive HTML report alongside the JSON file:

stati build --metrics --metrics-html

The HTML report provides:

  • Visual summary cards for key metrics
  • Interactive phase breakdown with click-to-expand descriptions
  • Timeline view showing phases in execution order
  • ISG cache performance visualization
  • Per-page timing waterfall chart (when --metrics-detailed is also used)
  • Embedded JSON data for debugging

Stati Build Metrics HTML Report

You can combine all flags for the most comprehensive output:

stati build --metrics --metrics-detailed --metrics-html

Understanding the Output

JSON Output

Metrics are saved to .stati/metrics/build-<timestamp>.json:

{
  "schemaVersion": "1",
  "meta": {
    "timestamp": "2024-01-15T10:30:00.000Z",
    "ci": false,
    "nodeVersion": "22.0.0",
    "platform": "darwin",
    "cliVersion": "1.11.0",
    "coreVersion": "1.18.0",
    "command": "build",
    "flags": {
      "force": false,
      "clean": false
    }
  },
  "totals": {
    "durationMs": 1250,
    "peakRssBytes": 105381888,
    "heapUsedBytes": 52428800
  },
  "phases": {
    "configLoadMs": 50,
    "contentDiscoveryMs": 100,
    "navigationBuildMs": 25,
    "cacheManifestLoadMs": 10,
    "pageRenderingMs": 800,
    "assetCopyMs": 200,
    "cacheManifestSaveMs": 15
  },
  "counts": {
    "totalPages": 20,
    "renderedPages": 5,
    "cachedPages": 15,
    "assetsCopied": 10,
    "templatesLoaded": 18,
    "markdownFilesProcessed": 20
  },
  "isg": {
    "enabled": true,
    "cacheHitRate": 0.75,
    "manifestEntries": 20,
    "invalidatedEntries": 5
  }
}

Key Metrics Explained

Cache Hit Rate

The cache hit rate (isg.cacheHitRate) shows what percentage of pages were served from cache without re-rendering:

  • 100%: All pages were cached (no changes detected)
  • 0%: Full rebuild (all pages rendered)
  • 50-90%: Incremental build (some pages changed)

A consistently low cache hit rate might indicate:

  • ISG is disabled
  • Content files are being modified unnecessarily
  • Template changes are triggering full rebuilds

Phase Breakdown

The phases object shows how long each build phase took:

Phase Description
configLoadMs Loading and parsing stati.config.ts
contentDiscoveryMs Finding and parsing markdown files
navigationBuildMs Building navigation tree from pages
cacheManifestLoadMs Loading ISG cache manifest
typescriptCompileMs Compiling TypeScript bundles
pageRenderingMs Rendering pages (markdown + templates)
shouldRebuildTotalMs Aggregate time checking if pages need rebuild
renderPageTotalMs Aggregate time rendering pages
fileWriteTotalMs Aggregate time writing HTML files to disk
cacheEntryTotalMs Aggregate time updating cache entries
searchIndexGenerationMs Generating search index JSON
searchIndexWriteMs Writing search index to disk
assetCopyMs Copying static assets
cacheManifestSaveMs Saving updated cache manifest
sitemapGenerationMs Generating sitemap.xml
rssGenerationMs Generating RSS feeds
tailwindInitMs Tailwind detection and inventory initialization
tailwindInventoryMs Writing Tailwind inventory at end of build
getDirectorySizeMs Calculating directory size for build stats
lockAcquireMs Build lock acquisition
lockReleaseMs Build lock release
hookBeforeAllMs Time spent in beforeAll hook (if configured)
hookAfterAllMs Time spent in afterAll hook (if configured)
hookBeforeRenderTotalMs Total time spent in beforeRender hooks across all pages
hookAfterRenderTotalMs Total time spent in afterRender hooks across all pages

Note: Not all phases appear in every build. Only phases that actually run will be included in the metrics output. For example, hook timings are only recorded when the corresponding hooks are defined in your stati.config.ts, and TypeScript compilation only runs when TypeScript bundles are configured.

Counts

The counts object tracks quantities processed during the build:

Counter Description
totalPages Total number of pages discovered
renderedPages Pages that were rendered (cache misses)
cachedPages Pages served from cache (cache hits)
assetsCopied Number of static assets copied
templatesLoaded Total templates loaded across all rendered pages
markdownFilesProcessed Number of markdown files processed

Templates Loaded

The templatesLoaded counter tracks the total number of Eta templates (layouts and partials) loaded during the build. This is an accumulated total across all rendered pages - if 5 pages each load 4 templates, templatesLoaded will be 20.

When using --metrics-detailed, per-page template counts are also available in pageTimings:

{
  "pageTimings": [
    { "url": "/docs/intro", "durationMs": 75, "cached": false, "templatesLoaded": 4 },
    { "url": "/docs/guide", "durationMs": 60, "cached": false, "templatesLoaded": 4 },
    { "url": "/about", "durationMs": 0, "cached": true }
  ]
}

Note that cached pages don’t have a templatesLoaded property since no templates are rendered for them.

Memory Usage

  • peakRssBytes: Maximum resident set size during build
  • heapUsedBytes: V8 heap usage at the end of the build

High memory usage might indicate:

  • Many large pages
  • Memory-intensive templates
  • Large static assets being processed

Using Metrics in CI

Enable metrics in CI to track build performance over time:

# .github/workflows/ci.yml
- name: Build with metrics
  run: stati build --metrics
  env:
    STATI_METRICS: '1'

- name: Upload metrics
  uses: actions/upload-artifact@v4
  with:
    name: build-metrics
    path: .stati/metrics/

Regression Detection

Compare metrics between builds to detect regressions:

# Get baseline
stati build --metrics --metrics-file baseline.json

# Make changes...

# Compare
stati build --metrics --metrics-file current.json

# Use jq to compare
jq -r '.totals.durationMs' baseline.json current.json

Programmatic Access

Access metrics programmatically in Node.js:

import { build } from '@stati/core';
import type { BuildResult, BuildMetrics } from '@stati/core';

const result: BuildResult = await build({
  metrics: { enabled: true, detailed: true }
});

if (result.buildMetrics) {
  const metrics: BuildMetrics = result.buildMetrics;
  console.log(`Build took ${metrics.totals.durationMs}ms`);
  console.log(`Cache hit rate: ${metrics.isg.cacheHitRate * 100}%`);

  // Per-page timings (when detailed: true)
  if (metrics.pageTimings) {
    for (const page of metrics.pageTimings) {
      if (page.cached) {
        console.log(`${page.url}: cached`);
      } else {
        // templatesLoaded shows partials + layout loaded for this page
        console.log(`${page.url}: ${page.durationMs}ms, ${page.templatesLoaded} templates`);
      }
    }
  }
}