Django Performance 效能調校全方位指南 | Django 教學
效能調校是 Django 應用從開發階段走向生產環境的關鍵環節。一個原則貫穿始終:量測先於優化(Measure Before Optimize)。盲目優化不僅浪費時間,還可能引入新的問題。本篇將從效能調校的正確思維出發,介紹 Django Debug Toolbar 效能診斷工具、資料庫索引(Database Index) 策略、快取(Cache) 機制回顧、靜態檔案優化(WhiteNoise、CDN)、模板效能技巧、Middleware 精簡、連線持久化(Connection Persistence) 以及 Gzip 壓縮 等全方位的效能調校手段,幫助你系統性地提升 Django 應用的回應速度與吞吐量。
效能調校思維:量測先於優化
在動手優化之前,最重要的一件事是:找到真正的瓶頸。Donald Knuth 曾說過:「過早優化是萬惡之源。」效能調校的正確流程是:
- 量測:使用工具找出效能瓶頸所在
- 分析:確認瓶頸的根本原因
- 優化:針對性地解決問題
- 驗證:量測優化後的效果,確認改善
常見的效能瓶頸通常出現在以下幾個層面:
| 層面 | 常見問題 | 診斷工具 |
|---|---|---|
| 資料庫 | N+1 查詢、缺少索引、慢查詢 | Django Debug Toolbar、EXPLAIN |
| 應用邏輯 | 不必要的計算、重複查詢 | django-silk、cProfile |
| 網路 | 靜態檔案未壓縮、未快取 | 瀏覽器 DevTools、Lighthouse |
| 模板 | 複雜模板邏輯、重複渲染 | Django Debug Toolbar Template 面板 |
Django Debug Toolbar 安裝與使用
Django Debug Toolbar 是 Django 開發者的必備效能診斷工具,它會在頁面側邊顯示一個可展開的工具面板,提供 SQL 查詢、模板渲染、快取命中率等詳細資訊。
安裝與設定
pip install django-debug-toolbar
# settings.py(僅開發環境)
INSTALLED_APPS += ['debug_toolbar']
# 必須放在 Middleware 列表的最前面
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
] + MIDDLEWARE
# 只對本機 IP 顯示 Debug Toolbar
INTERNAL_IPS = ['127.0.0.1']
# urls.py
from django.conf import settings
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns
關鍵面板解讀
| 面板 | 功能 | 重點關注 |
|---|---|---|
| SQL | 列出所有 SQL 查詢 | 查詢數量、重複查詢(紅色背景)、執行時間 |
| Timer | 頁面總載入時間 | 各階段耗時分佈 |
| Template | 模板渲染資訊 | 使用的模板數量、Context 變數 |
| Cache | 快取命中/未命中 | 命中率、快取操作次數 |
| Signal | Django Signal 觸發 | Signal 的執行時間 |
SQL 面板是最常用的,它會用紅色背景標記相似查詢超過 2 次的情況,這通常是 N+1 問題的徵兆。
資料庫索引策略
索引(Index) 是提升資料庫查詢效能最直接的手段。沒有索引的查詢需要逐筆掃描整張表(Full Table Scan,全表掃描),而有索引的查詢可以直接定位到目標資料。
單欄索引
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True) # unique=True 自動建立索引
status = models.CharField(max_length=20, db_index=True) # db_index 建立單欄索引
created_at = models.DateTimeField(auto_now_add=True)
Meta.indexes 複合索引
當查詢條件經常同時包含多個欄位時,複合索引(Composite Index) 比多個單欄索引更有效率:
class Article(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(User, on_delete=models.CASCADE)
status = models.CharField(max_length=20)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# 複合索引:用於 filter(author=x, status=y) 查詢
models.Index(
fields=['author', 'status'],
name='article_author_status_idx',
),
# 降序索引:用於 order_by('-created_at') 查詢
models.Index(
fields=['-created_at'],
name='article_created_desc_idx',
),
# 條件索引(Django 4.0+,僅 PostgreSQL)
models.Index(
fields=['created_at'],
name='article_published_idx',
condition=models.Q(status='published'),
),
]
索引使用原則
- 高選擇性欄位優先:狀態(status)的值種類少,選擇性低;使用者 ID 的值種類多,選擇性高
- 頻繁查詢的 WHERE 和 ORDER BY 欄位:根據實際查詢模式建立索引
- 避免過度索引:每個索引都會增加寫入成本(INSERT、UPDATE、DELETE 變慢)
- 使用 EXPLAIN 驗證:確認索引是否被實際使用
# 在 Django Shell 中查看 SQL 和 EXPLAIN
from django.db import connection
qs = Article.objects.filter(author_id=1, status='published')
print(qs.query) # 查看生成的 SQL
with connection.cursor() as cursor:
cursor.execute("EXPLAIN ANALYZE " + str(qs.query))
for row in cursor.fetchall():
print(row[0])
# Index Scan using article_author_status_idx → 索引生效
# Seq Scan on article → 全表掃描,需要建立索引!
快取策略回顧
快取是效能優化最有效的手段之一。如果你尚未閱讀快取相關的章節,建議先回顧第 32 篇的快取機制詳解。這裡快速回顧幾個關鍵策略:
快取層級一覽
| 層級 | 方式 | 適用場景 |
|---|---|---|
| 全站快取 | UpdateCacheMiddleware + FetchFromCacheMiddleware | 內容不常變動的靜態網站 |
| View 快取 | @cache_page(60 * 15) | 特定頁面快取 15 分鐘 |
| 模板片段快取 | {% cache 300 sidebar %} | 頁面中局部區塊的快取 |
| 低階 API | cache.get() / cache.set() | 自訂快取邏輯 |
# View 快取範例
from django.views.decorators.cache import cache_page
@cache_page(60 * 15) # 快取 15 分鐘
def article_list(request):
articles = Article.objects.filter(status='published')
return render(request, 'articles/list.html', {'articles': articles})
# 低階快取 API 範例
from django.core.cache import cache
def get_popular_articles():
cache_key = 'popular_articles'
articles = cache.get(cache_key)
if articles is None:
articles = list(
Article.objects.filter(status='published')
.order_by('-view_count')[:10]
.values('id', 'title', 'view_count')
)
cache.set(cache_key, articles, timeout=60 * 30) # 快取 30 分鐘
return articles
靜態檔案優化
靜態檔案(CSS、JavaScript、圖片)的載入速度直接影響使用者體驗。以下介紹兩種主要的優化策略。
WhiteNoise:簡化靜態檔案服務
WhiteNoise 讓 Django/Gunicorn 可以直接服務靜態檔案,無需額外的 Nginx 設定,並自動支援 Gzip 壓縮和檔名 hash 長期快取:
pip install whitenoise
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # 緊接在 SecurityMiddleware 之後
# ... 其他 Middleware
]
# 開啟壓縮 + Manifest(檔名加 hash,支援長期快取)
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WhiteNoise 的工作原理:
collectstatic收集所有靜態檔案到STATIC_ROOT- WhiteNoise 在啟動時掃描所有檔案,產生 Gzip/Brotli 壓縮版本
- 為每個檔案產生帶 hash 的檔名(如
style.abc123.css),可設定超長快取時間 - 請求靜態檔案時,直接從記憶體中回應
CDN 加速
對於高流量的生產環境,建議搭配 CDN(Content Delivery Network,內容分發網路) 將靜態檔案分發到全球節點:
# settings.py - 搭配 AWS S3 + CloudFront
STATIC_URL = 'https://d1234567.cloudfront.net/static/'
# 或使用 django-storages 自動上傳至 S3
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'
AWS_STORAGE_BUCKET_NAME = 'my-static-bucket'
AWS_S3_CUSTOM_DOMAIN = 'd1234567.cloudfront.net'
模板效能優化
Django 模板引擎的效能通常不是主要瓶頸,但在高流量場景下,一些優化技巧可以帶來顯著改善。
{% cache %} 模板片段快取
將不常變動的模板區塊包裹在 {% cache %} 標籤中,避免重複渲染:
{% load cache %}
<!-- 快取側邊欄 300 秒,快取 key 為 "sidebar" -->
{% cache 300 sidebar %}
<div class="sidebar">
{% for category in categories %}
<a href="{{ category.get_absolute_url }}">
{{ category.name }} ({{ category.article_count }})
</a>
{% endfor %}
</div>
{% endcache %}
<!-- 依據使用者 ID 產生不同的快取版本 -->
{% cache 600 user_dashboard request.user.id %}
<div class="dashboard">
<!-- 使用者專屬的儀表板內容 -->
</div>
{% endcache %}
with 標籤減少重複求值
{% with %} 標籤可以將複雜表達式的結果暫存為變數,避免在模板中重複計算:
<!-- 不佳:expensive_method() 被呼叫多次 -->
{% if article.get_comment_count > 0 %}
<p>{{ article.get_comment_count }} 則留言</p>
{% endif %}
<!-- 改善:只呼叫一次 -->
{% with comment_count=article.get_comment_count %}
{% if comment_count > 0 %}
<p>{{ comment_count }} 則留言</p>
{% endif %}
{% endwith %}
Middleware 效能影響
每個 Middleware 都會在每次請求/回應周期中被執行,因此過多或不必要的 Middleware 會拖慢效能。
精簡 Middleware 原則
# settings.py - 審視每個 Middleware 是否必要
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # 必要:安全
'whitenoise.middleware.WhiteNoiseMiddleware', # 必要:靜態檔案
'django.contrib.sessions.middleware.SessionMiddleware', # 必要:Session
'django.middleware.common.CommonMiddleware', # 必要:URL 正規化
'django.middleware.csrf.CsrfViewMiddleware', # 必要:CSRF 防護
'django.contrib.auth.middleware.AuthenticationMiddleware', # 必要:認證
'django.contrib.messages.middleware.MessageMiddleware', # 必要:訊息框架
'django.middleware.clickjacking.XFrameOptionsMiddleware', # 必要:Clickjacking 防護
# 'django.middleware.locale.LocaleMiddleware', # 未使用多語系可移除
# 'debug_toolbar.middleware.DebugToolbarMiddleware', # 僅開發環境
]
自訂 Middleware 的效能考量
如果你撰寫了自訂 Middleware,要注意避免在其中執行耗時操作:
# 不佳:每次請求都查詢資料庫
class SlowMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 每次請求都執行 DB 查詢 — 效能殺手!
request.site_config = SiteConfig.objects.first()
return self.get_response(request)
# 改善:搭配快取
class CachedMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
from django.core.cache import cache
config = cache.get('site_config')
if config is None:
config = SiteConfig.objects.first()
cache.set('site_config', config, timeout=300) # 快取 5 分鐘
request.site_config = config
return self.get_response(request)
連線持久化(CONN_MAX_AGE)
預設情況下,Django 在每次請求結束後會關閉資料庫連線,下次請求再重新建立。這在高流量場景下會造成大量的連線建立/銷毀開銷。CONN_MAX_AGE 設定可以讓 Django 在指定時間內重複使用同一條連線:
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'USER': 'myuser',
'PASSWORD': 'mypassword',
'HOST': 'localhost',
'PORT': '5432',
'CONN_MAX_AGE': 60, # 連線保持 60 秒
# CONN_MAX_AGE = 0 # 預設:每次請求後關閉
# CONN_MAX_AGE = None # 永久保持(需注意資料庫連線上限)
}
}
CONN_MAX_AGE 注意事項
| 設定值 | 行為 | 適用場景 |
|---|---|---|
0(預設) | 每次請求結束後關閉連線 | 開發環境 |
60 ~ 600 | 連線保持 1~10 分鐘 | 大多數生產環境 |
None | 永久保持連線 | 搭配連線池使用 |
搭配 Gunicorn 多 Worker 模式時,每個 Worker 都會持有自己的連線,因此總連線數 = Workers 數量 x 每個 Worker 的連線數。要確保不超過資料庫的最大連線數限制。
對於更進階的連線管理,可以使用外部連線池如 PgBouncer:
# 使用 django-db-connection-pool
# pip install django-db-connection-pool
DATABASES = {
'default': {
'ENGINE': 'dj_db_conn_pool.backends.postgresql',
'NAME': 'mydb',
'POOL_OPTIONS': {
'POOL_SIZE': 10, # 連線池基本大小
'MAX_OVERFLOW': 10, # 允許超出的連線數
'RECYCLE': 24 * 60 * 60, # 連線回收週期(1 天)
}
}
}
Gzip 壓縮
Gzip 壓縮 可以大幅減少 HTTP 回應的大小,加快傳輸速度。Django 提供了內建的 Gzip Middleware:
# settings.py
MIDDLEWARE = [
'django.middleware.gzip.GZipMiddleware', # 放在最前面
'django.middleware.security.SecurityMiddleware',
# ... 其他 Middleware
]
不過,更推薦在反向代理層(Nginx)處理 Gzip 壓縮,因為 Nginx 的壓縮效率更高,且不會佔用 Django Worker 的資源:
# nginx.conf
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1000; # 小於 1KB 的回應不壓縮
gzip_comp_level 6; # 壓縮等級(1-9,6 是效能與壓縮率的平衡點)
gzip_vary on; # 加入 Vary: Accept-Encoding 標頭
效能調校檢查清單
以下是一份實用的效能調校檢查清單,建議在專案上線前逐項確認:
| 類別 | 項目 | 狀態 |
|---|---|---|
| 資料庫 | 使用 select_related / prefetch_related 消除 N+1 | |
| 資料庫 | 為常用查詢欄位建立索引 | |
| 資料庫 | 啟用 CONN_MAX_AGE 連線持久化 | |
| 快取 | 為高頻讀取的資料設定快取 | |
| 快取 | 使用模板片段快取({% cache %}) | |
| 靜態檔案 | 啟用 WhiteNoise 或 Nginx 靜態檔案服務 | |
| 靜態檔案 | 開啟 Gzip 壓縮 | |
| 靜態檔案 | 設定合理的快取標頭(Cache-Control、ETag) | |
| 應用 | 精簡不必要的 Middleware | |
| 應用 | 使用 values() / only() 減少不必要的欄位載入 | |
| 監控 | 開發環境安裝 Django Debug Toolbar | |
| 監控 | 使用 assertNumQueries 防止查詢數回歸 |
總結
Django 效能調校的核心原則是 量測先於優化。透過 Django Debug Toolbar 找出效能瓶頸後,再有針對性地進行優化。資料庫層面,善用索引策略、連線持久化(CONN_MAX_AGE)和連線池能有效降低查詢延遲;快取層面,從全站快取到模板片段快取,根據資料更新頻率選擇合適的快取策略;靜態檔案層面,WhiteNoise 提供了簡單高效的解決方案,搭配 CDN 則能進一步加速全球存取;應用層面,精簡 Middleware、使用模板 {% cache %} 和 {% with %} 標籤都能帶來可觀的效能提升。下一篇我們將深入探討 Django 最常見的效能問題 — N+1 查詢,學習如何用 select_related 和 prefetch_related 徹底解決它。