Trong các hệ thống quản lý nghiệp vụ, dashboard thống kê gần như là một phần không thể thiếu. Nó giúp các cấp quản lý theo dõi doanh thu, tình hình hoạt động và các chỉ số quan trọng của hệ thống.
Tôi từng nhận một task mở rộng một dashboard đã tồn tại từ trước, chủ yếu là bổ sung thêm doanh thu từ các feature được triển khai sau này.
Khi bắt đầu tìm hiểu và triển khai, một số vấn đề dần xuất hiện:
API bắt đầu có latency cao.
Dữ liệu nằm ở nhiều table có relation với nhau.
Khối lượng dữ liệu lớn hơn trước.
Dashboard chứa nhiều nhóm thông tin như KPI, chart, table và heatmap.
Các công thức cũ đã được sử dụng ổn định trong production và không phù hợp để thay đổi lớn.
Trong bối cảnh đó, để tối ưu thời gian triển khai và hạn chế rủi ro, tôi chủ yếu bổ sung logic mới rồi group với logic hiện tại thay vì viết lại toàn bộ cách dữ liệu được hình thành.
Điều này tạo ra một constraint khá rõ:
Tôi không thể can thiệp quá sâu vào data formation layer, vì vậy phạm vi tối ưu chủ yếu nằm ở xung quanh và sau quá trình computation hiện tại.
1. Đừng compute lại những gì đã biết
Giải pháp dễ nghĩ đến nhất là cache kết quả ở API layer.
Flow cơ bản:
Request
↓
Check cache
↓
Cache hit ─────────→ Return
↓
Cache miss
↓
Compute
↓
Store cache
↓
ReturnƯu điểm là dễ triển khai, ít thay đổi architecture và chỉ những request thực sự được sử dụng mới chiếm cache.
Tuy nhiên, cache không giải quyết hoàn toàn latency.
Nếu cache miss:
Cache hit → 50ms
Cache miss → 5srequest đó vẫn phải chịu toàn bộ thời gian computation.
Một vấn đề khác là cache stampede.
Nếu cache vừa hết hạn và 20 request cùng vào:
20 requests = 20 expensive computationsCó thể sử dụng request deduplication hoặc single-flight để các request giống nhau share cùng một computation:
┌─ R1 compute
R2 ────────┤
R3 ────────┤ await same result
...
R20 ───────┘Kết quả:
20 requests = 1 computation + 19 waiters
Như vậy cache và single-flight thường nên được nhìn cùng nhau: cache giúp tránh computation giữa các request khác thời điểm, còn single-flight giúp tránh duplicated computation khi nhiều request cùng cache miss.
2. Pre-computation cho fixed range và dynamic range
Cache vẫn phụ thuộc vào request đầu tiên.
Với những dữ liệu có tính predictable cao, có thể compute trước.
Ví dụ dashboard có các preset:
Last 24 hours
Last 7 days
Last month
Last 3 months
Last year
Thay vì:
User request
↓
Compute
↓
Returncó thể:
Scheduler
↓
Compute
↓
Store result
↓
User request
↓
Read resultCách này phù hợp với các report nặng nhưng range thời gian cố định.
Dynamic range
Khó hơn là trường hợp user chọn time range bằng hai datetime picker.
Ví dụ:
2026-01-17 → 2026-09-09Không thể precompute mọi combination có thể xảy ra.
Một hướng khác là tạo aggregated building blocks theo ngày, tuần hoặc tháng.
Ví dụ:
partial January
+
February
+
March
+
April
+
May
+
June
+
July
+
August
+
partial September
Thay vì compute lại toàn bộ 8 tháng, hệ thống chỉ cần reuse các block đầy đủ và compute phần dư ở hai đầu.
Tuy nhiên không phải metric nào cũng merge đơn giản.
Các metric như:
SUM(revenue)
SUM(order_count)có thể cộng trực tiếp.
Nhưng các metric như:
AVG
COUNT DISTINCT
median
p95
conversion ratecần cách aggregate riêng.
Ví dụ với AVG, thay vì chỉ lưu average của từng block, nên lưu:
sum
countsau đó:
globalAverage =
totalSum / totalCountĐây là điểm quan trọng khi dùng aggregated building blocks: cần biết metric nào thực sự composable.
3. Không để workload chậm nhất khóa toàn bộ dashboard
Một dashboard thường có nhiều computation độc lập:
Revenue : 1.5s
Orders : 2.0s
Users : 1.0s
Heatmap : 5.0s
Nếu chạy tuần tự:
1.5 + 2 + 1 + 5 ≈ 9.5sNếu chúng không phụ thuộc nhau, có thể chạy song song.
Ví dụ TypeScript:
const [
revenue,
orders,
users,
heatmap,
] = await Promise.all([
getRevenue(),
getOrders(),
getUsers(),
getHeatmap(),
]);Thời gian có thể tiến gần workload chậm nhất: ≈ 5s
Tuy nhiên concurrency không nên hiểu là càng nhiều càng tốt. Nếu chạy đồng thời quá nhiều database query, hệ thống có thể gặp:
Connection pool exhausted
CPU spike
Database overload
Ngoài ra, không phải lúc nào cũng cần gom tất cả dữ liệu dashboard vào một API.
Ví dụ:
KPI 600ms
Revenue 1.2s
Product 2s
Heatmap 7sNếu tất cả nằm trong:
GET /dashboarduser có thể phải chờ gần 7 giây mới thấy toàn bộ trang.
Có thể chia thành:
GET /dashboard/overview
GET /dashboard/revenue
GET /dashboard/products
GET /dashboard/heatmapFrontend lúc này có thể render dần:
0.6s → KPI
1.2s → Revenue
2.0s → Product
7.0s → HeatmapĐiều này không nhất thiết làm heatmap nhanh hơn, nhưng cải thiện đáng kể perceived latency.
4. Khi bottleneck không còn nằm ở backend compute
Đôi khi backend đã xử lý nhanh nhưng dashboard vẫn chậm.
Ví dụ:
Server compute: 300ms
Response transfer: 2.5s
Browser processing: 1sLúc này tối ưu query thêm vài chục mili giây gần như không tạo khác biệt đáng kể.
Cần nhìn toàn bộ flow:
Database
↓
Backend compute
↓
Serialization
↓
Network
↓
Browser
↓
RenderMột số hướng có thể áp dụng:
Cache hierarchy
Client memory
↓
Browser / HTTP cache
↓
API cache
↓
Pre-computed result
↓
Source computationRequest càng được resolve ở layer gần client thì càng tránh được nhiều expensive operation phía sau.
Client-side server-state cache
Các thư viện như TanStack Query hoặc SWR có thể giúp reuse response trên browser.
Ví dụ user đi:
Dashboard
→ Detail
→ Back Dashboardthay vì fetch lại toàn bộ dữ liệu, có thể reuse cached result và background revalidate.
Payload optimization
Không nên gửi nhiều dữ liệu hơn mức UI cần.
Nếu chart chỉ cần:
{
"date": "2026-09",
"revenue": 5000
}thì không cần gửi thêm relation, metadata, audit logs hoặc các field không được sử dụng.
Tương tự, không nên gửi hàng triệu raw data points nếu chart cuối cùng chỉ có thể hiển thị vài trăm hoặc vài nghìn điểm.
Có thể aggregate trước:
raw events
↓
hourly/daily buckets
↓
chartTable lớn cũng nên sử dụng pagination hoặc lazy loading thay vì load toàn bộ dữ liệu một lần.
5. Khi workload quá nặng cho synchronous request
Một số tác vụ đơn giản là không phù hợp để user giữ HTTP request và chờ.
Ví dụ:
Export report 2 years → 30 secondsThay vì:
Client
↓
wait 30s
↓
downloadcó thể chuyển sang background job:
POST /exports
↓
202 Accepted
↓
return jobIdWorker xử lý:
Aggregate
↓
Generate file
↓
Store file
↓
Notify userClient có thể nhận trạng thái qua:
Polling
WebSocket
SSE
System notification
EmailTrong trường hợp này, optimization không nhất thiết là biến 30s thành 3s.
Nó có thể đơn giản là:
Không bắt user giữ một synchronous HTTP request trong 30 giây.
6. Measurement và failure handling
Performance optimization không nên chỉ dựa trên cảm giác.
Thay vì:
API này có vẻ chậm.
nên đo:
p50
p95
p99
cache hit rate
response size
DB time
aggregation time
serialization timeVí dụ:
Before
p50: 1.8s
p95: 7.4s
p99: 11.2s
response size: 2.8 MBSau optimization:
After
p50: 250ms
p95: 1.8s
p99: 6.5s
cache hit rate: 82%
response size: 620 KBNhìn vào đây có thể thấy average và p50 đã tốt hơn nhiều, nhưng p99 vẫn còn cao. Điều đó cho thấy cache miss hoặc một workload đặc biệt vẫn đang là bottleneck.
Ngoài happy path cũng cần quan tâm đến failure path.
Ví dụ Redis down.
Có thể fallback:
Redis error
↓
compute from source
↓
returnNhưng nếu 100 request cùng fallback:
100 requests
↓
100 expensive computations
↓
database overloadDo đó failure handling không chỉ là try/catch.
Có thể cần:
Single flight
Rate limiting
Stale cache
Circuit breaker
Với pre-computation cũng nên giữ result hợp lệ gần nhất:
Current result: v42
↓
Compute v43
↓
Success?
├─ yes → switch to v43
└─ no → continue serving v42
Không nên xóa dữ liệu cũ trước khi computation mới thành công.
Với background jobs, nên có lifecycle rõ ràng:
PENDING
↓
PROCESSING
↓
COMPLETED / FAILED
và xử lý retry/idempotency để một job retry không tạo file hoặc notification trùng lặp.
7. Khi nào cần thay đổi kiến trúc lớn hơn?
Các giải pháp như:
Materialized View
PostgreSQL aggregate tables
TimescaleDB
ClickHouse
Elasticsearch
BigQuery
Data Warehouseđều có thể phù hợp với analytical workload.
Nhưng không phải task nào cũng cần đi thẳng đến những giải pháp này.
Có thể chia thành hai mức.
Tactical optimization
cache
single-flight
pre-computation
parallel computation
split API
payload optimization
frontend cache
background jobƯu điểm:
reuse existing logic
implementation cost thấp hơn
migration risk thấp
delivery nhanh hơnArchitectural optimization
Materialized View
Aggregate tables
ClickHouse
TimescaleDB
Data Warehouse
BigQuery
Những solution này có thể scale tốt hơn cho analytics, nhưng đi kèm:
Migration
Data pipeline
Sync/invalidation
Infrastructure
Monitoring
Operational complexity
Vì vậy câu hỏi không nên chỉ là:
Công nghệ nào nhanh hơn?
Mà nên là:
Mức độ complexity nào thực sự phù hợp với bottleneck và constraint hiện tại?
Kết luận
Với một dashboard có latency cao, tôi thường nghĩ theo thứ tự:
Có đang compute lại cùng một thứ không?
↓
Có thể cache hoặc precompute không?
↓
Dynamic range có thể compose từ reusable blocks không?
↓
Có computation độc lập nào đang chạy tuần tự không?
↓
Một workload chậm có đang block toàn UI không?
↓
Payload có lớn hơn mức UI cần không?
↓
Task có thực sự nên chạy synchronous không?
↓
Optimization có được đo bằng metric không?
↓
Nếu cache hoặc worker fail thì hệ thống phản ứng thế nào?Trong trường hợp của tôi, constraint chính là không thể thay đổi mạnh cách dữ liệu được hình thành.
Vì vậy thay vì rewrite data layer ngay lập tức, hướng tiếp cận hợp lý hơn là:
Reuse trước khi recompute
Precompute phần predictable
Compose dynamic ranges khi có thể
Parallelize workload độc lập
Không để slow workload block cả UI
Giảm unnecessary data transfer
Đưa long-running work sang background
Đo trước và sau optimization
Luôn tính đến failure path
Một giải pháp tốt không nhất thiết là giải pháp phức tạp nhất.
Nó là giải pháp xử lý đúng bottleneck với mức implementation cost và architectural complexity phù hợp với hệ thống hiện tại.




