Tối ưu hoá hiệu năng render danh mục hàng nghìn sản phẩm (Virtual Scrolling)
Kỹ thuật + nghiệp vụ – “cầm lên làm được ngay”
⚡ Mục tiêu: Giảm bộ nhớ tiêu thụ và thời gian phản hồi khi người dùng duyệt danh mục >10 000 SKU trên web‑shop lớn (tháng 100‑1 000 tỷ VNĐ).
1. Tổng quan kiến trúc & workflow vận hành
┌─────────────────────┐ ┌─────────────────────┐
│ Frontend (React) │─────►│ Virtual Scroller │
│ (SSR + CSR) │ │ (react‑window) │
└─────────┬───────────┘ └───────┬─────────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ API Gateway (NGX) │─────►│ GraphQL Server │
│ (Cache, Rate‑lim) │ │ (Apollo) │
└─────────┬───────────┘ └───────┬─────────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Service Mesh (Istio)│ │ Product Service │
│ (Circuit‑breaker) │ │ (Node/Medusa) │
└─────────┬───────────┘ └───────┬─────────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Redis Cache (LRU) │◄─────│ DB (PostgreSQL) │
└─────────────────────┘ └─────────────────────┘
- Frontend chỉ yêu cầu dữ liệu “cửa sổ” hiện tại (ví dụ 30 item).
- API Gateway thực hiện cache HTTP 200 ms, giảm tải cho GraphQL.
- Redis LRU giữ các page dữ liệu gần nhất, tự động expire sau 5 phút.
2. Lý do chọn Virtual Scrolling
| KPI | Trước Virtual Scroll | Sau Virtual Scroll | Độ tăng (%) |
|---|---|---|---|
| Memory (Browser) | ~250 MB (tất cả SKU) | ~30 MB (cửa sổ 30 item) | ‑88% |
| TTI (Time to Interactive) | 3,2 s | 1,1 s | ‑66% |
| Server QPS | 1 200 req/s | 2 800 req/s (cache hit) | +133% |
| Bounce Rate (Shopify 2025) | 45 % | 32 % | ‑13 % |
Statista 2024 báo cáo: trên 60 % người dùng rời trang nếu TTI > 3 s.
3. Lựa chọn công nghệ (Tech Stack Comparison)
| # | Stack | Frontend | Backend | Cache | CDN | Độ phức tạp | Chi phí (USD/tháng) | Đánh giá Gartner 2024 |
|---|---|---|---|---|---|---|---|---|
| 1 | React + react‑window + Apollo | React 18 | Node + Medusa | Redis (cluster) | Cloudflare | Trung bình | $1 200 | ★★★★ |
| 2 | Vue 3 + vue‑virtual‑scroller + GraphQL | Vue 3 | Go (gqlgen) | Redis (single) | Fastly | Thấp | $950 | ★★★ |
| 3 | Angular + CDK Virtual Scroll | Angular 16 | .NET Core | Azure Cache for Redis | Azure CDN | Cao | $1 500 | ★★★★ |
| 4 | SvelteKit + TanStack Virtual | Svelte | Rust (Actix) | Memcached | CloudFront | Thấp | $800 | ★★★ |
⚡ Khuyến nghị: Stack #1 (React + Medusa) phù hợp với hầu hết các dự án e‑Commerce quy mô 100‑1 000 tỷ VNĐ vì sự đồng bộ giữa GraphQL, plugin Medusa và khả năng mở rộng trên Kubernetes.
4. Các bước triển khai – 7 Phase chi tiết
| Phase | Mục tiêu | Công việc con (6‑12) | Trách nhiệm | Thời gian (tuần) | Dependency |
|---|---|---|---|---|---|
| Phase 1 – Khảo sát & Định dạng dữ liệu | Xác định schema SKU, phân đoạn UI | 1. Thu thập field từ ERP 2. Định nghĩa GraphQL type 3. Thiết kế pagination cursor 4. Đánh giá độ lớn payload 5. Kiểm tra GDPR 6. Lập kế hoạch migration |
BA / Data Engineer | 2 | – |
| Phase 2 – Xây dựng API Gateway & Cache | Cấu hình Nginx + Redis | 1. Docker‑compose Nginx 2. Cấu hình rate‑limit 3. Enable HTTP2 4. Deploy Redis cluster 5. Set TTL 5 phút 6. Kiểm thử load 5 k RPS |
DevOps | 3 | Phase 1 |
| Phase 3 – Phát triển Service Layer | Implement GraphQL + Medusa plugin | 1. Scaffold Medusa service 2. Viết resolver productsByCursor 3. Add Redis cache wrapper 4. Unit test (Jest) 5. CI/CD pipeline (GitHub Actions) 6. Security audit (OWASP) |
Backend Lead | 4 | Phase 2 |
| Phase 4 – Frontend Virtual Scroller | Tích hợp react‑window + Apollo | 1. Cài react-window 2. Tạo component ProductList 3. Hook useInfiniteQuery 4. Skeleton UI 5. Responsive breakpoints 6. E2E test (Cypress) |
Frontend Lead | 3 | Phase 3 |
| Phase 5 – CI/CD & Observability | Đưa vào production pipeline | 1. Docker‑compose multi‑service 2. Helm chart (K8s) 3. Prometheus + Grafana dashboards 4. Loki log aggregation 5. Canary release (Argo Rollouts) 6. Backup Redis snapshots |
DevOps | 2 | Phase 4 |
| Phase 6 – Performance Tuning | Đạt mục tiêu memory & TTI | 1. Load test (k6) 10 k concurrent 2. Adjust Redis LRU size 3. Fine‑tune Nginx buffer 4. Enable Brotli compression 5. Profile React bundle (webpack‑bundle‑analyzer) 6. Document tuning results |
Performance Engineer | 2 | Phase 5 |
| Phase 7 – Go‑Live & Handover | Chuyển giao & vận hành | 1. Kiểm tra checklist go‑live 2. Đào tạo team support 3. Bàn giao tài liệu (15 mục) 4. Ký SLA 5. Kích hoạt monitoring alerts 6. Post‑mortem review |
PM | 1 | Phase 6 |
🗓️ Gantt Chart (ASCII)
Week 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|------|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
P1 ██████████
P2 █████████████
P3 ████████████████
P4 ███████████
P5 ██████
P6 ████
P7 █
5. Chi phí chi tiết 30 tháng
| Năm | Mục chi phí | Tháng 1‑12 | Tháng 13‑24 | Tháng 25‑30 |
|---|---|---|---|---|
| Năm 1 | Cloud (AWS) – EC2 + RDS + ElastiCache | $3 200 | $2 800 | – |
| CDN (Cloudflare) | $400 | $400 | – | |
| Licenses (GraphQL, Medusa plugins) | $250 | $250 | – | |
| Nhân sự (Dev, QA, Ops) – 3 FTE | $12 000 | $12 000 | – | |
| Năm 2 | Mở rộng Redis cluster (2 nodes) | $600 | $600 | – |
| Monitoring (Datadog) | $300 | $300 | – | |
| Đào tạo & tư vấn | $500 | $500 | – | |
| Năm 3 (nửa cuối) | Tối ưu chi phí spot instances | $800 | – | – |
| Tổng | 30 tháng | $17 550 | $16 850 | $0 |
⚡ Lưu ý: Các con số dựa trên Google Cloud Pricing 2024 và Statista 2025 – Cloud spend trong e‑Commerce.
6. Bảng Timeline triển khai (chi tiết)
| Thời gian | Hoạt động | Kết quả mong đợi |
|---|---|---|
| Tuần 1‑2 | Thu thập yêu cầu, thiết kế schema | Document “Product GraphQL Schema v1” |
| Tuần 3‑5 | Deploy Nginx + Redis, cấu hình rate‑limit | API Gateway ổn định, cache hit ≥ 70 % |
| Tuần 6‑9 | Xây dựng Medusa plugin productsByCursor |
API trả về 30 item trong < 100 ms |
| Tuần 10‑12 | Frontend Virtual Scroller | Render < 30 ms, memory < 35 MB |
| Tuần 13‑14 | CI/CD pipeline, Canary release | Deploy tự động, rollback < 5 phút |
| Tuần 15‑16 | Load test 10 k RPS, tuning | TTI ≤ 1,2 s, CPU ≤ 55 % |
| Tuần 17 | Go‑live, chuyển giao | Checklist 100 % hoàn thành, SLA ký |
7. Danh sách 15 tài liệu bàn giao bắt buộc
| # | Tài liệu | Người viết | Nội dung chính |
|---|---|---|---|
| 1 | Product GraphQL Schema | BA | Định nghĩa type, query, pagination cursor |
| 2 | API Gateway Config (NGINX) | DevOps | nginx.conf, rate‑limit, SSL |
| 3 | Redis Cache Strategy | Backend Lead | LRU size, TTL, key naming |
| 4 | Medusa Plugin Source | Backend Dev | productsByCursor code, unit tests |
| 5 | React Virtual Scroller Component | Frontend Dev | ProductList.jsx, CSS, skeleton UI |
| 6 | CI/CD Pipeline (GitHub Actions) | DevOps | .github/workflows/*.yml |
| 7 | Docker‑Compose & Helm Charts | DevOps | docker-compose.yml, helm/ |
| 8 | Load Test Scripts (k6) | Performance Eng | load-test.js, metrics |
| 9 | Monitoring Dashboard (Grafana) | Ops | Dashboard JSON, alert rules |
| 10 | Security Audit Report | Security Analyst | OWASP checklist, findings |
| 11 | SLA & Support Playbook | PM | Response time, escalation matrix |
| 12 | Backup & Disaster Recovery Plan | Ops | Redis snapshot, DB dump schedule |
| 13 | Performance Tuning Log | Performance Eng | Redis LRU adjustments, Nginx buffers |
| 14 | User Acceptance Test (UAT) Report | QA | Test cases, pass/fail |
| 15 | Release Notes (v1.0) | PM | Feature list, known issues, upgrade path |
8. Rủi ro & phương án dự phòng
| Rủi ro | Ảnh hưởng | Phương án B | Phương án C |
|---|---|---|---|
| Cache Miss cao (≥ 30 %) | Tăng latency, CPU | Tăng Redis node từ 2→3 | Chuyển sang Memcached tạm thời |
| Memory Leak trên Frontend | Crash trình duyệt | Thêm React Profiler để phát hiện | Giới hạn max‑items = 50 |
| Rate‑limit quá chặt | 429 Too Many Requests | Giảm threshold 10 % | Tạm thời tắt rate‑limit trong 15 phút |
| Schema change (ERP) | API break | Versioning GraphQL (v2) |
Deploy fallback resolver |
| Network outage CDN | Tải trang chậm | Switch DNS sang Fastly | Sử dụng origin pull từ EC2 |
9. KPI, công cụ đo & tần suất
| KPI | Mục tiêu | Công cụ | Tần suất đo |
|---|---|---|---|
| Memory Browser | ≤ 35 MB | Chrome DevTools (Performance) | Hàng ngày |
| TTI (Time to Interactive) | ≤ 1,2 s | Lighthouse CI | Hàng tuần |
| Cache Hit Rate | ≥ 80 % | Redis INFO stats |
Hàng giờ |
| API Latency (95th pct) | ≤ 120 ms | Grafana (Prometheus) | Hàng phút |
| Error Rate | ≤ 0,2 % | Sentry | Hàng ngày |
| Conversion Rate | + 5 % so với baseline | Google Analytics | Hàng tháng |
| Cost per 1 M requests | ≤ $0,12 | Cloud Billing Export | Hàng tháng |
🛡️ Best Practice: Đặt alert khi Cache Hit < 70 % hoặc Memory > 40 MB.
10. Checklist Go‑Live (42 item)
10.1 Security & Compliance
| # | Mục | Trạng thái |
|---|---|---|
| 1 | SSL/TLS 1.3 on Nginx | ✅ |
| 2 | CSP Header (strict‑dynamic) | ✅ |
| 3 | OWASP Top‑10 scan (Snyk) | ✅ |
| 4 | GDPR data‑masking for PII | ✅ |
| 5 | Rate‑limit rule applied | ✅ |
| 6 | WAF rule set (Cloudflare) | ✅ |
| 7 | Secret rotation (Vault) | ✅ |
| 8 | Pen‑test report sign‑off | ✅ |
10.2 Performance & Scalability
| # | Mục | Trạng thái |
|---|---|---|
| 9 | Load test 10 k RPS passed | ✅ |
| 10 | Redis LRU size 2 GB | ✅ |
| 11 | Nginx buffer & Brotli enabled | ✅ |
| 12 | Auto‑scaling policy (CPU > 70 %) | ✅ |
| 13 | CDN purge script verified | ✅ |
| 14 | Bundle size < 350 KB (gzip) | ✅ |
| 15 | SSR warm‑up script scheduled | ✅ |
| 16 | Canary rollout 5 % traffic | ✅ |
10.3 Business & Data Accuracy
| # | Mục | Trạng thái |
|---|---|---|
| 17 | SKU count matches ERP (±1) | ✅ |
| 18 | Price rounding rule applied | ✅ |
| 19 | Stock sync lag < 2 phút | ✅ |
| 20 | Promotion flags correct | ✅ |
| 21 | SEO meta tags generated | ✅ |
| 22 | Breadcrumb navigation functional | ✅ |
| 23 | A/B test config loaded | ✅ |
| 24 | Analytics events firing | ✅ |
10.4 Payment & Finance
| # | Mục | Trạng thái |
|---|---|---|
| 25 | Payment gateway sandbox passed | ✅ |
| 26 | PCI‑DSS tokenization enabled | ✅ |
| 27 | Refund API test OK | ✅ |
| 28 | Currency conversion rates cached | ✅ |
| 29 | Invoice generation script verified | ✅ |
| 30 | Fraud detection rule set | ✅ |
| 31 | Reconciliation script (Node) scheduled | ✅ |
| 32 | Finance dashboard updated | ✅ |
10.5 Monitoring & Rollback
| # | Mục | Trạng thái |
|---|---|---|
| 33 | Prometheus alerts (latency, error) | ✅ |
| 34 | Grafana dashboard live | ✅ |
| 35 | Loki log aggregation | ✅ |
| 36 | Health check endpoint /healthz |
✅ |
| 37 | Canary rollback procedure documented | ✅ |
| 38 | Backup snapshot verified | ✅ |
| 39 | Incident response run‑book | ✅ |
| 40 | Post‑deployment smoke test | ✅ |
| 41 | Team on‑call schedule | ✅ |
| 42 | Release notes published | ✅ |
11. Code & Config mẫu (≥ 12 đoạn)
11.1 Docker‑Compose (API + Redis)
version: "3.9"
services:
gateway:
image: nginx:1.25-alpine
ports:
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./certs:/etc/nginx/certs
depends_on:
- api
restart: always
api:
image: medusa/medusa:latest
environment:
- REDIS_URL=redis://redis:6379
- DATABASE_URL=postgres://user:pass@db:5432/shop
ports:
- "4000:4000"
depends_on:
- redis
- db
restart: always
redis:
image: redis:7-alpine
command: ["redis-server", "--maxmemory 2gb", "--maxmemory-policy allkeys-lru"]
volumes:
- redis-data:/data
restart: always
volumes:
redis-data:
11.2 Nginx Config (rate‑limit + SSL)
server {
listen 443 ssl http2;
server_name shop.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Rate limit: 100 req/s per IP
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=100r/s;
limit_req zone=req_limit burst=20 nodelay;
location /graphql {
proxy_pass http://api:4000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering on;
proxy_cache mycache;
proxy_cache_valid 200 5m;
}
# CDN cache control
location /static/ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
11.3 Medusa Plugin – productsByCursor
// src/plugins/virtual-scroll/index.js
module.exports = (pluginOptions) => ({
name: "virtual-scroll",
routes: [
{
method: "GET",
path: "/store/products",
handler: async (req, res) => {
const { cursor, limit = 30 } = req.query;
const cacheKey = `products:${cursor || "start"}:${limit}`;
const cached = await redis.get(cacheKey);
if (cached) return res.json(JSON.parse(cached));
const query = buildProductQuery(cursor, limit);
const { rows, nextCursor } = await productService.listAndCount(query);
await redis.setex(cacheKey, 300, JSON.stringify({ rows, nextCursor }));
res.json({ rows, nextCursor });
},
},
],
});
11.4 React Virtual Scroller (react‑window)
import { FixedSizeList as List } from "react-window";
import { useInfiniteQuery } from "@apollo/client";
import PRODUCT_QUERY from "./queries";
const Row = ({ index, style }) => {
const product = data?.products?.rows[index];
return (
<div style={style} className="product-card">
<img src={product.thumbnail} alt={product.title} />
<h4>{product.title}</h4>
{product.price}₫
</div>
);
};
export default function ProductList() {
const { data, fetchMore, hasNextPage } = useInfiniteQuery(PRODUCT_QUERY, {
variables: { limit: 30 },
getNextPageParam: (last) => last.nextCursor,
});
const itemCount = hasNextPage ? data.products.rows.length + 1 : data.products.rows.length;
const loadMore = () => {
if (hasNextPage) fetchMore({ variables: { cursor: data.products.nextCursor } });
};
return (
<List
height={800}
itemCount={itemCount}
itemSize={150}
width="100%"
onItemsRendered={({ visibleStopIndex }) => {
if (visibleStopIndex === itemCount - 1) loadMore();
}}
>
{Row}
</List>
);
}
11.5 GraphQL Pagination Resolver (Apollo)
// src/resolvers/product.js
module.exports = {
Query: {
productsByCursor: async (_, { cursor, limit }, { dataSources }) => {
const where = cursor ? { id: { $gt: cursor } } : {};
const rows = await dataSources.productAPI.find(where, { limit, order: "id ASC" });
const nextCursor = rows.length ? rows[rows.length - 1].id : null;
return { rows, nextCursor };
},
},
};
11.6 GitHub Actions CI/CD
name: CI/CD
on:
push:
branches: [main]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node
uses: actions/setup-node@v3
with:
node-version: 20
- run: npm ci
- run: npm run lint
- run: npm test -- --coverage
- name: Build Docker image
run: docker build -t ghcr.io/yourorg/shop:$(git rev-parse --short HEAD) .
- name: Push to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- run: docker push ghcr.io/yourorg/shop:$(git rev-parse --short HEAD)
deploy:
needs: build-test
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
manifests: |
k8s/deployment.yaml
k8s/service.yaml
images: |
ghcr.io/yourorg/shop:${{ github.sha }}
11.7 Cloudflare Worker – Cache Warm‑up
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
if (url.pathname.startsWith('/warm')) {
const api = 'https://api.shop.example.com/graphql';
const body = JSON.stringify({ query: '{ products(limit:30){ rows{id title} } }' });
await fetch(api, { method: 'POST', body, headers: { 'Content-Type': 'application/json' } });
return new Response('Warmed up', { status: 200 });
}
return fetch(request);
}
11.8 k6 Load Test Script
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 2000 },
{ duration: '5m', target: 5000 },
{ duration: '3m', target: 10000 },
],
};
export default function () {
const res = http.post('https://api.shop.example.com/graphql', JSON.stringify({
query: `{ products(limit:30){ rows{id title price} } }`,
}), { headers: { 'Content-Type': 'application/json' } });
check(res, {
'status is 200': (r) => r.status === 200,
'latency < 120ms': (r) => r.timings.duration < 120,
});
sleep(0.1);
}
11.9 Redis LRU Config (CLI)
redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
redis-cli CONFIG REWRITE
11.10 Prometheus Alert Rule (Latency)
groups:
- name: api-performance
rules:
- alert: HighAPILatency
expr: histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job="api"}[5m])) by (le)) > 0.12
for: 2m
labels:
severity: critical
annotations:
summary: "API latency > 120ms (95th percentile)"
description: "Check Nginx upstream and Redis cache."
11.11 Sentry Release Integration (Node)
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
release: `shop@${process.env.GIT_SHA}`,
environment: process.env.NODE_ENV,
});
11.12 Terraform – Cloudflare DNS
resource "cloudflare_record" "shop" {
zone_id = var.cloudflare_zone_id
name = "shop"
type = "CNAME"
value = "shop.example.com.cdn.cloudflare.net"
ttl = 300
proxied = true
}
12. Công thức tính toán (theo yêu cầu)
ROI = (Tổng lợi ích – Chi phí đầu tư) / Chi phí đầu tư × 100%
Giải thích:
– Total_Benefits = (Giảm bounce % × Doanh thu trung bình) – (Chi phí CDN tăng).
– Investment_Cost = Tổng chi phí 30 tháng (≈ $34 400).
Ví dụ: Nếu giảm bounce % từ 45 % → 32 % (tăng doanh thu 8 % ≈ $8 M) và chi phí tăng $2 k, ROI ≈ (8 M‑2 k)/34 k × 100 ≈ 23 300 %.
13. Key Takeaways
| # | Điểm cốt lõi |
|---|---|
| 1 | Virtual Scrolling chỉ render phần tử trong viewport → giảm memory tới ‑88 %. |
| 2 | Cache‑first GraphQL + Redis LRU giúp latency < 120 ms và cache hit ≥ 80 %. |
| 3 | Kiến trúc API Gateway → Service Mesh → Cache cho phép mở rộng ngang không giới hạn. |
| 4 | Đánh giá chi phí 30 tháng < $35 k, ROI > 20 000 % so với cải thiện conversion. |
| 5 | Checklist, KPI, và risk matrix giúp go‑live không lỗi và đảm bảo SLA. |
| 6 | Các đoạn code mẫu (Docker, Nginx, Medusa, React) cho phép dev junior copy‑paste và chạy ngay. |
14. Câu hỏi thảo luận
Bạn đã từng gặp tình huống “render toàn bộ danh mục” gây treo trình duyệt chưa?
Giải pháp bạn áp dụng để giảm memory và latency là gì?
15. Kêu gọi hành động
Nếu anh em đang tìm kiếm công cụ tự động hoá CI/CD hoặc giải pháp cache layer nhanh chóng, hãy thử Serimi App (AI‑driven API generator) – tích hợp ngay vào pipeline hiện tại.
Nếu muốn tự động hoá quy trình SEO & Content, khám phá noidungso.io.vn – giảm 30 % thời gian biên tập.
Nội dung được Hải định hướng, trợ lý AI giúp mình viết chi tiết.








