Full-Stack

Analytics Dashboard

Dashboard demoed perfectly. Production performance unacceptable.

Full-Stack
Performance
ClientSaaS Corp
Year2023

Overview

A B2B analytics platform had built a sleek, feature-rich dashboard. In demos with sample data, it was snappy and impressive. But in production, with real customer data, it was unusable. Initial page load took 12+ seconds. Interactions triggered full-page freezes. Customers were churning specifically because of dashboard performance.

The engineering team had tried various fixes—caching layers, query optimization, CDN setup—but nothing moved the needle significantly. They needed a systematic approach.

The Problem

Surface-level optimizations had missed the real issues:

  • N+1 Query Explosions: GraphQL resolvers triggering hundreds of database queries per request
  • Render Blocking: Entire dashboard waiting for slowest widget, not streaming
  • Memory Leaks: 30-minute sessions consuming 2GB+ browser memory
  • Unoptimized Aggregations: Real-time recalculating metrics that rarely changed
  • Bundle Bloat: 4.2MB JavaScript bundle blocking first paint

System Architecture

The performance problems spanned the entire stack:

Issues at Each Layer

Before: Monolithic Approach

User Request

Single GraphQL Query

N+1 Database Calls

Full Data Load

Client Render All

12+ Second Load

Unoptimized Query Patterns

No Dataloader

No Streaming

Render Blocking

Redesigned Architecture

The solution required changes at every layer:

Client Layer

Compute Layer

Data Layer

API Layer

Request Layer

Cache Miss

Analytics

Cache Hit

User Request

Route-Level Code Split

Critical Path Identifier

GraphQL Gateway

DataLoader Batching

Query Complexity Limiter

Redis Query Cache

Primary: PostgreSQL

Analytics: ClickHouse

Cache: Redis

Materialized Views

Pre-Aggregation Jobs

Incremental Updates

Background Workers

Streaming Render

Virtualized Lists

Lazy Widgets

Memory Manager

Performance Optimization Flow

The new request flow prioritizes perceived performance:

DatabaseRedisDataLoaderGraphQLClient AppUserDatabaseRedisDataLoaderGraphQLClient AppUserpar[Critical Path First][Above Fold Widgets]User seesinteractivedashboardalt[Pre-computed Available][Compute Required]par[Background Loading][Prefetch Next Views]Full dashboard inunder 1sOpen Dashboard1Request Header Data2Check Cache3Cache Hit4Header Data (50ms)5Render HeaderImmediately6Request Primary Widgets7Batch Widget Queries8Single Batched Query9Combined Results10Resolved Data11Primary Widgets (200ms)12Render Primary Area13Request Secondary Widgets14Check Aggregation Cache15Materialized Data16Fast Response17Aggregation Query18Raw Data19Cache Result20Computed Response21Render as Available(Streaming)22Prefetch Likely Next Data23Warm Cache24

Widget Optimization Strategy

Different widgets required different optimization approaches:

Real-time Strategy

Chart Strategy

Table Strategy

KPI Strategy

Widget Types

KPI Cards

Data Tables

Charts

Real-time

Pre-aggregate Daily

Incremental Updates

5-min Cache TTL

Server Pagination

Virtual Scrolling

Lazy Cell Render

Data Sampling

Canvas Rendering

Worker Threads

WebSocket Diffs

Throttled Updates

Priority Queuing

The Solution

Phase 1: Profiling & Diagnosis (Week 1)

Systematic performance profiling revealed the real bottlenecks:

LayerIssueImpact
DatabaseN+1 queries, missing indexes60% of latency
APINo batching, no caching20% of latency
ClientRender blocking, memory leaks15% of latency
NetworkBundle size, no compression5% of latency

Phase 2: Backend Optimization (Week 2-3)

  • Implemented DataLoader for automatic query batching
  • Added materialized views for common aggregations
  • Set up Redis caching with smart invalidation
  • Migrated heavy analytics to ClickHouse

Phase 3: Frontend Optimization (Week 3-4)

  • Code-split by route and widget
  • Implemented streaming SSR with React 18
  • Added virtualization for large data sets
  • Built memory management with cleanup hooks

Phase 4: Infrastructure & Monitoring (Week 5)

  • Configured CDN with edge caching
  • Set up real-user monitoring (RUM)
  • Created performance budgets with CI checks
  • Built anomaly alerting for regressions

Results

The dashboard transformed from liability to selling point:

MetricBeforeAfterChange
Initial Load12.4s0.9s-93%
Time to Interactive15.2s1.2s-92%
Largest Contentful Paint11.8s0.8s-93%
Memory (30-min session)2.1GB180MB-91%
Bundle Size4.2MB420KB-90%
Database Queries/Request34712-97%

Customer churn citing "performance" dropped from 23% to under 2%.

Technical Stack

ComponentTechnology
FrontendReact 18, Next.js
State ManagementTanStack Query (aggressive caching)
VisualizationD3.js, Canvas (not SVG)
APIGraphQL, Apollo Server
BatchingDataLoader
Primary DBPostgreSQL
Analytics DBClickHouse
CacheRedis, CDN (Cloudflare)
MonitoringDatadog RUM, Lighthouse CI
BuildWebpack, Bundle Analyzer

Key Learnings

  1. Profile before optimizing: Intuition about bottlenecks is often wrong—measure first
  2. N+1 is the silent killer: GraphQL makes N+1 easy to create and hard to spot
  3. Perceived performance matters: Users care about time-to-interactive, not total load time
  4. Memory leaks accumulate: Performance testing needs to include long-running sessions
  5. Performance is a feature: After optimization, "speed" became a key differentiator in sales calls

More Work

Other projects in Full-Stack

View All Work