gstack Benchmark: Performance Testing Before Every PR

gstack Benchmark runs Core Web Vitals, bundle size checks, and load time analysis before every PR. Catch regressions before they hit production.

Mark Rachapoom
Mark Rachapoom
·6 min read
gstack Benchmark: Performance Testing Before Every PR

Performance regressions are insidious. They rarely break anything — the application still works, tests still pass, CI is still green. But over time, small degradations compound. The bundle size grew by 50KB. The main page load time increased by 400ms. The largest contentful paint went from 1.2s to 2.8s. Each individual change seemed fine; the cumulative effect is a significantly worse user experience.

gstack's Benchmark phase measures performance before every PR, establishes baselines, and blocks releases that introduce significant regressions. Performance is a feature — and features need tests.

What gstack Benchmark Measures#

The Benchmark phase measures four categories of performance:

1. Bundle Size#

  • Total JavaScript bundle size (initial load + all chunks)
  • Tree-shaking effectiveness: are unused dependencies being included?
  • Per-route bundle size for code-split applications
  • CSS bundle size
  • Image sizes (uncompressed, with format recommendations)

2. Core Web Vitals#

Following Google's measurement framework:

LCP (Largest Contentful Paint): How long until the main content is visible?

  • Excellent: <2.5s
  • Needs Improvement: 2.5s - 4.0s
  • Poor: >4.0s

FID/INP (First Input Delay / Interaction to Next Paint): How responsive is the UI to user interaction?

  • Excellent: <100ms (FID) / <200ms (INP)
  • Needs Improvement: 100-300ms (FID) / 200-500ms (INP)
  • Poor: >300ms (FID) / >500ms (INP)

CLS (Cumulative Layout Shift): Does the layout jump around as content loads?

  • Excellent: <0.1
  • Needs Improvement: 0.1 - 0.25
  • Poor: >0.25

3. Page Load Times#

  • Time to First Byte (TTFB)
  • First Contentful Paint (FCP)
  • Time to Interactive (TTI)
  • Total Blocking Time (TBT)

4. Database Query Performance#

  • Slow queries identified (>100ms)
  • N+1 queries detected
  • Index usage analyzed
  • Query count per page load

Setting Performance Budgets#

The key to making Benchmark useful: performance budgets. Without thresholds, you're just collecting data. With thresholds, you're enforcing standards.

DenchClaw's default performance budgets:

# .gstack/performance-budget.yaml
bundles:
  initial_js: 200KB        # Initial JavaScript bundle
  total_js: 500KB           # Total JavaScript across all chunks
  initial_css: 50KB         # Initial CSS bundle
  total_css: 100KB          # Total CSS
 
core_web_vitals:
  lcp: 2500ms               # Largest Contentful Paint
  inp: 200ms                # Interaction to Next Paint
  cls: 0.1                  # Cumulative Layout Shift
  fcp: 1800ms               # First Contentful Paint
  tti: 3500ms               # Time to Interactive
 
database:
  slow_query_threshold: 100ms
  max_queries_per_page: 20
 
# How much can these change between PRs?
regression_thresholds:
  bundle_size_increase: 10%     # Block if JS bundle grows >10%
  lcp_increase: 20%             # Block if LCP gets >20% slower
  ttfb_increase: 30%            # Block if TTFB gets >30% slower

These budgets are adjustable. The point is that they're explicit. "We decided 200KB initial JS is our budget" is better than "we noticed the bundle is 1.2MB and nobody knows when it got that way."

The Before/After Report#

For every PR that touches client-side code, gstack Benchmark generates a before/after comparison:

Bundle Size Analysis
====================
Initial JS:   178KB → 184KB (+3.4%) ⚠️
Total JS:     410KB → 415KB (+1.2%) ✅
CSS:          42KB → 42KB (no change) ✅

Analysis:
The 6KB increase in initial JS is from importing `date-fns` format functions
in the new calendar widget without tree-shaking. 

Recommendation: Import only the specific functions needed:
  // Instead of:
  import { format, parseISO, addDays } from 'date-fns'
  
  // Use:
  import format from 'date-fns/format'
  import parseISO from 'date-fns/parseISO'
  import addDays from 'date-fns/addDays'

This would reduce the bundle increase to ~1.2KB.

Core Web Vitals (desktop, synthetic test)
==========================================
LCP:   1.8s → 1.9s (+5.6%) ✅ (within budget)
INP:   85ms → 82ms (-3.5%) ✅ (improved!)
CLS:   0.04 → 0.04 (no change) ✅

Database (contacts page, 100 test records)
==========================================
Total queries: 8 → 9 (+1) ⚠️
Slowest query: 45ms → 62ms (+37.8%) ⚠️
N+1 detected: None ✅

New query (62ms): SELECT * FROM v_recent_interactions WHERE contact_id = ?
This runs once per contact in the list. With 100 contacts, this would be
100 queries on the contacts list page.

Recommendation: Add this data to the contacts PIVOT view or use a JOIN
to fetch all recent interactions in a single query.

This report is attached to the PR automatically. Code reviewers see the performance impact alongside the code diff.

Catching Performance Regressions in CI#

gstack Benchmark runs as part of CI on every PR. The regression thresholds are enforced:

  • Bundle size increase >10%: CI fails with analysis and recommendations
  • LCP increase >20%: CI fails with specific page and cause
  • New N+1 query detected: CI fails with the query and optimization suggestion

This keeps performance from drifting. When a PR introduces a regression, it gets caught before merge, when the fix is easiest.

Historical Performance Tracking#

Beyond per-PR benchmarks, gstack tracks performance trends over time.

The trend chart shows:

  • Bundle sizes over the last 90 days (are they growing?)
  • LCP by page over the last 90 days (are pages getting slower?)
  • Database query count trends (are we adding queries to pages?)

Trend analysis catches gradual degradation that would pass individual PR thresholds but represents a real problem when viewed holistically.

Performance Optimization Recommendations#

When gstack detects a performance issue, it doesn't just report the problem — it recommends specific optimizations:

For bundle size issues:

  • Which specific imports are adding the most size
  • Tree-shaking opportunities
  • Code splitting recommendations
  • Dynamic import opportunities for below-the-fold features

For rendering performance:

  • Specific components with unnecessary re-renders
  • Missing React.memo or useMemo on expensive computations
  • Image optimization opportunities (format, size, lazy loading)
  • CSS animation performance (will-change, transform vs. position)

For database performance:

  • Index recommendations for slow queries
  • N+1 elimination patterns
  • Query consolidation opportunities
  • Caching opportunities for frequently-run identical queries

Frequently Asked Questions#

How does gstack Benchmark run performance tests without real users?#

Synthetic testing: it uses a headless browser (Playwright or Puppeteer) to load pages against a test environment and measure metrics. This doesn't replace real-user monitoring, but it's reproducible, baseline-able, and can run on every PR.

Should performance budgets be the same for all pages?#

No. Marketing landing pages need <1.5s LCP to maximize conversion. Internal admin pages can tolerate slightly higher LCP. Dashboard pages should have excellent INP since they're highly interactive. Set budgets appropriate for each page's user context.

How do you handle performance regressions introduced before you started benchmarking?#

Establish the current state as baseline without enforcing budgets. Run benchmarks for 2-4 weeks to understand the actual baseline. Then set budgets at slightly above the observed baseline and enforce regressions. Separately, create a performance improvement roadmap for known issues.

What's the most common cause of JavaScript bundle growth?#

Adding new npm packages without checking their bundle impact first. Use bundlephobia.com before adding any new dependency. Many "small" utilities include large polyfills or peer dependencies. A 500-byte utility can add 50KB to your bundle.

Can gstack Benchmark replace Lighthouse CI?#

Largely yes. gstack uses similar underlying measurement approaches. The difference: gstack Benchmark integrates with the full gstack workflow, includes database performance measurement, and generates AI-powered recommendations. For teams already in the DenchClaw ecosystem, gstack Benchmark is the simpler path.

Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →

Related articles

Keep reading

View all