Giới thiệu
Trong vai trò lập trình viên Frontend sử dụng ReactJS, tôi đã kế thừa một ứng dụng React có tốc độ tải chậm một cách đáng ngạc nhiên. Người dùng liên tục phàn nàn về thời gian tải lâu và tỷ lệ thoát của chúng tôi đang gia tăng. Các bên liên quan đã đưa ra một thách thức rõ ràng: "Làm cho nó nhanh hơn, hoặc chúng tôi sẽ xem xét việc viết lại hoàn toàn."
Thống kê đáng chú ý:
- Thời gian tải ban đầu: ~4.2 giây
- Thời gian tương tác (TTI): ~6.8 giây
- Kích thước gói: 2.1 MB
- Điểm số hiệu suất Lighthouse: 34/100
Sau khi triển khai những chiến lược tôi sẽ chia sẻ dưới đây, chúng tôi đã đạt được:
- Thời gian tải: 3.1 giây (cải thiện 26%)
- TTI: 4.9 giây (cải thiện 28%)
- Kích thước gói: 1.4 MB (giảm 33%)
- Điểm số Lighthouse: 78/100 (cải thiện 129%)
Hãy cùng tôi tìm hiểu cách chúng tôi thực hiện điều này.
Bước 1: Đo lường trước, Tối ưu sau
Trước khi chạm vào bất kỳ mã nào, tôi đã thiết lập các chỉ số cơ bản bằng cách sử dụng nhiều công cụ:
bash
# Cài đặt công cụ giám sát hiệu suất
npm install --save-dev webpack-bundle-analyzer
npm install --save-dev lighthouse
Cài đặt đo lường:
javascript
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
// ... cấu hình khác
plugins: [
process.env.ANALYZE && new BundleAnalyzerPlugin()
].filter(Boolean)
};
// package.json
{
"scripts": {
"analyze": "ANALYZE=true npm run build"
}
}
Thông tin quan trọng: Bạn không thể cải thiện những gì bạn không đo lường. Công cụ phân tích gói ngay lập tức cho thấy rằng phần lớn nhất của chúng tôi là một thư viện biểu đồ mà chúng tôi rất ít sử dụng.
Bước 2: Tách mã và Tải lười
Chiến thắng lớn nhất đến từ việc tách gói monolithic của chúng tôi:
Trước:
javascript
// App.js - Tất cả đều được nhập trước
import Dashboard from './components/Dashboard';
import Analytics from './components/Analytics';
import Reports from './components/Reports';
import Settings from './components/Settings';
function App() {
return (
<Router>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/reports" element={<Reports />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Router>
);
}
Sau:
javascript
// App.js - Tải lười các thành phần của route
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./components/Dashboard'));
const Analytics = lazy(() => import('./components/Analytics'));
const Reports = lazy(() => import('./components/Reports'));
const Settings = lazy(() => import('./components/Settings'));
function App() {
return (
<Router>
<Suspense fallback={<div className="loading-spinner">Đang tải...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/reports" element={<Reports />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</Router>
);
}
Kết quả: Kích thước gói ban đầu giảm từ 2.1MB xuống 1.2MB ngay lập tức.
Bước 3: Tối ưu hóa cấp độ thành phần
Chiến lược Memoization
Tôi đã xác định các thành phần đang tái render không cần thiết:
javascript
// Trước - Tái render trên mỗi lần cập nhật của cha
const UserCard = ({ user, onEdit }) => {
return (
<div className="user-card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<button onClick={() => onEdit(user.id)}>Chỉnh sửa</button>
</div>
);
};
// Sau - Chỉ tái render khi dữ liệu người dùng thay đổi
const UserCard = React.memo(({ user, onEdit }) => {
return (
<div className="user-card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<button onClick={() => onEdit(user.id)}>Chỉnh sửa</button>
</div>
);
}, (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name;
});
Sử dụng useMemo cho các phép tính tốn kém
javascript
// Trước - Tính toán lại trên mỗi lần render
const Dashboard = ({ data }) => {
const processedData = data.map(item => ({
...item,
calculations: heavyCalculation(item)
}));
return <Chart data={processedData} />;
};
// Sau - Chỉ tính toán lại khi dữ liệu thay đổi
const Dashboard = ({ data }) => {
const processedData = useMemo(() =>
data.map(item => ({
...item,
calculations: heavyCalculation(item)
})), [data]
);
return <Chart data={processedData} />;
};
Bước 4: Tối ưu hóa hình ảnh
Hình ảnh là một điểm nghẽn lớn. Dưới đây là những gì đã hiệu quả:
javascript
// Hook tùy chỉnh cho tải hình ảnh tiến bộ
const useProgressiveImage = (src) => {
const [loading, setLoading] = useState(true);
const [imgSrc, setImgSrc] = useState(null);
useEffect(() => {
const img = new Image();
img.onload = () => {
setImgSrc(src);
setLoading(false);
};
img.src = src;
}, [src]);
return { loading, imgSrc };
};
// Sử dụng thành phần
const ImageComponent = ({ src, placeholder, alt }) => {
const { loading, imgSrc } = useProgressiveImage(src);
return (
<div className="image-container">
{loading ? (
<img src={placeholder} alt={alt} className="placeholder" />
) : (
<img src={imgSrc} alt={alt} className="loaded" />
)}
</div>
);
};
Bước 5: Tối ưu hóa gọi API
Tránh gọi API trùng lặp
javascript
// Hook tùy chỉnh để ngăn chặn gọi API trùng lặp
const useApiCall = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const requestCache = useRef(new Map());
const fetchData = useCallback(async () => {
if (requestCache.current.has(url)) {
return requestCache.current.get(url);
}
setLoading(true);
try {
const response = await fetch(url);
const result = await response.json();
requestCache.current.set(url, result);
setData(result);
return result;
} catch (error) {
console.error('Gọi API thất bại:', error);
} finally {
setLoading(false);
}
}, [url]);
return { data, loading, fetchData };
};
Tiền lấy dữ liệu
javascript
// Tiền lấy dữ liệu cho các trang có khả năng truy cập tiếp theo
const Dashboard = () => {
const { data: currentData } = useApiCall('/api/dashboard');
useEffect(() => {
// Tiền lấy dữ liệu phân tích (người dùng có khả năng truy cập tiếp theo)
const timer = setTimeout(() => {
fetch('/api/analytics').then(response => response.json());
}, 2000);
return () => clearTimeout(timer);
}, []);
return <DashboardContent data={currentData} />;
};
Bước 6: Tối ưu hóa gói
Cải thiện Tree Shaking
javascript
// Trước - Nhập toàn bộ thư viện
import _ from 'lodash';
import moment from 'moment';
// Sau - Nhập chỉ những gì bạn cần
import { debounce, throttle } from 'lodash';
import dayjs from 'dayjs'; // Thay thế nhỏ hơn cho moment
Nhập động cho các thư viện nặng
javascript
// Tải thư viện biểu đồ nặng chỉ khi cần thiết
const ChartComponent = ({ data }) => {
const [ChartLibrary, setChartLibrary] = useState(null);
useEffect(() => {
import('react-chartjs-2').then((module) => {
setChartLibrary(() => module.Line);
});
}, []);
if (!ChartLibrary) {
return <div>Đang tải biểu đồ...</div>;
}
return <ChartLibrary data={data} />;
};
Bước 7: Triển khai Service Worker
javascript
// public/sw.js - Chiến lược lưu trữ cơ bản
const CACHE_NAME = 'app-v1';
const urlsToCache = [
'/',
'/static/css/main.css',
'/static/js/main.js'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => response || fetch(event.request))
);
});
Đo lường tác động
Cài đặt giám sát hiệu suất:
javascript
// utils/performance.js
export const measureComponentPerformance = (componentName) => {
return function(WrappedComponent) {
return function MeasuredComponent(props) {
useEffect(() => {
const startTime = performance.now();
return () => {
const endTime = performance.now();
console.log(`${componentName} thời gian render: ${endTime - startTime}ms`);
};
});
return <WrappedComponent {...props} />;
};
};
};
// Sử dụng
export default measureComponentPerformance('Dashboard')(Dashboard);
Kết quả
Sau khi triển khai các tối ưu hóa này trong 3 tuần:
| Chỉ số | Trước | Sau | Cải thiện |
|---|---|---|---|
| Thời gian tải | 4.2s | 3.1s | Nhanh hơn 26% |
| Thời gian tương tác | 6.8s | 4.9s | Nhanh hơn 28% |
| Kích thước gói | 2.1MB | 1.4MB | Nhỏ hơn 33% |
| Điểm số Lighthouse | 34/100 | 78/100 | Tốt hơn 129% |
Bài học chính đã học
- Đo lường mọi thứ - Bạn không thể tối ưu hóa những gì bạn không thấy
- Bắt đầu với những thắng lớn nhất - Tách mã đã cho chúng tôi giảm kích thước gói ngay lập tức 40%
- Không tối ưu hóa quá mức - Một số tối ưu hóa nhỏ không xứng đáng với độ phức tạp
- Trải nghiệm người dùng là quan trọng nhất - Đôi khi một spinner tải tốt hơn là một UI bị đông cứng
Công cụ đã tạo ra sự khác biệt
- React DevTools Profiler - Nhận diện các thành phần chậm
- Webpack Bundle Analyzer - Hình dung cấu trúc gói
- Chrome DevTools - Phân tích mạng và hiệu suất
- Lighthouse CI - Kiểm tra hiệu suất tự động
Điều gì tiếp theo?
- Triển khai các tính năng đồng thời của React 18
- Khám phá các thành phần máy chủ của React
- Thêm các chiến lược lưu trữ tinh vi hơn
- A/B testing các mẫu tải khác nhau
Tối ưu hóa hiệu suất là một hành trình, không phải là một điểm đến. Các kỹ thuật trên đã mang lại cho chúng tôi những cải thiện đáng kể, nhưng luôn có nhiều điều hơn để khám phá.
Bạn đang gặp phải thách thức hiệu suất nào trong các ứng dụng React của bạn? Hãy chia sẻ trải nghiệm của bạn trong phần bình luận!