Django performance is an end-to-end problem. The framework can return a small HTML response quickly, but slow queries, request-time API calls, unversioned assets, oversized images, or too much browser JavaScript can still make a page feel slow.
The reliable approach is to trace the production request, fix the largest measured constraint, and retest under the same conditions.
Separate backend and browser time
Begin with the request waterfall and a server trace. Time to First Byte can include DNS, connection setup, proxy and CDN behavior, queueing, application work, database calls, and upstream services. Do not assume Django itself is the bottleneck.
Record at least:
- total request duration at the edge and application;
- database query count and cumulative query time;
- cache hit or miss state;
- external service duration;
- response size and compression;
- LCP, CLS, TBT, FCP, and Speed Index in the browser.
PageSpeed Insights combines a Lighthouse lab run with field data when the URL has enough Chrome UX Report traffic. Read How to Check Website Speed before comparing runs, and use a server profiler or application telemetry to connect browser symptoms to backend causes.
Remove avoidable database work
Query count often matters more than the choice of web server. Inspect representative production views and API endpoints for N+1 access patterns.
Use select_related() for appropriate single-valued relationships and prefetch_related() for collections. Fetch only the columns required by the response when a wide model or large text field is involved. Add indexes for real filter, join, and ordering patterns after inspecting query plans.
articles = (
Article.objects
.filter(status="published")
.select_related("author")
.prefetch_related("categories")
.only("slug", "title", "summary", "author__name")
)
only() is not a universal optimization; accessing a deferred field later triggers another query. Verify the query log and rendered template together.
Keep expensive aggregation, feed generation, and media processing out of the request path when the result can be prepared asynchronously. Pagination should have bounded page sizes, and list views should avoid computing data that is not rendered.
Cache at the right layer
Django supports per-site, per-view, template-fragment, and low-level caching. The Django cache documentation explains their different scopes.
Choose the narrowest cache that removes meaningful repeated work:
- cache public page responses only when they are safe to share;
- use fragment caching for an expensive section inside an otherwise personalized page;
- cache computed query results with stable keys and bounded lifetimes;
- let the CDN cache versioned public assets for a long time;
- invalidate or version cache keys when the underlying content changes.
Never share a response that varies by account, locale, authorization, or cookie unless those dimensions are correctly represented in the cache key and downstream Vary behavior.
Django database connections also need topology-aware tuning. CONN_MAX_AGE controls persistent connection lifetime, and current Django supports health checks for reused connections. Persistent connections can reduce setup cost, but every worker may hold a connection. If a proxy or transaction pooler is present, align connection lifetime and capacity with that layer rather than copying a value from another deployment.
Deploy static files correctly
Modern Django uses the STORAGES setting. STATICFILES_STORAGE and DEFAULT_FILE_STORAGE were removed and should not appear in current examples.
For local or origin-served static assets, hashed filenames enable long-lived browser caching:
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
},
}
Run collectstatic as a release step, then serve the collected files from a static server, object storage, or CDN. Django's official static-file deployment guide describes that separation. Verify that HTML references hashed URLs and that those immutable objects receive long cache lifetimes.
Do not use Django's development static-file server in production. Avoid setting a long cache lifetime on mutable, unhashed filenames because clients may keep old CSS or JavaScript after a deployment.
Build an image pipeline
ImageField can validate image uploads and expose dimensions, but it requires the separately installed Pillow library. Pillow is not an automatic Django dependency. More importantly, ImageField is not a complete delivery pipeline.
At ingestion or in a background job:
- validate the decoded file, dimensions, and resource limits;
- normalize orientation and strip metadata that should not be public;
- generate the sizes the design actually uses;
- encode suitable WebP or AVIF variants with a fallback where needed;
- store width, height, format, and descriptive alternative text metadata;
- serve variants through
srcsetandsizes.
<img
src="/media/products/analytics-dashboard-960.webp"
srcset="/media/products/analytics-dashboard-640.webp 640w,
/media/products/analytics-dashboard-960.webp 960w,
/media/products/analytics-dashboard-1440.webp 1440w"
sizes="(max-width: 720px) 100vw, 960px"
width="1440"
height="900"
alt="Analytics dashboard with traffic and conversion charts"
>
Do not lazy-load the image that is likely to become LCP. Lazy-load appropriate off-screen images, and preserve intrinsic dimensions to reduce layout shifts.
Tune the application server with load tests
Gunicorn documents (2 × CPU cores) + 1 as a starting heuristic for synchronous workers, not a universally correct production setting. Its worker design guide explicitly recommends tuning for the actual hardware and workload.
More workers can increase throughput until memory use, database connections, context switching, or downstream capacity becomes the limit. Choose worker type based on behavior: long blocking upstream calls, streaming, and WebSockets have different requirements from short synchronous requests.
Test through the same reverse proxy and application settings used in production. Watch latency percentiles, errors, CPU, memory, connection pools, and queue time while gradually increasing concurrency. A configuration that wins a synthetic throughput test but exhausts the database is not an improvement.
Control frontend work
Django templates do not protect a page from browser-side regressions. Bundle and minify application assets, split code by page where practical, and avoid loading an entire UI library for one control. Use defer for ordered non-blocking scripts that do not need to run during parsing; use async only when execution order is unimportant.
Reserve dimensions for images, videos, embeds, and injected banners. Self-host or subset fonts where licensing permits, preload only truly critical resources, and measure every third-party script after consent and production configuration are active.
Lighthouse's lab score is useful for diagnosing these issues, but it is not the score Google directly uses for ranking. Does Page Speed Affect SEO? explains how field Core Web Vitals relate to Search.
Use a production checklist
Before calling a Django performance change complete, verify:
- query count and query plans for representative pages;
- cache keys, lifetimes, invalidation, and personalization boundaries;
STORAGES,collectstatic, hashed assets, and CDN headers;- image variants, dimensions, loading behavior, and alternative text;
- application worker and database connection capacity under load;
- compression and proxy behavior on actual production responses;
- repeated lab tests under equivalent conditions;
- field Core Web Vitals separately from Lighthouse scores.
Browse recorded measurements on IndieTools Speed, then use the individual LCP, CLS, and TBT guides to investigate the metric that actually regressed.


