ai-developer-skill-os 8.1.7 → 8.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +3 -3
- package/.agents/skills/qk-code-review/SKILL.md +189 -0
- package/.agents/skills/qk-code-review/references/ai/ai-anti-patterns.md +28 -0
- package/.agents/skills/qk-code-review/references/ai/v8-schema-validation.md +65 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/architecture-review-guide.md +212 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/async-concurrency-patterns.md +515 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/code-quality-universal.md +358 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/code-review-best-practices.md +136 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/common-bugs-checklist.md +124 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/error-handling-principles.md +492 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/n-plus-one-queries.md +309 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/performance-review-guide.md +387 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/security-review-guide.md +318 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/sql-injection-prevention.md +308 -0
- package/.agents/skills/qk-code-review/references/cross-cutting/xss-prevention.md +264 -0
- package/.agents/skills/qk-code-review/references/languages/angular.md +768 -0
- package/.agents/skills/qk-code-review/references/languages/c.md +890 -0
- package/.agents/skills/qk-code-review/references/languages/cpp.md +893 -0
- package/.agents/skills/qk-code-review/references/languages/csharp.md +519 -0
- package/.agents/skills/qk-code-review/references/languages/css-less-sass.md +661 -0
- package/.agents/skills/qk-code-review/references/languages/django.md +985 -0
- package/.agents/skills/qk-code-review/references/languages/fastapi.md +580 -0
- package/.agents/skills/qk-code-review/references/languages/go.md +993 -0
- package/.agents/skills/qk-code-review/references/languages/java.md +409 -0
- package/.agents/skills/qk-code-review/references/languages/java8.md +586 -0
- package/.agents/skills/qk-code-review/references/languages/kotlin.md +1018 -0
- package/.agents/skills/qk-code-review/references/languages/nestjs.md +593 -0
- package/.agents/skills/qk-code-review/references/languages/php.md +684 -0
- package/.agents/skills/qk-code-review/references/languages/python.md +1073 -0
- package/.agents/skills/qk-code-review/references/languages/qt.md +757 -0
- package/.agents/skills/qk-code-review/references/languages/react.md +871 -0
- package/.agents/skills/qk-code-review/references/languages/ruby.md +964 -0
- package/.agents/skills/qk-code-review/references/languages/rust.md +846 -0
- package/.agents/skills/qk-code-review/references/languages/svelte.md +1064 -0
- package/.agents/skills/qk-code-review/references/languages/swift.md +936 -0
- package/.agents/skills/qk-code-review/references/languages/typescript.md +1016 -0
- package/.agents/skills/qk-code-review/references/languages/vue.md +924 -0
- package/.agents/skills/qk-code-review/references/languages/zig.md +440 -0
- package/README.md +3 -3
- package/bin/install.js +5 -2
- package/package.json +1 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# N+1 查询问题 — 跨语言通用指南
|
|
2
|
+
|
|
3
|
+
> N+1 查询是 ORM 和数据库访问层最常见的性能反模式。本文档覆盖问题定义、检测方法、通用解决方案和跨语言代码示例。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
- [问题定义](#问题定义)
|
|
8
|
+
- [性能影响](#性能影响)
|
|
9
|
+
- [检测方法](#检测方法)
|
|
10
|
+
- [通用解决方案](#通用解决方案)
|
|
11
|
+
- [语言特定实现](#语言特定实现)
|
|
12
|
+
- [Review Checklist](#review-checklist)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 问题定义
|
|
17
|
+
|
|
18
|
+
N+1 查询是指:**1 次查询获取 N 条记录,随后在循环中触发 N 次额外查询**来获取关联数据。
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
请求流程:
|
|
22
|
+
1 query → 获取 N 条主记录
|
|
23
|
+
N queries → 每条主记录查一次关联数据
|
|
24
|
+
─────────
|
|
25
|
+
Total: 1 + N queries
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 危害
|
|
29
|
+
|
|
30
|
+
| 问题 | 影响 |
|
|
31
|
+
|------|------|
|
|
32
|
+
| **查询数量线性增长** | 100 条记录 = 101 条 SQL,1000 条 = 1001 条 |
|
|
33
|
+
| **网络延迟叠加** | 每条查询都有往返延迟(RTT),N 次往返 >> 1 次批量查询 |
|
|
34
|
+
| **连接池耗尽** | 大量查询占满数据库连接,拖慢整个应用 |
|
|
35
|
+
| **难以在开发中发现** | 开发环境数据少,N+1 不明显;生产环境数据量大时性能崩塌 |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 性能影响
|
|
40
|
+
|
|
41
|
+
### 场景对比:获取 100 个用户及其订单
|
|
42
|
+
|
|
43
|
+
| 方案 | SQL 数量 | 延迟(假设 RTT=1ms) | 适用场景 |
|
|
44
|
+
|------|----------|---------------------|---------|
|
|
45
|
+
| N+1 懒加载 | 101 条 | ~101ms | 极少数据量 |
|
|
46
|
+
| Eager loading (JOIN) | 1 条 | ~1ms | 一对多,数据量适中 |
|
|
47
|
+
| Eager loading (IN) | 2 条 | ~2ms | 多对多,大数据集 |
|
|
48
|
+
| DataLoader / batch | 2 条 | ~2ms | GraphQL / 复杂图查询 |
|
|
49
|
+
|
|
50
|
+
### SQL 数量对比
|
|
51
|
+
|
|
52
|
+
```sql
|
|
53
|
+
-- ❌ N+1: 1 + 100 = 101 queries
|
|
54
|
+
SELECT * FROM users; -- 1 query
|
|
55
|
+
SELECT * FROM orders WHERE user_id = 1; -- query 2
|
|
56
|
+
SELECT * FROM orders WHERE user_id = 2; -- query 3
|
|
57
|
+
...
|
|
58
|
+
SELECT * FROM orders WHERE user_id = 100; -- query 101
|
|
59
|
+
|
|
60
|
+
-- ✅ Batch: 2 queries
|
|
61
|
+
SELECT * FROM users;
|
|
62
|
+
SELECT * FROM orders WHERE user_id IN (1,2,...,100);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 检测方法
|
|
68
|
+
|
|
69
|
+
### 1. ORM SQL 日志
|
|
70
|
+
|
|
71
|
+
开启 SQL 日志,在测试或开发环境中观察查询数量:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# Django
|
|
75
|
+
import logging
|
|
76
|
+
logging.getLogger('django.db.backends').setLevel(logging.DEBUG)
|
|
77
|
+
|
|
78
|
+
# SQLAlchemy
|
|
79
|
+
import logging
|
|
80
|
+
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```java
|
|
84
|
+
// Spring Boot application.yml
|
|
85
|
+
spring:
|
|
86
|
+
jpa:
|
|
87
|
+
show-sql: true
|
|
88
|
+
properties:
|
|
89
|
+
hibernate.format_sql: true
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```csharp
|
|
93
|
+
// EF Core
|
|
94
|
+
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 2. 查询计数断言
|
|
98
|
+
|
|
99
|
+
在测试中断言 SQL 查询数量:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# Django: django-assert-num-queries
|
|
103
|
+
from django.test.utils import CaptureQueriesContext
|
|
104
|
+
from django.db import connection
|
|
105
|
+
|
|
106
|
+
with CaptureQueriesContext(connection) as ctx:
|
|
107
|
+
list(User.objects.select_related("profile").all())
|
|
108
|
+
assert len(ctx) <= 2 # 预期最多 2 条查询
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```java
|
|
112
|
+
// Hibernate: p6spy 或 datasource-proxy
|
|
113
|
+
// 在测试中统计 SQL 执行次数
|
|
114
|
+
assertThat(sqlCount).isLessThanOrEqualTo(2);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 3. APM / 数据库监控工具
|
|
118
|
+
|
|
119
|
+
- **Django Debug Toolbar** — 实时显示 SQL 数量和时间
|
|
120
|
+
- **p6spy** (Java) — JDBC 层拦截,记录所有 SQL
|
|
121
|
+
- **MiniProfiler** (.NET) — 页面内嵌 SQL 统计
|
|
122
|
+
- **DataDog / New Relic** — 生产环境慢查询告警
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 通用解决方案
|
|
127
|
+
|
|
128
|
+
### 方案 1: Eager Loading(JOIN 预加载)
|
|
129
|
+
|
|
130
|
+
一次 JOIN 查询获取主记录和关联记录。适用于一对一、一对多。
|
|
131
|
+
|
|
132
|
+
### 方案 2: Batch Fetching(IN 子句批量查询)
|
|
133
|
+
|
|
134
|
+
两次查询:主记录 + `WHERE id IN (...)` 批量获取关联记录。适用于多对多、大数据集。
|
|
135
|
+
|
|
136
|
+
### 方案 3: DataLoader Pattern
|
|
137
|
+
|
|
138
|
+
在 GraphQL 或复杂图查询场景中,收集所有需要的 ID,合并为一次批量查询。
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
// DataLoader 伪代码
|
|
142
|
+
class DataLoader<K, V> {
|
|
143
|
+
load(K key) → V // 注册需求,不立即查询
|
|
144
|
+
loadAll([K]) → [V] // 合并为一次批量查询
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 方案 4: Projection(投影)
|
|
149
|
+
|
|
150
|
+
只查询需要的字段,减少数据传输量:
|
|
151
|
+
|
|
152
|
+
```sql
|
|
153
|
+
-- ❌ 获取所有列
|
|
154
|
+
SELECT * FROM users JOIN profiles ON ...
|
|
155
|
+
|
|
156
|
+
-- ✅ 只投影需要的字段
|
|
157
|
+
SELECT u.name, p.avatar_url FROM users u JOIN profiles p ON ...
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 语言特定实现
|
|
163
|
+
|
|
164
|
+
### Python / Django
|
|
165
|
+
|
|
166
|
+
> 详见 [Django Guide](../django.md#n1-查询优化)
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
# ForeignKey / OneToOne → select_related (SQL JOIN)
|
|
170
|
+
books = Book.objects.select_related("publisher")
|
|
171
|
+
|
|
172
|
+
# M2M / reverse FK → prefetch_related (2 queries + Python merge)
|
|
173
|
+
authors = Author.objects.prefetch_related("books")
|
|
174
|
+
|
|
175
|
+
# 嵌套预加载
|
|
176
|
+
authors = Author.objects.prefetch_related("books__publisher")
|
|
177
|
+
|
|
178
|
+
# Prefetch 对象精细控制
|
|
179
|
+
from django.db.models import Prefetch
|
|
180
|
+
authors = Author.objects.prefetch_related(
|
|
181
|
+
Prefetch("books", queryset=Book.objects.filter(published=True), to_attr="published_books")
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Python / SQLAlchemy (FastAPI)
|
|
186
|
+
|
|
187
|
+
> 详见 [FastAPI Guide](../fastapi.md#database-sessions--n1)
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from sqlalchemy.orm import selectinload
|
|
191
|
+
|
|
192
|
+
# selectinload: IN 子句批量加载(推荐异步场景)
|
|
193
|
+
stmt = select(Order).options(selectinload(Order.customer))
|
|
194
|
+
|
|
195
|
+
# joinedload: JOIN 加载
|
|
196
|
+
stmt = select(Order).options(joinedload(Order.customer))
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Java / JPA (Spring Boot)
|
|
200
|
+
|
|
201
|
+
> 详见 [Java Guide](../java.md)
|
|
202
|
+
|
|
203
|
+
```java
|
|
204
|
+
// ❌ FetchType.EAGER 或循环中触发懒加载
|
|
205
|
+
@OneToMany(fetch = FetchType.EAGER) // 危险!
|
|
206
|
+
|
|
207
|
+
// ✅ JOIN FETCH
|
|
208
|
+
@Query("SELECT u FROM User u JOIN FETCH u.orders")
|
|
209
|
+
List<User> findAllWithOrders();
|
|
210
|
+
|
|
211
|
+
// ✅ @EntityGraph(声明式)
|
|
212
|
+
@EntityGraph(attributePaths = {"orders", "profile"})
|
|
213
|
+
List<User> findAll();
|
|
214
|
+
|
|
215
|
+
// ✅ @BatchSize(减少 N+1 为 N/batchSize + 1)
|
|
216
|
+
@OneToMany
|
|
217
|
+
@BatchSize(size = 50)
|
|
218
|
+
private List<Order> orders;
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### C# / EF Core
|
|
222
|
+
|
|
223
|
+
> 详见 [C# Guide](../csharp.md)
|
|
224
|
+
|
|
225
|
+
```csharp
|
|
226
|
+
// ❌ N+1: foreach 触发懒加载
|
|
227
|
+
foreach (var blog in await context.Blogs.ToListAsync())
|
|
228
|
+
foreach (var post in blog.Posts) // 每次循环都查询!
|
|
229
|
+
|
|
230
|
+
// ✅ Include + ThenInclude
|
|
231
|
+
var blogs = await context.Blogs
|
|
232
|
+
.Include(b => b.Posts)
|
|
233
|
+
.ToListAsync();
|
|
234
|
+
|
|
235
|
+
// ✅ 投影(最安全,避免过度获取)
|
|
236
|
+
var data = await context.Blogs
|
|
237
|
+
.Select(b => new { b.Url, PostTitles = b.Posts.Select(p => p.Title) })
|
|
238
|
+
.ToListAsync();
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### PHP / Laravel / Doctrine
|
|
242
|
+
|
|
243
|
+
> 详见 [PHP Guide](../php.md)
|
|
244
|
+
|
|
245
|
+
```php
|
|
246
|
+
// ❌ 循环内查询
|
|
247
|
+
foreach ($orders as $order) {
|
|
248
|
+
$customer = $customerRepo->find($order->customerId);
|
|
249
|
+
render($order, $customer);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ✅ 批量预加载
|
|
253
|
+
$customerIds = array_unique(array_map(fn($o) => $o->customerId, $orders));
|
|
254
|
+
$customers = $customerRepo->findByIds($customerIds);
|
|
255
|
+
|
|
256
|
+
foreach ($orders as $order) {
|
|
257
|
+
render($order, $customers[$order->customerId] ?? null);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Laravel Eloquent: with()
|
|
261
|
+
$orders = Order::with('customer')->get();
|
|
262
|
+
|
|
263
|
+
// Doctrine: JOIN FETCH
|
|
264
|
+
$dql = 'SELECT o, c FROM Order o JOIN o.customer c';
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### TypeScript / Prisma
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// ❌ N+1
|
|
271
|
+
const users = await prisma.user.findMany();
|
|
272
|
+
for (const user of users) {
|
|
273
|
+
user.posts = await prisma.post.findMany({ where: { userId: user.id } });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ✅ include(Prisma 自动生成 JOIN 或批量查询)
|
|
277
|
+
const users = await prisma.user.findMany({
|
|
278
|
+
include: { posts: true },
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ✅ 嵌套 include
|
|
282
|
+
const users = await prisma.user.findMany({
|
|
283
|
+
include: {
|
|
284
|
+
posts: {
|
|
285
|
+
include: { comments: true },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Review Checklist
|
|
294
|
+
|
|
295
|
+
### 检测
|
|
296
|
+
- [ ] 开启了 SQL 日志或查询计数监控
|
|
297
|
+
- [ ] 测试中有查询数量断言
|
|
298
|
+
- [ ] APM 工具配置了 N+1 告警
|
|
299
|
+
|
|
300
|
+
### 修复
|
|
301
|
+
- [ ] ForeignKey / OneToOne 关系使用 JOIN eager loading
|
|
302
|
+
- [ ] M2M / 反向关系使用 IN 批量预加载
|
|
303
|
+
- [ ] 避免在循环中触发数据库查询
|
|
304
|
+
- [ ] 使用投影只获取需要的字段
|
|
305
|
+
|
|
306
|
+
### 架构
|
|
307
|
+
- [ ] 列表 API 分页,避免一次加载过多记录
|
|
308
|
+
- [ ] GraphQL 场景使用 DataLoader
|
|
309
|
+
- [ ] 缓存策略(Redis)处理高频读取的关联数据
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Hướng dẫn Review Hiệu năng (Performance Review Guide)
|
|
2
|
+
|
|
3
|
+
Bí kíp đánh giá hiệu năng toàn diện, bao phủ từ Frontend, Backend, Database, Độ phức tạp thuật toán (Algorithm complexity) cho đến hiệu năng API.
|
|
4
|
+
|
|
5
|
+
## Mục lục
|
|
6
|
+
|
|
7
|
+
- [Hiệu năng Frontend (Core Web Vitals)](#hiệu-năng-frontend-core-web-vitals)
|
|
8
|
+
- [Hiệu năng JavaScript](#hiệu-năng-javascript)
|
|
9
|
+
- [Quản lý Bộ nhớ (Memory Management)](#quản-lý-bộ-nhớ)
|
|
10
|
+
- [Hiệu năng Database](#hiệu-năng-database)
|
|
11
|
+
- [Hiệu năng API](#hiệu-năng-api)
|
|
12
|
+
- [Độ phức tạp Thuật toán](#độ-phức-tạp-thuật-toán)
|
|
13
|
+
- [Checklist Đánh giá Hiệu năng](#checklist-đánh-giá-hiệu-năng)
|
|
14
|
+
- [Anti-pattern Hiệu suất cấp thấp](#anti-pattern-hiệu-suất-cấp-thấp)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Hiệu năng Frontend (Core Web Vitals)
|
|
19
|
+
|
|
20
|
+
### Các chỉ số cốt lõi (Core Vitals 2024)
|
|
21
|
+
|
|
22
|
+
| Chỉ số | Tên đầy đủ | Mục tiêu | Ý nghĩa |
|
|
23
|
+
|------|------|--------|------|
|
|
24
|
+
| **LCP** | Largest Contentful Paint | ≤ 2.5s | Thời gian render phần tử lớn nhất trên màn hình |
|
|
25
|
+
| **INP** | Interaction to Next Paint | ≤ 200ms | Độ trễ phản hồi tương tác (Thay thế FID từ 2024) |
|
|
26
|
+
| **CLS** | Cumulative Layout Shift | ≤ 0.1 | Độ giật / Xê dịch layout cộng dồn |
|
|
27
|
+
| **FCP** | First Contentful Paint | ≤ 1.8s | Thời gian render nội dung đầu tiên |
|
|
28
|
+
| **TBT** | Total Blocking Time | ≤ 200ms | Tổng thời gian Main Thread bị block |
|
|
29
|
+
|
|
30
|
+
### Tối ưu LCP
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<!-- ❌ Lazy load hình ảnh LCP - Làm chậm nội dung quan trọng -->
|
|
34
|
+
<img src="hero.jpg" loading="lazy" />
|
|
35
|
+
|
|
36
|
+
<!-- ✅ Load ngay lập tức hình ảnh LCP -->
|
|
37
|
+
<img src="hero.jpg" fetchpriority="high" />
|
|
38
|
+
|
|
39
|
+
<!-- ❌ Định dạng ảnh cũ, nặng -->
|
|
40
|
+
<img src="hero.png" /> <!-- File PNG quá to -->
|
|
41
|
+
|
|
42
|
+
<!-- ✅ Dùng định dạng hiện đại + Responsive -->
|
|
43
|
+
<picture>
|
|
44
|
+
<source srcset="hero.avif" type="image/avif" />
|
|
45
|
+
<source srcset="hero.webp" type="image/webp" />
|
|
46
|
+
<img src="hero.jpg" alt="Hero" />
|
|
47
|
+
</picture>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Checklist LCP:**
|
|
51
|
+
- [ ] Phần tử LCP có được set `fetchpriority="high"` không?
|
|
52
|
+
- [ ] Đã dùng định dạng WebP/AVIF chưa?
|
|
53
|
+
- [ ] Có Server-Side Rendering (SSR) hoặc Static Site Generation (SSG) không?
|
|
54
|
+
- [ ] CDN đã cấu hình đúng chưa?
|
|
55
|
+
|
|
56
|
+
### Tối ưu FCP
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<!-- ❌ CSS chặn render (Render-blocking) -->
|
|
60
|
+
<link rel="stylesheet" href="all-styles.css" />
|
|
61
|
+
|
|
62
|
+
<!-- ✅ Inline CSS quan trọng (Critical CSS) + Load bất đồng bộ phần còn lại -->
|
|
63
|
+
<style>/* Critical CSS cho màn hình đầu tiên */</style>
|
|
64
|
+
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
|
|
65
|
+
|
|
66
|
+
<!-- ❌ Font chữ chặn render -->
|
|
67
|
+
@font-face {
|
|
68
|
+
font-family: 'CustomFont';
|
|
69
|
+
src: url('font.woff2');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
<!-- ✅ Tối ưu hiển thị Font -->
|
|
73
|
+
@font-face {
|
|
74
|
+
font-family: 'CustomFont';
|
|
75
|
+
src: url('font.woff2');
|
|
76
|
+
font-display: swap; /* Dùng font hệ thống trước, load xong sẽ tráo đổi */
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Tối ưu INP
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
// ❌ Long task block Main Thread
|
|
84
|
+
button.addEventListener('click', () => {
|
|
85
|
+
// Thao tác đồng bộ tốn 500ms
|
|
86
|
+
processLargeData(data);
|
|
87
|
+
updateUI();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ✅ Chia nhỏ Long task
|
|
91
|
+
button.addEventListener('click', async () => {
|
|
92
|
+
// Trả lại quyền điều khiển cho Main Thread
|
|
93
|
+
await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
|
|
94
|
+
|
|
95
|
+
// Xử lý từng lô nhỏ (chunk)
|
|
96
|
+
for (const chunk of chunks) {
|
|
97
|
+
processChunk(chunk);
|
|
98
|
+
await scheduler.yield?.();
|
|
99
|
+
}
|
|
100
|
+
updateUI();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ✅ Đẩy tính toán nặng sang Web Worker
|
|
104
|
+
const worker = new Worker('heavy-computation.js');
|
|
105
|
+
worker.postMessage(data);
|
|
106
|
+
worker.onmessage = (e) => updateUI(e.data);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Tối ưu CLS
|
|
110
|
+
|
|
111
|
+
```css
|
|
112
|
+
/* ❌ Hình ảnh/Video không có kích thước cố định */
|
|
113
|
+
img { width: 100%; }
|
|
114
|
+
|
|
115
|
+
/* ✅ Đặt trước không gian (Aspect ratio) */
|
|
116
|
+
img {
|
|
117
|
+
width: 100%;
|
|
118
|
+
aspect-ratio: 16 / 9;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ❌ Nội dung động gây đẩy layout (Shift) */
|
|
122
|
+
.ad-container { }
|
|
123
|
+
|
|
124
|
+
/* ✅ Dự phòng chiều cao cố định */
|
|
125
|
+
.ad-container {
|
|
126
|
+
min-height: 250px;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Hiệu năng JavaScript
|
|
133
|
+
|
|
134
|
+
### Code Splitting & Lazy Loading
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
// ❌ Load toàn bộ code một lúc
|
|
138
|
+
import { HeavyChart } from './charts';
|
|
139
|
+
import { PDFExporter } from './pdf';
|
|
140
|
+
|
|
141
|
+
// ✅ Tải ngầm theo nhu cầu (On-demand / Lazy)
|
|
142
|
+
const HeavyChart = lazy(() => import('./charts'));
|
|
143
|
+
const PDFExporter = lazy(() => import('./pdf'));
|
|
144
|
+
|
|
145
|
+
// ✅ Chia code theo Route (Route-based splitting)
|
|
146
|
+
const routes = [
|
|
147
|
+
{ path: '/dashboard', component: lazy(() => import('./pages/Dashboard')) }
|
|
148
|
+
];
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Tối ưu kích thước Bundle (Bundle Size)
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
// ❌ Import nguyên cả cái thư viện bự
|
|
155
|
+
import _ from 'lodash';
|
|
156
|
+
import moment from 'moment';
|
|
157
|
+
|
|
158
|
+
// ✅ Import đích danh hàm cần dùng
|
|
159
|
+
import debounce from 'lodash/debounce';
|
|
160
|
+
import { format } from 'date-fns';
|
|
161
|
+
|
|
162
|
+
// ❌ Tránh việc không dùng Tree Shaking
|
|
163
|
+
export default { fn1() {}, fn2() {} }; // fn2 không xài cũng bị gom vào bundle
|
|
164
|
+
|
|
165
|
+
// ✅ Export đích danh để hỗ trợ Tree Shaking
|
|
166
|
+
export function fn1() {}
|
|
167
|
+
export function fn2() {}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Tối ưu Render danh sách (List Rendering)
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
// ❌ Render hàng ngàn DOM Node cùng lúc
|
|
174
|
+
function List({ items }) {
|
|
175
|
+
return (
|
|
176
|
+
<ul>
|
|
177
|
+
{items.map(item => <li key={item.id}>{item.name}</li>)}
|
|
178
|
+
</ul>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ✅ Dùng Virtual List (Cuộn ảo) - Chỉ render phần đang nhìn thấy trên màn hình
|
|
183
|
+
import { FixedSizeList } from 'react-window';
|
|
184
|
+
|
|
185
|
+
function VirtualList({ items }) {
|
|
186
|
+
return (
|
|
187
|
+
<FixedSizeList height={400} itemCount={items.length} itemSize={35}>
|
|
188
|
+
{({ index, style }) => <div style={style}>{items[index].name}</div>}
|
|
189
|
+
</FixedSizeList>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Quản lý Bộ nhớ (Memory Management)
|
|
197
|
+
|
|
198
|
+
### Các dạng rò rỉ bộ nhớ (Memory Leaks) điển hình
|
|
199
|
+
|
|
200
|
+
**1. Quên gỡ Event Listener**
|
|
201
|
+
```javascript
|
|
202
|
+
// ❌ Unmount nhưng Event vẫn chạy
|
|
203
|
+
useEffect(() => { window.addEventListener('resize', handleResize); }, []);
|
|
204
|
+
|
|
205
|
+
// ✅ Cleanup Event Listener
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
window.addEventListener('resize', handleResize);
|
|
208
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
209
|
+
}, []);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**2. Quên xóa Timer (Interval/Timeout)**
|
|
213
|
+
```javascript
|
|
214
|
+
// ✅ Luôn luôn clearInterval
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
const timer = setInterval(fetchData, 5000);
|
|
217
|
+
return () => clearInterval(timer);
|
|
218
|
+
}, []);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**3. Rò rỉ qua Closure**
|
|
222
|
+
```javascript
|
|
223
|
+
// ❌ Closure giữ lại tham chiếu mảng khổng lồ không cần thiết
|
|
224
|
+
function createHandler() {
|
|
225
|
+
const largeData = new Array(1000000).fill('x');
|
|
226
|
+
return function handler() { console.log(largeData.length); }; // Giữ luôn nguyên cái mảng
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ✅ Chỉ giữ lại giá trị thực sự cần
|
|
230
|
+
function createHandler() {
|
|
231
|
+
const largeData = new Array(1000000).fill('x');
|
|
232
|
+
const length = largeData.length; // Chỉ lưu cái số đếm
|
|
233
|
+
return function handler() { console.log(length); };
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**4. Rò rỉ qua Connection (WebSocket/SSE)**
|
|
238
|
+
- Luôn gọi `ws.close()` trong hàm cleanup.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Hiệu năng Database
|
|
243
|
+
|
|
244
|
+
### Vấn đề N+1 Queries
|
|
245
|
+
> Xem chi tiết tại [N+1 Queries Checklist](n-plus-one-queries.md)
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
# ❌ N+1
|
|
249
|
+
users = User.objects.all() # 1 Query
|
|
250
|
+
for user in users:
|
|
251
|
+
print(user.profile.bio) # N Queries
|
|
252
|
+
|
|
253
|
+
# ✅ Eager Loading
|
|
254
|
+
users = User.objects.select_related('profile').all() # 2 Queries
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Tối ưu Index (Chỉ mục)
|
|
258
|
+
|
|
259
|
+
```sql
|
|
260
|
+
-- ❌ Full table scan (Quét toàn bảng)
|
|
261
|
+
SELECT * FROM orders WHERE status = 'pending';
|
|
262
|
+
|
|
263
|
+
-- ✅ Đánh Index
|
|
264
|
+
CREATE INDEX idx_orders_status ON orders(status);
|
|
265
|
+
|
|
266
|
+
-- ❌ Lỗi mất Index: Chạy Hàm trên cột
|
|
267
|
+
SELECT * FROM users WHERE YEAR(created_at) = 2024;
|
|
268
|
+
|
|
269
|
+
-- ✅ Đổi thành Range Query để xài được Index
|
|
270
|
+
SELECT * FROM users WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
|
|
271
|
+
|
|
272
|
+
-- ❌ Lỗi mất Index: LIKE '%...' (Có dấu % ở đầu)
|
|
273
|
+
SELECT * FROM products WHERE name LIKE '%phone%';
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Tối ưu Query
|
|
277
|
+
|
|
278
|
+
```sql
|
|
279
|
+
-- ❌ Dùng SELECT * lấy những cột không cần thiết
|
|
280
|
+
SELECT * FROM users WHERE id = 1;
|
|
281
|
+
|
|
282
|
+
-- ✅ Chỉ select các cột thực sự cần
|
|
283
|
+
SELECT id, name, email FROM users WHERE id = 1;
|
|
284
|
+
|
|
285
|
+
-- ❌ Lấy cả bảng siêu to mà không có LIMIT
|
|
286
|
+
SELECT * FROM logs WHERE type = 'error';
|
|
287
|
+
|
|
288
|
+
-- ✅ Luôn có Phân trang (Pagination/LIMIT)
|
|
289
|
+
SELECT * FROM logs WHERE type = 'error' LIMIT 100 OFFSET 0;
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## Hiệu năng API
|
|
295
|
+
|
|
296
|
+
### Phân trang (Pagination)
|
|
297
|
+
Tuyệt đối KHÔNG BAO GIỜ trả về nguyên cái Array không giới hạn (Unbounded Array). Luôn luôn nhận vào `page` và `limit` (max 100).
|
|
298
|
+
|
|
299
|
+
### Caching (Bộ nhớ đệm)
|
|
300
|
+
```javascript
|
|
301
|
+
// ✅ Dùng Redis
|
|
302
|
+
const cached = await redis.get(cacheKey);
|
|
303
|
+
if (cached) return JSON.parse(cached);
|
|
304
|
+
|
|
305
|
+
// ✅ Cấu hình HTTP Cache Headers
|
|
306
|
+
res.set({
|
|
307
|
+
'Cache-Control': 'public, max-age=86400', // 24 giờ
|
|
308
|
+
'ETag': 'abc123',
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Nén phản hồi (Response Compression)
|
|
313
|
+
```javascript
|
|
314
|
+
// ✅ Kích hoạt Gzip / Brotli
|
|
315
|
+
const compression = require('compression');
|
|
316
|
+
app.use(compression());
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Giới hạn Tốc độ (Rate Limiting)
|
|
320
|
+
- Bảo vệ API bằng cách giới hạn số lượng request từ 1 IP/User trong khoảng thời gian nhất định.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Độ phức tạp Thuật toán (Algorithm Complexity)
|
|
325
|
+
|
|
326
|
+
| Độ phức tạp | Ký hiệu | Ý nghĩa | 10 record | 1000 record | Ví dụ |
|
|
327
|
+
|--------|------|-------|-------|---------|------|
|
|
328
|
+
| **O(1)** | Constant | Cố định | 1 | 1 | Hash map (Dict/Map) / Tra cứu Index |
|
|
329
|
+
| **O(log n)**| Logarithmic | Tăng rất chậm | 3 | 10 | Tìm kiếm nhị phân |
|
|
330
|
+
| **O(n)** | Linear | Tuyến tính | 10 | 1000 | Vòng lặp for 1 cấp |
|
|
331
|
+
| **O(n log n)**| Linearithmic | Hơi tốn | 33 | 10000 | Sort (Sắp xếp) |
|
|
332
|
+
| **O(n²)** | Quadratic | Rất tốn kém | 100 | 1.000.000 | Vòng lặp lồng nhau (Nested loops) |
|
|
333
|
+
|
|
334
|
+
### Nhận diện O(n²) trong Code Review
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
// ❌ O(n²) - Gọi includes() bên trong vòng lặp for
|
|
338
|
+
function removeDuplicates(arr) {
|
|
339
|
+
const result = [];
|
|
340
|
+
for (const item of arr) {
|
|
341
|
+
if (!result.includes(item)) { // .includes() là O(n)
|
|
342
|
+
result.push(item);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ✅ O(n) - Dùng Set (Cấu trúc dữ liệu O(1) tra cứu)
|
|
349
|
+
function removeDuplicates(arr) {
|
|
350
|
+
return [...new Set(arr)];
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
// ❌ O(n) - Dùng find() bừa bãi
|
|
356
|
+
function getUser(id) {
|
|
357
|
+
return users.find(u => u.id === id); // Duyệt mảng từ đầu tới cuối
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ✅ O(1) - Biến mảng thành Map để tra cứu siêu tốc
|
|
361
|
+
const userMap = new Map(users.map(u => [u.id, u]));
|
|
362
|
+
function getUser(id) {
|
|
363
|
+
return userMap.get(id);
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Anti-pattern Hiệu suất cấp thấp
|
|
370
|
+
|
|
371
|
+
### Làm việc thừa (Unnecessary repetitive work)
|
|
372
|
+
- Đọc file `config.json` bên trong vòng lặp thay vì đọc 1 lần đưa ra ngoài.
|
|
373
|
+
- Gọi lại cùng 1 câu lệnh DB Query nhiều lần trong 1 Request.
|
|
374
|
+
|
|
375
|
+
### Bỏ lỡ cơ hội chạy Song song (Missed Concurrency)
|
|
376
|
+
- Đợi 2 cái Promise độc lập nhau một cách tuần tự (`await A; await B`).
|
|
377
|
+
- Hãy dùng `Promise.all([A, B])` hoặc `Task.WhenAll()`.
|
|
378
|
+
|
|
379
|
+
### Bùng nổ Dữ liệu không giới hạn (Unbounded data structures)
|
|
380
|
+
```python
|
|
381
|
+
# ❌ Cache vào Dictionary mút mùa không bao giờ đầy, gây Out of Memory (OOM)
|
|
382
|
+
_cache = {}
|
|
383
|
+
|
|
384
|
+
# ✅ Dùng LRU Cache có giới hạn max size
|
|
385
|
+
from functools import lru_cache
|
|
386
|
+
@lru_cache(maxsize=128)
|
|
387
|
+
```
|