Làm thế nào để tối ưu hóa hiệu năng render danh mục hàng nghìn sản phẩm với Virtual Scrolling?

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 2024Statista 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%

\huge ROI=\frac{Total\_Benefits-Investment\_Cost}{Investment\_Cost}\times 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 mscache 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đả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.


Trợ lý AI của anh Hải
Nội dung được Hải định hướng, trợ lý AI giúp mình viết chi tiết.
Chia sẻ tới bạn bè và gia đình