Django performance optimization - improve application response speed and concurrency capabilities | Daoman PythonAI

#django performance optimization - improve application response speed and concurrency capabilities

📂 Stage: Part 3 - Advanced Topics | 🎯 Difficulty level: Advanced | ⏰ Estimated learning time: 6-8 hours 🎒 Prerequisite knowledge: 缓存策略, 数据库查询优化


##Performance Optimization Overview {#Performance Optimization Overview}

The response speed of the application is directly related to the user experience and server cost. A page that takes half a second to open may lose a large number of users; and an unnecessary database query will exponentially increase the waste of resources in high concurrency scenarios.

Before doing any optimization, remember this iron rule: Measure first, optimize later. Performance tuning without data support is often just luck.

Core optimization direction

According to experience, 80% of performance problems focus on the following three directions, which we call the "three major bottlenecks":

  1. Database Query: The most common N+1 problems, missing indexes, repeated queries, etc.
  2. Caching strategy: low cache hit rate, cache penetration, cache avalanche, etc.
  3. Resource loading: The static file size is too large, the third-party API response is slow, etc.

In the following content, we will overcome these problems one by one to make your Django application run faster and more stably.


Database query optimization

The database is the performance black hole of most Django applications, but as long as you master the correct usage of ORM, you can double the query efficiency.

Solving N+1 queries

N+1 is the easiest pitfall for novices: first find a group of objects, and then access related objects one by one in a loop, resulting in an explosive increase in the number of database queries.

# ❌ 错误做法:每次都发起新查询
posts = Post.objects.all()
for post in posts:
    # 每次循环都会执行一次 SQL 查询 author 和 tags
    print(post.author.username)
    print([t.name for t in post.tags.all()])

The correct solution is to useselect_relatedorprefetch_related

  • Foreign Key, One to One: Useselect_related(SQL JOIN, one query brings back all the data).
  • Many-to-many, reverse one-to-many: useprefetch_related(Python level splicing after additional queries).
from django.db.models import Prefetch
from django.contrib.auth.models import User

class Tag(models.Model):
    name = models.CharField(max_length=50)

class Comment(models.Model):
    post = models.ForeignKey('Post', on_delete=models.CASCADE)

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    tags = models.ManyToManyField(Tag)
    comments = models.ManyToManyField(Comment)

# ✅ 正确做法:一次性批量加载关联数据
posts = Post.objects.select_related('author')\
    .prefetch_related(
        'tags',
        Prefetch('comments', queryset=Comment.objects.select_related('user').order_by('-created_at')[:5])
    ).filter(status='published')

This way, no matter how many articles there are, the associated authors, tags, and the latest 5 comments are all done with very few queries.

Loading field optimization

Many times we don't need all fields of a model, especially fields that contain large text or binary content. Loading only the columns you need can significantly reduce the amount of data transferred.

# ✅ 只查询 username 和 email
light_users = User.objects.only('username', 'email')

# ✅ 排除大字段 content
articles = Article.objects.defer('content')

only()anddefer()Ability to finely control SQL SELECT fields to avoid useless data slowing down the process.

Batch operations and aggregation

Called in a loopsave()It is a performance killer, so try to use batch operations.

# 批量更新:一条 SQL 完成所有修改
Post.objects.filter(status='draft').update(status='archived')

# 聚合统计:在数据库层面完成计算
from django.db.models import Count
categories = Category.objects.annotate(post_count=Count('posts'))

Always remember: **Things that can be solved with a single SQL statement should not be handled in a Python loop. **

Performance analysis tools

During the development phase, you can use django’s own SQL log to view the query status:

from django.db import connection

# 打印当前请求的所有 SQL(调试用)
print(connection.queries)

But what is more recommended is a dedicated tool:

  • django-debug-toolbar: Essential for development environment, intuitively displays SQL query, cache, request time, etc.
  • django-silk: Suitable for lightweight performance analysis in production environments, records request details and supports playback.

Cache strategy optimization

A reasonable caching strategy can reduce response time from seconds to milliseconds. But the most difficult part of caching is how to "invalidate" it, that is, to ensure that the data seen by users is new enough.

Common caching methods

Django provides a variety of caching methods from the whole site to the function level:

from django.core.cache import cache
from django.views.decorators.cache import cache_page

# 1️⃣ 视图缓存:整个页面缓存 15 分钟
@cache_page(60 * 15)
def hot_products(request):
    products = Product.objects.filter(views__gt=1000)
    return render(request, 'hot_products.html', {'products': products})

In the template, you can also cache only part of the fragment to avoid caching the entire page and causing confusion in the login status:

{% load cache %}
{% cache 600 hot_product_sidebar request.user.id %}
    <!-- 这里的内容按用户ID独立缓存,10分钟后更新 -->
{% endcache %}

More flexible is to manually control the caching logic, such as caching user data:

def get_user_profile(user_id):
    key = f'profile:{user_id}'
    profile = cache.get(key)
    if not profile:
        profile = UserProfile.objects.get(user_id=user_id)
        cache.set(key, profile, 60 * 5)  # 缓存 5 分钟
    return profile

Avoid cache avalanche and penetration

Under high concurrency, cache failure may cause serious consequences:

  • Cache Penetration: Query a non-existent data, which is not in the cache, and will be penetrated to the database every time.
  • Cache Avalanche: A large number of caches expire at the same time, and all requests are pressed to the database instantly.

Coping strategies:

# 解决缓存穿透:缓存空值
def get_product(product_id):
    key = f'product:{product_id}'
    product = cache.get(key)
    if product is None:
        try:
            product = Product.objects.get(id=product_id)
            cache.set(key, product, 60 * 5)
        except Product.DoesNotExist:
            # 缓存空值,避免反复查询不存在的 id
            cache.set(key, 'empty', 60)
    if product == 'empty':
        return None
    return product
# 解决缓存雪崩:加上随机过期时间
import random
timeout = 60 * 5 + random.randint(0, 60)  # 5 分钟 + 随机偏移
cache.set(key, value, timeout)

In this way, even if a large number of caches expire at the same time, the actual expiration time will be dispersed within a time window to avoid instantaneous pressure.


Asynchronous processing optimization

If I/O-intensive tasks such as sending emails, calling third-party APIs, and processing images are executed synchronously, the request processing thread will be seriously blocked. Making these tasks asynchronous can release main thread resources and improve concurrency capabilities.

Celery asynchronous tasks

Celery is the most mature asynchronous task queue in the Django ecosystem and is usually used in conjunction with Redis or RabbitMQ.

First install the dependencies and create a Celery application instance:

# celery.py
import os
from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

Configure the message broker (Broker) and result storage:

# settings.py
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/1'
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/2'

Define specific tasks:

# tasks.py
from celery import shared_task

@shared_task
def send_welcome_email(email):
    # 模拟耗时操作
    import time
    time.sleep(3)
    print(f'Welcome email sent to {email}')

Called asynchronously in views or business logic:

# views.py
def register(request):
    # ... 用户注册逻辑
    send_welcome_email.delay(user.email)  # 立即返回,任务在后台执行
    return redirect('home')

pass.delay()When called, the request thread will not be blocked due to sending emails, and the response speed of the page will be immediate.


Static file optimization

Static files (CSS, JS, images) often take up a lot of page loading time. Optimizing their transmission efficiency can directly improve user perception speed.

Basic configuration

Turn on file fingerprinting (Hash) so that the browser can permanently cache static files:

# settings.py
from django.contrib.staticfiles.storage import ManifestStaticFilesStorage

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = ManifestStaticFilesStorage

In this way, every time the file content changes, the file name will have the corresponding hash value to avoid CDN or browser cache failure problems.

CDN acceleration

In a production environment, it is recommended to upload static files to CDN and modify the static file URL:

# settings.py(生产配置)
STATIC_URL = 'https://cdn.yourdomain.com/static/'

With the support of HTTP/2 or HTTP/3, the loading speed of static resources will be qualitatively improved.


Deployment optimization

Code-level optimization is only the first step. How it is run and deployed also affects performance. Recommended classic architecture for production environment: Gunicorn + Nginx + Redis + PostgreSQL.

Gunicorn configuration

passgunicorn.conf.pyFine control of work process:

# gunicorn.conf.py
bind = "127.0.0.1:8000"
workers = 2 * os.cpu_count() + 1  # 推荐的 worker 数量
worker_class = "sync"             # IO 密集可换 gevent
timeout = 30
max_requests = 1000               # 达到请求数后重启,防止内存泄漏
  • workersThe number is set according to the number of CPU cores, generally2n+1
  • worker_classchoosesyncSuitable for CPU-intensive applications. If there is a large amount of IO waiting, you can switch togevent
  • max_requestsWorkers can be recycled regularly to avoid memory expansion caused by long-term running of the Python process.

Cooperating with Nginx reverse proxy to handle static files, SSL termination and load balancing, a highly available service cluster can be built.


Summary of this chapter

Performance optimization is not a one-time job, but a continuous process. Following the cycle of “measurement → optimization → verification”, we start from the following four aspects:

  1. Database Optimization: Eliminate N+1, check only necessary fields, use batch operations and aggregation.
  2. Caching Strategy: Reasonably select cache granularity to prevent penetration and avalanche.
  3. Asynchronous processing: Use Celery to move time-consuming tasks out of the request thread.
  4. Deployment Optimization: Select the appropriate WSGI server and configuration parameters.

At the same time, make good use ofdjango-debug-toolbardjango-silkTools such as Monitor Performance Changes allow your application to remain lightweight during continuous iterations.


🔗 Related tutorials

🏷️ tag cloud:django性能优化 数据库优化 缓存策略 异步处理 Celery