On this episode, our focus will be on static files. Static files are vital to your application, but they have little to do with Python code. We’ll see what they are and what they do.
Listen at Spotify.
Last Episode
On the last episode, we looked at Django middleware. We discussed why middleware is useful and how you can work with it.
What Are Static Files?
Static files are files that don’t change when your application is running.
These files do a lot to improve your application, but they aren’t dynamically generated by your Python web server. In a typical web application, your most common static files will be the following types:
- Cascading Style Sheets, CSS
- JavaScript
- Images
Static files are crucial to your Django project because the modern web requires more than dynamically generated HTML markup. Do you visit any website that has zero styling of its HTML? These kinds of sites exist and can be awesome for making a quick tool, but most users expect websites to be aesthetically pleasing. For us, that means that we should be prepared to include some CSS styling at a minimum.
Configuration
To use static files
in your project,
you need the django.contrib.staticfiles
app
in your project’s INSTALLED_APPS
list.
This is another one
of the default Django applications
that Django will include
if you start from the startproject
command.
The staticfiles
app has a handful
of settings
that we need to consider to start.
# project/settings.py
...
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
Next,
we can define the URL path prefix
that Django will use
when it serves a static file.
Let’s says you have site.css
in the root
of your project’s static
directory.
You probably wouldn’t want the file
to be accessible as mysite.com/site.css
.
To do so would mean that static files could conflict
with URL paths
that your app might need to direct
to a view.
The STATIC_URL
setting lets us namespace our static files
and, as the Zen of Python says:
Namespaces are one honking great idea – let’s do more of those!
# project/settings.py
...
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
STATIC_URL = '/static/'
With STATIC_URL
set,
we can access site.css
from mysite.com/static/site.css
.
There’s one more crucial setting
that we need to set,
and it is called STATIC_ROOT
.
When we deploy our Django project,
Django wants to find all static files
from a single directory.
The reason for this is for efficiency.
It’s possible for Django
to search through all the app static
directories
and any directories set
in STATICFILES_DIRS
whenever it searches for a file
to serve,
but that would be slow.
Once we set STATIC_ROOT
,
Django will have the desired output location
for static files.
If you set the path somewhere
in your repository,
don’t forget to put that path
in your .gitignore
if you’re using version control
with Git
(and I highly recommend that you do!).
I happen to set my STATIC_ROOT
to a staticfiles
directory.
# project/settings.py
...
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
STATIC_URL = '/static/'
Working With Static Files
The primary way
of working with static files
is with a template tag.
The static
template tag will help render the proper URL
for a static file for your site.
{% load static %}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="{% static "css/site.css" %}">
</head>
<body>
<h1>Example of static template tag!</h1>
</body>
</html>
Since we know that STATIC_URL
is /static/
from the configuration section,
why don’t I hardcode the link tag path
to /static/css/site.css
?
You could,
and that might work,
but you’ll probably run into some long term problems.
- What if you ever wanted to change
STATIC_URL
? Maybe you want to change it to something shorter like/s/
. If you hardcode the name, now you have more than one place to change. - Using some extra features,
Django may change the name
of a file to something unique
by adding a hash to the file name.
With a hardcoded path of
/static/css/site.css
, this may lead to a 404 response if Django expects the unique name instead. We’ll see what the unique name is for in the next section.
We should remember
to use the static
tag
in the same way
that we use the url
tag
when we want to resolve a Django URL path.
Both of these tags help avoid harcoding paths
that can change.
# application/views.py
from django.http import JsonResponse
from django.templatetags.static import static
def get_css(request):
return JsonResponse({'css': static('css/site.css')})
Deployment Considerations
When you deploy your application
to a server,
one crucial setting
to disable
is the DEBUG
setting.
If DEBUG
is on,
all kinds of secret data can leak
from your application,
so the Django developers expect DEBUG
to be False
for your live site.
Because of this expectation,
certain parts of Django behave differently
when DEBUG
changes,
and the staticfiles
app is one such part.
When DEBUG
is True
and you are using the runserver
command
to run the development web server,
Django will search for files
using a set of “finders”
whenever a user requests a static file.
These finders are defined
by the STATICFILES_FINDERS
setting,
which defaults to:
[
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
As you might guess,
the FileSystemFinder
looks for any static files
found in the file system directory
that we listed in STATICFILES_DIRS
.
The AppDirectoriesFinder
looks for static files
in the static
directory
of each Django application
that you have.
You can see how this gets slow
when you realize
that Django will walk
through len(STATICFILES_DIRS) + len(INSTALLED_APPS)
before giving up
to find a single file.
collectstatic
will copy all the files it discovers
from iterating through each finder
and collecting files
from what a finder lists.
In my example below,
my Django project directory is myproject
,
and I set STATIC_ROOT
to staticfiles
.
$ ./manage.py collectstatic
42 static files copied to '/Users/matt/myproject/staticfiles'.
When deploying your application
to your server,
you would run collectstatic
before starting the web server.
By doing that,
you ensure that the web server
can access any static files
that the Django app might request.
Optimizing Performance In Django
The last setting we’ll consider is the STATICFILES_STORAGE
setting.
This setting controls how static files are stored and accessed
by Django.
We may want to change STATICFILES_STORAGE
to improve the efficiency
of the application.
The biggest boost we can get from this setting
will provide file caching.
In an ideal world, your application would only have to serve a static file exactly one time to a user’s browser. In that scenario, if an application needed to use the file again, then the browser would reuse the cached file that it already retrieved. The challenge that we have is that static files (ironically?) change over time.
The “trick” is to serve a “fingerprinted” version
of the file.
As a part of the deployment process,
we would like to uniquely identify each file
with some kind of version information.
An easy way for a computer to do this is
to take the file’s content
and calculate a hash value.
We can have code take site.css
,
calculate the hash,
and generate a file with the same content,
but with a different filename
like site.abcd1234.css
if abcd1234
was the generated hash value.
The next part of the process is
to make the template rendering use the site.abcd1234.css
name.
Remember how we used the static
template tag
instead of hardcoding /static/css/site.css
?
This example is a great reason why we did that.
By using the static
tag,
Django can render the filename
that includes the hash
instead of only using site.css
.
The final bit that brings this scheme together is
to tell the browser
to cache site.abcd1234.css
for a very long time
by sending back a certain caching header
in the HTTP response.
- If the user fetches
site.abcd1234.css
, their browser will keep it for a long time and never need to download it again. This can reused every time the user visits a page in your app. - If we ever change
site.css
, then the deployment process can generate a new file likesite.ef567890.css
. When the user makes a request, the HTML will include the new version, their browser won’t have it in the cache, and the browser will download the new version with your new changes.
Great!
How do we get this
and how much work is it going to require?
The answer comes back
to the STATICFILES_STORAGE
setting
and a tool called
WhiteNoise
(get it!? “white noise” is “static.” har har).
WhiteNoise is a pretty awesome piece of software. The library will handle that entire caching scheme that I described above.
To set up WhiteNoise,
you install it with pip install whitenoise
.
Then,
you need to change your MIDDLEWARE
list
and STATICFILES_STORAGE
settings.
# project/settings.py
...
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
# ...
]
STATICFILES_STORAGE = \
'whitenoise.storage.CompressedManifestStaticFilesStorage'
When your application runs,
the WhiteNoise middleware will handle which files
to serve.
Because files are static
and don’t require dynamic processing,
we include the middleware high
on the list
to skip a lot of needless extra Python processing.
In my configuration example,
I left the SecurityMiddleware
above WhiteNoise
so the app can still benefit
from certain security protections.
The scheme that I described is not the only way to handle static files. In fact, there are some tradeoffs to think about:
- Building with WhiteNoise means that we only need to deploy a single app and let Python handle all of the processing.
- Python, for all its benefits, is not the fastest programming language out there. Leaving Python to serve your static requests will run slower than some other methods. Additionally, your web server’s processes must spend time serving the static files rather than being fully devoted to dynamic requests.
Optimizing Performance With A Reverse Proxy
An alternative approach to using Django to serve static files is to use another program as a reverse proxy. This setup is more complex, but it can offer better performance if you need it. A reverse proxy is software that sits between your users and your Django application server. CloudFlare has a good article if you want to understand why “reverse” is in the name.
If you set up a reverse proxy,
you can instruct it
to handle many things,
including URL paths
coming to your site’s domain.
This is where STATIC_ROOT
and collectstatic
are useful outside of Django.
You can set a reverse proxy
to serve all the files
that Django collects
into STATIC_ROOT
.
The process is roughly:
- Run
collectstatic
to put files intoSTATIC_ROOT
. - Configure the reverse proxy
to handle any URL pattern
that starts with
STATIC_URL
(recall/static/
as an example) and pass those requests to the directory structure ofSTATIC_ROOT
. - Anything that doesn’t look
like a static file (e.g.,
/accounts/login/
) is delegated to the app server running Django.
In this setup, the Django app never has to worry about serving static files because the reverse proxy takes care of those requests before reaching the app server. The performance boost comes from the reverse proxy itself. Most reverse proxies are designed in very high performance languages like C because they are designed to handle a specific problem: routing requests. This flow lets Django handle the dynamic requests that it needs to and prevents the slower Python processes from doing work that reverse proxies are built for.
If this kind of setup appeals to you, one such reverse proxy that you can consider is Nginx. The configuration of Nginx is beyond the scope of this series, but there are plenty of solid tutorials that will show how to configure a Django app with Nginx.
Summary
In this episode, we covered static files.
We looked at:
- How to configure static files
- The way to work with static files
- How to handle static files when deploying your site to the internet
Next Time
On the next episode, we’re going to talk about testing your app. We’ll see how automated tests can provide you the peace of mind that your application works as you expect.
You can follow the show on Spotify. Or follow me or the show on X at @mblayman or @djangoriffs.
Please rate or review on Apple Podcasts, Spotify, or from wherever you listen to podcasts. Your rating will help others discover the podcast, and I would be very grateful.
Django Riffs is supported by listeners like you. If you can contribute financially to cover hosting and production costs, please check out my Patreon page to see how you can help out.