Performance Optimization: Tối ưu ứng dụng web
Hướng dẫn tối ưu performance ứng dụng web: frontend, backend, database và monitoring
Performance Optimization: Tối ưu ứng dụng web toàn diện
Performance là critical factor cho user experience và SEO. Trong bài viết này, chúng ta sẽ explore comprehensive performance optimization strategies.
1. Frontend Performance Optimization
1.1 Code Splitting và Lazy Loading
// React lazy loading
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() =>
import('./components/HeavyComponent')
);
function App() {
return (
<div>
<h1>My App</h1>
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
}
// Dynamic imports với webpack
const loadAnalytics = () => import('./analytics');
// Load analytics only khi cần
button.addEventListener('click', async () => {
const { trackEvent } = await loadAnalytics();
trackEvent('button_click');
});
1.2 Image Optimization
// Next.js Image component (tự động optimize)
import Image from 'next/image';
function OptimizedImage() {
return (
<Image
src="/large-image.jpg"
alt="Description"
width={800}
height={600}
priority={true} // Preload critical images
placeholder="blur" // Blur placeholder
blurDataURL="data:image/jpeg;base64,..."
/>
);
}
// Vanilla JavaScript lazy loading
const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
observer.unobserve(img);
}
});
});
images.forEach(img => imageObserver.observe(img));
1.3 Bundle Size Optimization
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Remove console.log
drop_debugger: true, // Remove debugger statements
},
},
}),
new OptimizeCSSAssetsPlugin(),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true,
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
},
};
// Tree shaking example
// utils.js
export const heavyFunction = () => {
// Heavy computation
return largeDataSet.process();
};
export const lightFunction = () => {
return 'light';
};
// Only import what you need
import { lightFunction } from './utils'; // heavyFunction sẽ bị loại bỏ
1.4 Caching Strategies
// Service Worker cho caching
// sw.js
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/api/config'
];
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 => {
// Cache hit - return response
if (response) {
return response;
}
// Clone request
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then(response => {
// Check valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone response
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
// HTTP caching headers (Express.js)
app.use('/static', express.static('public', {
maxAge: '1y', // 1 year cho static assets
etag: true,
lastModified: true
}));
app.use('/api/data', (req, res) => {
res.set({
'Cache-Control': 'private, max-age=300', // 5 minutes
'ETag': generateETag(data)
});
res.json(data);
});
2. Backend Performance Optimization
2.1 Database Query Optimization
// Indexing strategies
// PostgreSQL example
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_created_at ON posts(created_at);
CREATE INDEX idx_posts_author_created_at ON posts(author_id, created_at DESC);
// Composite index cho common queries
CREATE INDEX idx_orders_user_status_date ON orders(user_id, status, created_at);
// Query optimization
// Bad: N+1 query problem
const users = await User.findAll();
for (const user of users) {
const posts = await Post.findAll({ where: { userId: user.id } });
user.posts = posts;
}
// Good: Eager loading
const users = await User.findAll({
include: [{
model: Post,
as: 'posts',
limit: 10,
order: [['createdAt', 'DESC']]
}],
limit: 20
});
// Pagination optimization
// Bad: OFFSET với large datasets
const posts = await Post.findAll({
limit: 20,
offset: 10000, // Slow với large offset
order: [['createdAt', 'DESC']]
});
// Good: Cursor-based pagination
const posts = await Post.findAll({
where: {
createdAt: {
[Op.lt]: cursor // cursor là createdAt của last item
}
},
limit: 20,
order: [['createdAt', 'DESC']]
});
2.2 Caching Implementation
// Redis caching layer
const redis = require('redis');
const client = redis.createClient();
class CacheService {
async get(key) {
try {
const cached = await client.get(key);
return cached ? JSON.parse(cached) : null;
} catch (error) {
console.error('Cache get error:', error);
return null;
}
}
async set(key, value, ttl = 3600) {
try {
await client.setex(key, ttl, JSON.stringify(value));
} catch (error) {
console.error('Cache set error:', error);
}
}
async invalidate(pattern) {
try {
const keys = await client.keys(pattern);
if (keys.length > 0) {
await client.del(...keys);
}
} catch (error) {
console.error('Cache invalidation error:', error);
}
}
}
// Cache-aside pattern
class UserService {
constructor(cacheService, userRepository) {
this.cache = cacheService;
this.userRepo = userRepository;
}
async getUserById(userId) {
const cacheKey = `user:${userId}`;
// Try cache first
let user = await this.cache.get(cacheKey);
if (!user) {
// Cache miss - fetch from database
user = await this.userRepo.findById(userId);
if (user) {
// Cache for future requests
await this.cache.set(cacheKey, user, 1800); // 30 minutes
}
}
return user;
}
async updateUser(userId, updateData) {
// Update database
const user = await this.userRepo.update(userId, updateData);
// Update cache
const cacheKey = `user:${userId}`;
await this.cache.set(cacheKey, user, 1800);
// Invalidate related caches
await this.cache.invalidate('users:list:*');
return user;
}
}
2.3 Connection Pooling
// Database connection pooling
// PostgreSQL với pg-pool
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 20, // Maximum number of clients in the pool
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
connectionTimeoutMillis: 2000, // Return an error after 2 seconds if connection could not be established
maxUses: 7500, // Close (and replace) a connection after it is used 7500 times
});
// Usage
const query = async (text, params) => {
const start = Date.now();
try {
const res = await pool.query(text, params);
const duration = Date.now() - start;
console.log('Query executed', { text, duration, rows: res.rowCount });
return res;
} catch (error) {
console.error('Query error', { text, error: error.message });
throw error;
}
};
// Redis connection pooling
const redis = require('redis');
const redisConfig = {
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD,
retry_strategy: (options) => {
if (options.error && options.error.code === 'ECONNREFUSED') {
return new Error('Redis server connection refused');
}
if (options.total_retry_time > 1000 * 60 * 60) {
return new Error('Redis retry time exhausted');
}
if (options.attempt > 10) {
return undefined;
}
return Math.min(options.attempt * 100, 3000);
}
};
// Create Redis clients
const redisClient = redis.createClient(redisConfig);
const redisSubscriber = redis.createClient(redisConfig);
const redisPublisher = redis.createClient(redisConfig);
// Handle connection events
redisClient.on('connect', () => {
console.log('Redis client connected');
});
redisClient.on('error', (err) => {
console.error('Redis client error:', err);
});
2.4 API Rate Limiting
// Rate limiting với express-rate-limit
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const generalLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:general:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
});
const strictLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:strict:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20, // Limit each IP to 20 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
skipSuccessfulRequests: true,
});
const apiLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:api:'
}),
windowMs: 60 * 1000, // 1 minute
max: 60, // Limit each IP to 60 requests per minute
message: 'API rate limit exceeded',
keyGenerator: (req) => {
return req.user ? req.user.id : req.ip;
}
});
// Apply different rate limits cho different routes
app.use('/api/', apiLimiter);
app.use('/auth/login', strictLimiter);
app.use('/', generalLimiter);
3. Database Performance
3.1 Indexing Strategies
-- PostgreSQL indexing examples
-- B-tree index (default) - tốt cho comparison operators
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_products_price ON products(price);
-- Partial index - index subset của data
CREATE INDEX idx_active_users ON users(last_login)
WHERE status = 'active';
-- Composite index - multiple columns
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at DESC);
-- GIN index - cho array và JSONB
CREATE INDEX idx_posts_tags ON posts USING GIN(tags);
CREATE INDEX idx_users_metadata ON users USING GIN(metadata);
-- GiST index - cho geometric và range types
CREATE INDEX idx_locations ON locations USING GIST(coordinates);
-- Expression index - index computed expression
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
-- Unique index
CREATE UNIQUE INDEX idx_users_username_unique ON users(username);
3.2 Query Optimization
-- Explain query execution plan
EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2023-01-01'
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 5
ORDER BY order_count DESC
LIMIT 10;
-- Optimize với CTE (Common Table Expression)
WITH recent_users AS (
SELECT id, name
FROM users
WHERE created_at > '2023-01-01'
),
user_orders AS (
SELECT user_id, COUNT(*) as order_count
FROM orders
WHERE user_id IN (SELECT id FROM recent_users)
GROUP BY user_id
)
SELECT ru.name, uo.order_count
FROM recent_users ru
JOIN user_orders uo ON ru.id = uo.user_id
WHERE uo.order_count > 5
ORDER BY uo.order_count DESC;
-- Optimize với window functions
SELECT name, order_count
FROM (
SELECT u.name,
COUNT(o.id) as order_count,
RANK() OVER (ORDER BY COUNT(o.id) DESC) as rank
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2023-01-01'
GROUP BY u.id, u.name
) ranked_users
WHERE order_count > 5
ORDER BY order_count DESC;
4. Monitoring và Profiling
4.1 Application Performance Monitoring
// APM với custom metrics
const prometheus = require('prom-client');
// Create metrics
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10]
});
const httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
const databaseQueryDuration = new prometheus.Histogram({
name: 'database_query_duration_seconds',
help: 'Duration of database queries in seconds',
labelNames: ['query_type', 'table']
});
// Middleware để collect metrics
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const labels = {
method: req.method,
route: req.route?.path || req.path,
status_code: res.statusCode
};
httpRequestDuration.observe(labels, duration);
httpRequestTotal.inc(labels);
});
next();
});
// Database query monitoring
const monitorQuery = async (query, queryType, table) => {
const start = Date.now();
try {
const result = await query();
const duration = (Date.now() - start) / 1000;
databaseQueryDuration.observe({
query_type: queryType,
table: table
}, duration);
return result;
} catch (error) {
// Track error metrics
throw error;
}
};
// Metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', prometheus.register.contentType);
res.end(await prometheus.register.metrics());
});
4.2 Real User Monitoring (RUM)
// Web Vitals monitoring
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
// Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
(navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
fetch('/analytics', { body, method: 'POST', keepalive: true });
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
// Custom performance monitoring
const observePerformance = () => {
// Monitor long tasks
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) { // Tasks > 50ms
console.warn('Long task detected:', entry.duration + 'ms');
// Send to analytics
sendToAnalytics({
name: 'long_task',
value: entry.duration,
startTime: entry.startTime
});
}
}
});
observer.observe({ entryTypes: ['longtask'] });
}
// Monitor resource loading
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 1000) { // Resources taking > 1s
sendToAnalytics({
name: 'slow_resource',
value: entry.duration,
resource: entry.name
});
}
}
});
resourceObserver.observe({ entryTypes: ['resource'] });
};
// Initialize monitoring
observePerformance();
5. Performance Testing
5.1 Load Testing với K6
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
const errorRate = new Rate('errors');
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up to 100 users
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', target: 200 }, // Ramp up to 200 users
{ duration: '5m', target: 200 }, // Stay at 200 users
{ duration: '2m', target: 0 }, // Ramp down to 0 users
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests should be below 500ms
errors: ['rate<0.1'], // Error rate should be below 10%
},
};
export default function () {
const res = http.get('https://api.example.com/users');
const success = check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'response has data': (r) => JSON.parse(r.body).data.length > 0,
});
errorRate.add(!success);
sleep(1); // Wait 1 second between requests
}
6. Kết luận
Performance optimization là continuous process. Key strategies:
Frontend:
- Code splitting và lazy loading
- Image optimization và proper formats
- Bundle optimization và tree shaking
- Caching strategies và service workers
Backend:
- Database optimization và indexing
- Caching layers (Redis, CDN)
- Connection pooling và rate limiting
- API optimization và pagination
Monitoring:
- APM tools cho application metrics
- RUM cho real user experience
- Load testing cho capacity planning
- Continuous monitoring và alerting
💡 Pro Tip: Measure trước khi optimize! Dùng tools như Lighthouse, WebPageTest, và custom metrics để identify real bottlenecks.
Performance Budget Example:
{
"budgets": [
{
"resourceSizes": [
{ "resourceType": "script", "budget": 300 },
{ "resourceType": "image", "budget": 500 },
{ "resourceType": "total", "budget": 1500 }
],
"resourceCounts": [
{ "resourceType": "third-party", "budget": 10 }
],
"timings": [
{ "metric": "interactive", "budget": 5000 },
{ "metric": "largest-contentful-paint", "budget": 2500 }
]
}
]
}
Bài viết này là phần đầu tiên trong series về performance. Trong các bài tiếp theo, chúng ta sẽ explore mobile performance và advanced monitoring techniques.