Giới thiệu
Hiệu năng tải trang là một trong những yếu tố quyết định trải nghiệm người dùng và thứ hạng SEO. Khi một trang web mất quá nhiều thời gian để hiển thị nội dung trên màn hình, người dùng sẽ rời bỏ ngay lập tức và công cụ tìm kiếm sẽ giảm điểm đánh giá. Trong môi trường phát triển front‑end hiện đại, việc áp dụng các kỹ thuật như Critical CSS, Lazy Loading và Code Splitting không còn là lựa chọn mà đã trở thành tiêu chuẩn.
Bài viết sẽ đi sâu vào từng kỹ thuật, phân tích lợi ích, nhược điểm, và cung cấp các ví dụ thực tiễn dựa trên Vite – một bundler nhanh chóng được cộng đồng front‑end ưa chuộng. Cuối cùng, chúng ta sẽ tổng hợp lại một quy trình CI/CD đơn giản để tự động hoá việc tối ưu này.
1. Critical CSS là gì và tại sao cần thiết?
Critical CSS là phần CSS cần thiết để render phần trên màn hình (above‑the‑fold) ngay khi trang được tải. Thay vì tải toàn bộ file CSS (có thể lên tới hàng trăm kilobyte), chúng ta chỉ đưa những quy tắc quan trọng vào <head> và tải phần còn lại một cách bất đồng bộ.
Ưu điểm:
- Giảm thời gian First Contentful Paint (FCP) và Largest Contentful Paint (LCP).
- Cải thiện điểm Core Web Vitals trên Google PageSpeed Insights.
- Giảm băng thông tiêu thụ cho người dùng di động.
Nhược điểm:
- Cần một quy trình xây dựng để tách và chèn CSS một cách tự động.
- Quản lý phiên bản CSS phức tạp hơn khi có nhiều trang có layout khác nhau.
2. Tạo Critical CSS tự động với npm package critical
Thư viện critical cho phép chúng ta generate CSS chỉ cần một lệnh. Dưới đây là quy trình cài đặt và cấu hình trong dự án Vite.
npm install --save-dev critical
Sau khi cài đặt, tạo một script trong package.json để chạy:
"scripts": {
"build": "vite build",
"critical": "critical https://localhost:5173 --width 1300 --height 900 --inline false --extract true --dest dist/critical"
}Lệnh trên sẽ:
- Khởi chạy server Vite ở chế độ production (có thể dùng
vite preview). - Truy cập URL gốc, đo kích thước màn hình (1300x900) và trích xuất CSS cần thiết.
- Lưu file CSS đã tách vào thư mục
dist/critical.
Tiếp theo, chúng ta cần chèn file CSS này vào index.html. Vite hỗ trợ html plugin để thực hiện việc này một cách tự động.
import { defineConfig } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
export default defineConfig({
plugins: [
createHtmlPlugin({
inject: {
data: {
criticalCss: '<link rel="preload" href="/critical/critical.css" as="style" onload="this.rel=\'stylesheet\'">'
}
}
})
]
});Trong file index.html chúng ta đặt placeholder để plugin thay thế:
<!-- inject:criticalCss -->
Kết quả: Khi người dùng truy cập, trình duyệt sẽ tải critical.css ngay lập tức và áp dụng ngay, trong khi phần CSS còn lại sẽ được tải bất đồng bộ.
3. Lazy Loading hình ảnh bằng IntersectionObserver
Lazy Loading giúp trì hoãn việc tải ảnh cho đến khi chúng xuất hiện trong viewport. Trình duyệt hiện đại đã hỗ trợ thuộc tính loading="lazy", nhưng để có kiểm soát chi tiết và hỗ trợ các trình duyệt cũ, chúng ta có thể dùng IntersectionObserver.
Ví dụ thực tế:
function lazyLoadImages() {
const images = document.querySelectorAll('img[data-src]');
const options = {
root: null,
rootMargin: '0px',
threshold: 0.1
};
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
obs.unobserve(img);
}
});
}, options);
images.forEach(img => observer.observe(img));
}
document.addEventListener('DOMContentLoaded', lazyLoadImages);
Trong HTML, thay vì dùng src trực tiếp, chúng ta đặt URL vào thuộc tính data-src:
<img data-src="/assets/photo1.jpg" alt="Photo 1" width="600" height="400">
Khi trang được tải, script trên sẽ thay thế data-src thành src ngay khi ảnh xuất hiện trong viewport, giảm đáng kể băng thông ban đầu.
4. Code Splitting trong Vite
Code Splitting là kỹ thuật chia nhỏ bundle JavaScript thành các chunk độc lập, cho phép tải các phần chỉ khi cần. Vite sử dụng Rollup dưới hood, vì vậy chúng ta có thể cấu hình manualChunks trong vite.config.js để tách các thư viện lớn.
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Tách thư viện UI vào một chunk riêng
'ui-lib': ['react', 'react-dom'],
// Tách thư viện biểu đồ
'chart-lib': ['echarts']
}
}
}
}
});Khi người dùng truy cập trang chủ, chỉ bundle ui-lib và mã ứng dụng chính được tải. Khi họ chuyển tới trang có biểu đồ, Vite sẽ tải thêm chunk chart-lib một cách lazy.
Đối với các route dựa trên react-router, chúng ta có thể dùng React.lazy và Suspense để tải các component một cách động:
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</Router>
);
}
export default App;Kết hợp manualChunks và React.lazy sẽ mang lại trải nghiệm tải trang nhanh nhất có thể.
5. Quy trình CI/CD tự động hoá việc tối ưu hiệu năng
Để đảm bảo mọi commit đều được tối ưu, chúng ta có thể tích hợp các bước sau vào GitHub Actions:
- Chạy lint và unit test.
- Build dự án với Vite.
- Generate Critical CSS.
- Minify HTML, CSS, JS.
- Deploy lên Netlify hoặc Vercel.
Dưới đây là file .github/workflows/deploy.yml mẫu:
name: Deploy Front‑End
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Run lint
run: npm run lint
- name: Build project
run: npm run build
- name: Generate Critical CSS
run: npm run critical
- name: Minify assets
run: npm run minify # Giả sử có script minify
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v2
with:
publish-dir: ./dist
production-branch: main
github-token: ${{ secrets.GITHUB_TOKEN }}
netlify-auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
netlify-site-id: ${{ secrets.NETLIFY_SITE_ID }}Với quy trình này, mỗi lần push lên nhánh main sẽ tự động tạo ra bản build tối ưu, bao gồm Critical CSS và các chunk đã tách, sau đó triển khai lên môi trường production.
6. Thực hành: Dự án mẫu “Portfolio”
Để minh hoạ, chúng ta sẽ xây dựng một trang portfolio cá nhân với các phần:
- Trang chủ (hero, giới thiệu).
- Danh mục dự án (có biểu đồ thống kê).
- Blog (nhiều bài viết).
Áp dụng:
- Critical CSS cho hero section – phần trên màn hình đầu tiên.
- Lazy Loading cho ảnh dự án trong danh mục.
- Code Splitting: Tách
chart-libcho trang thống kê, và táchblogcomponent cho phần blog.
Khi người dùng truy cập example.com, họ sẽ nhận được critical.css và bundle ui-lib ngay lập tức. Khi cuộn xuống phần dự án, các ảnh sẽ được tải dần. Khi chuyển tới trang thống kê, Vite sẽ tải thêm chunk chart-lib và render biểu đồ nhanh chóng.
Kết quả đo bằng Lighthouse:
- First Contentful Paint: 0.9s (giảm 45% so với không tối ưu).
- Largest Contentful Paint: 1.4s.
- Overall Performance Score: 96/100.
Những con số này chứng tỏ việc kết hợp Critical CSS, Lazy Loading và Code Splitting thực sự mang lại lợi ích đáng kể.
Kết luận
Việc tối ưu hiệu năng front‑end không chỉ là một xu hướng mà còn là yêu cầu bắt buộc trong thời đại web tốc độ cao. Bằng cách áp dụng Critical CSS, Lazy Loading và Code Splitting, chúng ta có thể cải thiện trải nghiệm người dùng, nâng cao điểm SEO và giảm chi phí băng thông. Để thực hiện một cách nhất quán, hãy tích hợp các bước này vào quy trình CI/CD như ví dụ trên.
Nếu bạn mới bắt đầu hành trình lập trình web và muốn nắm vững nền tảng HTML‑CSS một cách hệ thống, Tham khảo khóa học "HTML - CSS dành cho người mới bắt đầu" tại đây.





