Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

Tối ưu performance web 2026: Giảm load time từ 3s xuống 0.5s với CDN, caching và compression

Ếch Trendy
Ếch Trendy

2 tháng trước · 8 phút đọc

Bài toán thực tế: website 3 giây load time

Mình từng nhận một task tưởng chừng đơn giản: "tối ưu performance cho site bán hàng". Mở DevTools ra, Lighthouse chạy - điểm Performance: 34/100. Load time trên mobile: 3.2 giây. Bounce rate? Từ 1 giây lên 3 giây, bounce rate tăng 32%; lên 5 giây tăng 90%; lên 10 giây tăng 123% (Google / SOASTA (Google Think Insights)).

Thực ra con số đó không quá bất ngờ. Vấn đề là site này dùng shared hosting, ảnh không nén, JS bundle 2.4MB chưa split, không có CDN. Mọi thứ sai từ nền tảng.

Bài này mình sẽ walk-through qua đúng những gì mình đã làm - từ audit ban đầu đến khi load time còn 0.5s. Không lý thuyết suông, có số liệu thật và code cụ thể.

346074 Lighthouse 34 → 96 sau khi áp dụng đủ stack - không phải phép màu, chỉ là làm đúng thứ tự

Tools cần có trước khi làm gì

Trước khi optimize, phải biết mình đang ở đâu. Ba tools mình dùng thường xuyên:

  • Lighthouse (Chrome DevTools): baseline nhanh, đủ cho local audit
  • WebPageTest (WebPageTest): test từ nhiều location, nhiều device thật
  • GTmetrix (GTmetrix): waterfall chart đẹp, dễ đọc cho client

Chạy cả ba trên cùng một URL, lấy trung bình. Đừng chạy một lần rồi kết luận vì network noise ảnh hưởng kết quả đáng kể.


Compression: Brotli thay vì Gzip

Nếu server của bạn vẫn đang dùng Gzip, đây là thứ nên đổi ngay hôm nay.

Brotli (do Google phát triển) nén tốt hơn Gzip Brotli is typically 10-20% smaller than Gzip for web text assets, with some benchmarks showing 5-25% reduction depending on content and compression level (Web Performance Benchmarks 2024) với text-based assets (HTML, CSS, JS). Trên thực tế, file JS 2.4MB của mình sau Brotli còn 480KB - so với 620KB khi Gzip.

346075 Brotli thường cho kết quả nhỏ hơn Gzip rõ rệt với file JS/CSS - đáng để đổi nếu server hỗ trợ

Config Nginx:

# Bật Brotli compression trên Nginx
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;

# Fallback về Gzip nếu browser không hỗ trợ Brotli
gzip on;
gzip_comp_level 5;
gzip_types text/plain text/css application/javascript application/json;

Kiểm tra server đã bật Brotli chưa:

# Gửi request với Accept-Encoding: br và kiểm tra response header
curl -H "Accept-Encoding: br" -I https://yourdomain.com/main.js | grep content-encoding
# Nếu ra: content-encoding: br → đã bật thành công

Lưu ý: nếu đang dùng Cloudflare free plan, Brotli được bật mặc định cho static assets - không cần config thêm.


CDN: thứ tạo ra sự khác biệt lớn nhất

Mình đã từng nghĩ CDN chỉ dành cho big tech. Sai hoàn toàn.

Khi user ở Đà Nẵng truy cập site host tại Singapore, mỗi request phải đi vài trăm km. CDN giải quyết bằng cách cache và serve content từ edge server gần người dùng nhất. Kết quả? Latency giảm, server gốc ít tải hơn.

346076 Không có CDN: mọi request đi thẳng về origin server. Có CDN: static assets serve từ node gần nhất

Với Cloudflare (free tier đủ dùng cho hầu hết project):

// Cache-Control header cho static assets
// Đặt trên server hoặc qua Cloudflare Page Rules

// Assets tĩnh: cache lâu dài
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
// immutable = browser không cần revalidate khi refresh

// HTML: không cache hoặc cache ngắn
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
// Hoặc:
res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');

Một điểm mình hay quên: cache invalidation. Khi deploy code mới, assets cũ vẫn nằm trên CDN. Giải pháp chuẩn là content hashing:

// Webpack / Vite tự sinh hash trong tên file
// Ví dụ output: main.a3f7c9b2.js
// Thay đổi code → hash thay đổi → URL mới → browser fetch file mới

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        // Tự động hash tên file khi build
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash].[ext]'
      }
    }
  }
};

Cloudflare Workers (Cloudflare Workers) là bước tiếp theo nếu bạn muốn custom logic tại edge - ví dụ A/B testing, geo-routing, hay transform response on-the-fly.


Browser caching và service workers

CDN cache ở edge. Browser cache ở máy người dùng. Hai lớp này hoạt động độc lập - tối ưu được cả hai mới đúng.

Mình phân loại cache theo loại content:

Loại content Cache strategy Ví dụ
JS/CSS với hash max-age=31536000, immutable main.a3f7.js
Ảnh tĩnh max-age=604800 (7 ngày) logo.png
Font max-age=31536000, immutable inter.woff2
HTML no-cache index.html
API responses max-age=60 hoặc no-store /api/products

Service Worker là lớp cache thứ ba - chạy trong browser, intercept network requests, và quyết định serve từ cache hay fetch từ network.

346077 Service Worker đứng giữa browser và network - cache hit = 0ms latency

// service-worker.js - Chiến lược Cache First cho static assets
const CACHE_NAME = 'v1-static-assets';
const STATIC_ASSETS = [
  '/',
  '/main.css',
  '/main.js'
];

// Cài đặt: precache các file quan trọng
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
  );
});

// Fetch: trả về từ cache trước, fallback về network
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      // Cache hit → trả ngay, không cần network
      if (cached) return cached;
      // Cache miss → fetch từ network rồi cache lại
      return fetch(event.request).then(response => {
        const clone = response.clone();
        caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
        return response;
      });
    })
  );
});

Workbox (Workbox) từ Google làm việc này tốt hơn nhiều so với viết tay - có sẵn các strategies như StaleWhileRevalidate, NetworkFirst, và tự handle versioning.


Lazy loading và code splitting

Hai kỹ thuật này tấn công cùng một vấn đề: đừng tải thứ người dùng chưa cần.

Lazy loading ảnh

Native lazy loading đã supported 95-96% global browser support for native lazy loading img attribute in 2024-2025 (Can I Use) browser hiện đại - không cần thư viện:

<!-- Thêm loading="lazy" là xong - browser tự handle intersection -->
<img
  src="product-photo.jpg"
  alt="Mô tả ảnh"
  loading="lazy"
  width="800"
  height="600"
/>

<!--
  QUAN TRỌNG: luôn khai báo width và height
  Tránh layout shift (CLS) khi ảnh load xong
-->

Với ảnh hero (above the fold), đừng lazy load - dùng loading="eager" hoặc bỏ attribute đó để browser ưu tiên tải trước.

Code splitting trong React

import { lazy, Suspense } from 'react';

// Lazy import: component chỉ được fetch khi cần render
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));

function App() {
  return (
    <Suspense fallback={<div>Đang tải...</div>}>
      {/* HeavyChart.js chỉ download khi component này được render */}
      <HeavyChart data={chartData} />
    </Suspense>
  );
}

// Route-based splitting với React Router
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
// Mỗi page là một chunk riêng - user chỉ tải page họ truy cập

346078 Code splitting chia bundle thành nhiều chunk nhỏ - initial load chỉ tải chunk cần thiết

Sau khi áp dụng code splitting cho site mình đề cập ở đầu bài: initial JS bundle từ 2.4MB xuống 340KB. Đây là improvement lớn nhất trong cả pipeline.

Nếu bạn đang học React và muốn hiểu rõ hơn về cách React hoạt động bên dưới, khóa Xây Dựng Website với ReactJS cover khá chi tiết các patterns như lazy loading, Suspense, và tối ưu re-render.


Image optimization: thứ mọi người hay bỏ qua nhất

JS bundle 2.4MB nghe to, nhưng ảnh chưa nén trên nhiều site còn tệ hơn. Một ảnh JPEG chụp từ điện thoại dễ đạt 5-8MB nếu không xử lý.

WebP và AVIF là hai format mình recommend:

  • WebP: nhỏ hơn JPEG 25%-34% smaller than JPEG at the same SSIM visual similarity level (Google WebP Compression Study), support rộng hơn AVIF
  • AVIF: nhỏ hơn WebP thêm 20-30% nữa, nhưng encoding chậm hơn
<!-- picture element: browser chọn format nó hỗ trợ -->
<picture>
  <!-- Thử AVIF trước (nhỏ nhất) -->
  <source srcset="hero.avif" type="image/avif" />
  <!-- Fallback sang WebP -->
  <source srcset="hero.webp" type="image/webp" />
  <!-- Fallback cuối cùng: JPEG -->
  <img src="hero.jpg" alt="Hero image" width="1200" height="600" />
</picture>

346079 AVIF > WebP > JPEG về kích thước - dùng picture element để không bỏ user cũ

Tự động hóa trong build pipeline với Sharp (Node.js):

// scripts/optimize-images.js
const sharp = require('sharp');
const path = require('path');

async function optimizeImage(inputPath) {
  const filename = path.basename(inputPath, path.extname(inputPath));
  const outputDir = path.dirname(inputPath);

  // Tạo WebP
  await sharp(inputPath)
    .webp({ quality: 80 })
    .toFile(`${outputDir}/${filename}.webp`);

  // Tạo AVIF (chất lượng thấp hơn vì AVIF nén tốt hơn)
  await sharp(inputPath)
    .avif({ quality: 65 })
    .toFile(`${outputDir}/${filename}.avif`);

  console.log(`✓ Optimized: ${filename}`);
}

Next.js có next/image component làm tất cả việc này tự động - resize, convert sang WebP/AVIF, lazy load, và generate srcset cho responsive. Nếu bạn đang dùng Next.js, không có lý do gì để không dùng nó.