django-compressor: bundling CSS and JS inside a Django template tag
Compresses linked and inline javascript or CSS into a single cached file.
At a glance
- What is it?
- A template tag that parses your markup, concatenates and minifies it, and serves the result under a content hash. Old enough to predate bundlers, still maintained for Django 6.1.
- Who is it for?
- django-compressor solves asset bundling inside the server-rendered template, which is a narrower problem than a modern bundler solves and a better fit for the projects it was written for. The generated filename depends on content, which is what makes a far future expiry safe, and that property has not changed since the project began.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 35 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Everything between the compress tags becomes one file
The mechanism is a template tag, and the README describes the whole lifecycle in a few sentences. All HTML between `{% compress js %}` and `{% endcompress %}`, or the CSS equivalent, is parsed and searched for stylesheets and scripts. Those references are then run through configurable compilers and filters, and the result is either written out as a single file or inlined back into the template.
The final step is what makes it usable rather than merely interesting: the tag emits a `<script>` or `<link>` tag pointing at the optimized file, or it inlines the content directly. A project can therefore start with inline blocks and switch to a served file without rewriting the templates, which is a sensible default when you do not yet know how a page will be deployed.
The caching argument is the part that has aged best. Since the filename is derived from the content, the generated assets can carry a far future expiration date without any risk of a stale browser cache serving old code. That is the same property that later made every content-hashed build system standard, and django-compressor has had it since before that was the default expectation.
The other property worth noting is that the work does not have to happen per request. The concatenation and compression can be run once, outside the request and response cycle, with the `manage.py compress` management command. For a site with a large static surface, that moves the cost off the critical path entirely.
Compilers, filters and two dependency choices that matter
Built-in support covers more than minification. The README lists YUI CSS and JS compression, yUglify for the same, Google's Closure Compiler, a Python port of Douglas Crockford's JSmin, a Python port of the YUI CSS Compressor, and a filter that converts some images into data URIs. If your setup needs something else, a custom filter is a matter of extending one of the base classes.
The dependency list in `pyproject.toml` tells you which of those are pure Python and ship by default. It is four entries: `django>=5.2`, `django-appconf>=1.0.3`, `rcssmin>=1.2.1` and `rjsmin>=1.2.4`. The last two are the CSS and JavaScript minifiers implemented in Python, which means the default install needs no external binary and no JVM.
`django-appconf` is the other one worth understanding. It lets the package define its settings with defaults in code and let a project override them in Django settings, which is why django-compressor can ship a large set of configurable options without asking every project to declare them.
Python support starts at 3.10, running through 3.14, and the framework classifiers cover Django 5.2, 6.0 and 6.1. The build backend is setuptools with the version read from `compressor.__version__`, and the license field in the pyproject is MIT, maintained by Mathieu Pillard. The default branch is `develop`, which is worth knowing if you are reading source links, since they will not match the released code.
Four HTML parsers and the one you probably want
Parsing the template output is the step most likely to surprise people, because the tool has to cope with whatever your templates actually emitted. Django Compressor uses lxml when it is available and falls back to Python's built-in HTMLParser otherwise. It also offers a BeautifulSoup based parser and an html5lib based parser, plus an abstract base class so you can write your own.
That is four parsers for one job, and the choice has real consequences. A parser that does not understand a construct will either drop it, escape it, or pass it through, and each behaviour shows up as a broken stylesheet rather than an error. Installing lxml, which the project recommends when it is available, is the cheapest way to reduce that class of surprise.
The default CSS filter is the other quiet behaviour to know. It rewrites paths to static files so they become absolute, because after concatenation into a single file, a relative path no longer points at the right place. If you write your own filters, inheriting that behaviour is usually what you want.
The Makefile shows how the project tests this. Coverage runs the Django test runner against a test settings module, flake8 runs over the package ignoring E203, E501 and W503, and the default target chains all three:
test: flake8 runtests coveragereportA parser matrix is the kind of thing that needs exactly that kind of test suite, and the fact that it exists in a package this old is a reasonable signal about how the maintainers work.
Where it fits against a JavaScript bundler
This is the comparison that matters and the README does not make it, so it is worth being explicit. django-compressor takes markup that Django already rendered and processes it server side. A JavaScript bundler starts from a JavaScript entry point, walks an import graph, and emits a bundle as a build step. They are different tools pointed at overlapping problems.
The consequence is that django-compressor can combine a stylesheet, an inline block and a static file into one request without anyone writing a JavaScript entry module, and it works on templates that were written years before it existed. What it cannot do is anything that requires understanding module semantics. There is no dependency graph to analyse, so no tree shaking, no code splitting, no dynamic import handling, and no content hash negotiation per module.
The second difference is caching granularity. One hash per compress block means a change in one small stylesheet invalidates the whole combined file. On a large site with several blocks that is fine, because each block is usually scoped to a page type. On a single-page application front end it is the wrong shape entirely, and the tooling ecosystem there has moved on by a decade.
The third difference is timing. django-compressor runs during the request unless you pre-run `manage.py compress`, at which point it is a build step wearing a Django costume. That is a legitimate deployment model, and the existence of the management command means the project does not force you to pay the cost per page load.
Deployment concerns the README does not cover
The README stops at features, which means the operational questions have no answers there and you have to bring your own judgement. Two of them decide whether this package works in your deployment at all.
The first is where the generated files are written. The package needs a writable location and a URL that serves it, and in a container with immutable layers or a read-only filesystem that configuration is the whole ball game. The second is remote storage. Searches for this project cluster heavily around S3, which tells you that storing generated assets off the local filesystem is a common requirement, and the README does not address it, so it is something to verify against the settings documentation before committing to the package.
There is also the deployment ordering problem. If the management command runs as part of your release step, the generated assets have to exist before the new code serves requests that reference them, otherwise the first request after a deploy gets a 404 on a content-hashed filename that nothing has generated yet. Running compression lazily inside the request avoids that failure mode and reintroduces the per-request cost.
Caching headers are the third. The far future expiry the README recommends only pays off if your CDN and your origin agree on it, and if the static file view is not itself the thing overriding your headers.
On maintenance, the repository is not archived and the last push was on 2026-09-01, and the classifiers already list Django 6.1, which suggests the release caught up with the framework. There are no tagged releases in the repository listing, so pinning behaviour means pinning a PyPI version rather than tracking a branch.
When you should not reach for a template tag
There are three cases where this is the wrong choice, and all three come from the same architectural decision: the asset pipeline lives inside the template render.
The first is any project not using Django templates for its pages. If the HTML arrives from a React or Vue build, there is no template tag to hang the tag on, and you want a bundler. The second is when CSS or JS is genuinely dynamic per request, generated from user data or a CMS field. A content-hashed cache key means the cache cannot be shared across users, and you have built yourself a per-request compilation step.
The third is team friction, and it is the one least likely to appear in a README. The settings surface is large and configurable, the parser choice has four options, and the interaction between filters, compilers and storage backends is where the debugging happens. On a project where several people touch the frontend, a tool whose behaviour lives in template markup rather than in a build config can become the thing nobody wants to change.
On the security side, the package processes markup that your own templates generate, and it can inline processed content directly into the response. If you are ever feeding untrusted content into a compress block, that is a different problem from the one this tool addresses, and the README does not discuss it.
What the project does well is narrow and real: it makes one HTTP request per block, it gives you cache-safe filenames for free, and it does not require a second language toolchain to be installed. On a Django project with server-rendered pages and a handful of included stylesheets, that is exactly the right size of solution.
Editorial conclusion
django-compressor solves asset bundling inside the server-rendered template, which is a narrower problem than a modern bundler solves and a better fit for the projects it was written for. The generated filename depends on content, which is what makes a far future expiry safe, and that property has not changed since the project began. Its real limits are the ones the README is quiet about: no HTTP/2 concerns, no module graph, no tree shaking, and a dependency on external compressor binaries unless you stay with the pure Python defaults. Licence is MIT and the classifiers cover Django 5.2 through 6.1. Install the stable package, wire the parser and filter settings in `docs/settings.rst`, and only reach for the management command once your base template has settled.
Frequently asked questions
How do I install django-compressor?
Install the stable package with pip. It needs Python 3.10 or later and Django 5.2 or later, and it pulls in `django-appconf`, `rcssmin` and `rjsmin`. For the in-development version the README gives a git URL: `pip install git+https://github.com/django-compressor/django-compressor.git`.
How does the compress template tag work?
Everything between `{% compress js %}` or `{% compress css %}` and `{% endcompress %}` is parsed, the referenced files are concatenated and minified through configurable filters, and the tag then emits a `<script>` or `<link>` tag for the result or inlines it back into the template. The generated filename comes from the content, which is why it can carry a far future expiry date.
Can django-compressor store generated assets on S3?
The README does not describe a storage backend, so that has to be checked in the settings documentation rather than assumed. What the project documents is the storage configuration point itself, since generated files have to go somewhere your web server or CDN can serve. Searches around this project frequently pair it with S3, which suggests remote storage is a common requirement.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/django-compressor-django-compressor)