Compressed content¶
Static Content¶
To speed up transmission, thus reducing load times, it’s common to compress our data. When it comes to our static assets, we can spend some extra time to compress it heavily once, and serve it pre-compressed, instead of spending the effort to compress it every time we serve it.
The simplest way to get uWSGI to do this is with the static-gzip-all option,
which will mean any time we’re serving static assets, if the browser supports
it, uWSGI will check if there is the same filename with a .gz
extension.
If there is, it will serve that instead, with the headers to tell the browser
it’s compressed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | [uwsgi]
strict = true
master = true
http = :8000
processes = 4
cheaper = 1
threads = 2
pythonpath = code/
module = app
offload-threads = 1
check-static = static/
static-gzip-all = true
|
Now you can compress all your static assets with the following command:
$ find static/ \( -name "*.css" -or -name "*.js" \) -exec gzip -9fk {} +
Note
As of the 2.0.16 release of uWSGI it also supports Brotli compression,
selecting it over gzip if supported by the browser. It is enabled by the
static-gzip-all
flag, also.
Note
There are other tools which can provide gzip compatible files but compress better than gzip. These include advdef from the AdvanceCOMP suite, and zopfli from Google.
How much difference does it make?
For an example, let’s try a copy of Bootstrap’s CSS. I’ve grabbed the minified CSS for Bootstrap 3.3.7. Typically web servers will set gzip to level 5 or 6 to get good compression, without taking too long.
File | Size |
---|---|
bootstrap.min.css | 121200 |
gzip -1 | 25214 |
gzip -6 | 19610 |
gzip -9 | 19453 |
advdef -z4 | 18325 |
zopfli | 18302 |
As you can see, the improvement drops off quickly. But since we’re compressing it once, and serving it repeatedly, we can spend all the time we like compressing it.
Dynamic content¶
So this takes care of our static assets, but what about our dynamic content?
In this case, we can easily ask our HTTP worker do handle this for us - after all, all our responses will go through it anyway, and having a separate process do it frees up our app workers sooner.
First we enable http keepalive, then we allow auto gzip.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | [uwsgi]
strict = true
master = true
http = :8000
http-keepalive = 1
http-auto-gzip = true
processes = 4
cheaper = 1
threads = 2
pythonpath = code/
module = app
offload-threads = 1
check-static = static/
static-gzip-all = true
|
However, this isn’t quite enough yet. uWSGI won’t jsut attempt to compress all responses. We need to add a header to compressible responses to tell the HTTP worker we want it compressed.
For this, we’re going to use uWSGI’s internal routing feature. This lets us run some simple logic before and after requests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | [uwsgi]
strict = true
master = true
http = :8000
http-keepalive = 1
http-auto-gzip = true
processes = 4
cheaper = 1
threads = 2
pythonpath = code/
module = app
offload-threads = 1
check-static = static/
static-gzip-all = true
collect-header = Content-Type RESPONSE_CONTENT_TYPE
response-route-if = equal:${RESPONSE_CONTENT_TYPE};application/json addheader:uWSGI-Encoding: gzip
response-route-if = startswith:${RESPONSE_CONTENT_TYPE};text/html addheader:uWSGI-Encoding: gzip
|
These three lines do as follows:
- Instruct uWSGI to copy the Content-Type header from the response into a variable we’ve called RESPONSE_CONTENT_TYPE.
- Test if the new variable equals application/json, and if so add a new header.
- Test if the new variable contains text/html, and if so add a new header.
The reason for the different tests is that a text/html content type might include additional fields, like a charset declaration. JSON, on the other hand, is always UTF-8 encoded.
Now in the startup output you’ll see:
*** dumping internal response routing table ***
[rule: 0] subject: ${RESPONSE_CONTENT_TYPE};application/json func: equal action: addheader:uWSGI-Encoding: gzip
[rule: 1] subject: ${RESPONSE_CONTENT_TYPE};text/html func: startswith action: addheader:uWSGI-Encoding: gzip
*** end of the internal response routing table ***
If you now check the response headers you’ll see our new header and, for the
right content, a Content-Encoding: gzip
header.
This all leaves the uWSGI-Encoding
header in the resposne. If you want to
remove this we can add the routing line after the others:
response-route-run = delheader:uWSGI-Encoding